1. 程式人生 > >JDK8新特性一覽

JDK8新特性一覽

Jdk8新特性.png

下面對幾個常用的特性做下重點說明。

一、Lambda表示式

1.1 函數語言程式設計


百科介紹:http://baike.baidu.com/link?url=LL9X3-SoS4XJGgdzrXvURuKEGm6ad5zY1NLDxDygjTaSRnEZ0Bp3wqX0QgkB7fjPwMSQS1tLfqdRMKUhNti7MH7DEK7JQ_lXcs9k6LXHT1A9dSJW8uwJMONJcvXY53h6myMCkqjL3IqW8QRgbdNDl_
函式程式設計非常關鍵的幾個特性如下:
(1)閉包與高階函式
函式程式設計支援函式作為第一類物件,有時稱為 
閉包或者 仿函式(functor)物件。實質上,閉包是起函式的作用並可以像物件一樣操作的物件。
與此類似,FP 語言支援 高階函式。高階函式可以用另一個函式(間接地,用一個表示式) 作為其輸入引數,在某些情況下,它甚至返回一個函式作為其輸出引數。這兩種結構結合在一起使得可以用優雅的方式進行模組化程式設計,這是使用 FP 的最大好處。
(2)惰性計算
在惰性計算中,表示式不是在繫結到變數時立即計算,而是在求值程式需要產生表示式的值時進行計算。延遲的計算使您可以編寫可能潛在地生成無窮輸出的函式。因為不會計算多於程式的其餘部分所需要的值,所以不需要擔心由無窮計算所導致的 out-of-memory 錯誤。

(3)沒有“副作用”
所謂"副作用"(side effect),指的是函式內部與外部互動(最典型的情況,就是修改全域性變數的值),產生運算以外的其他結果。函數語言程式設計強調沒有"副作用",意味著函式要保持獨立,所有功能就是返回一個新的值,沒有其他行為,尤其是不得修改外部變數的值。
綜上所述,函數語言程式設計可以簡言之是: 使用不可變值和函式, 函式對一個值進行處理, 對映成另一個值。這個值在面嚮物件語言中可以理解為物件,另外這個值還可以作為函式的輸入。

1.2 Lambda表示式


官方教程地址

1.2.1 語法


完整的Lambda表示式由三部分組成:引數列表、箭頭、宣告語句;
(Type1 param1, Type2 param2, ..., TypeN paramN)
-> { statment1; statment2; //............. return statmentM;}

1. 絕大多數情況,編譯器都可以從上下文環境中推斷出lambda表示式的引數型別,所以引數可以省略:
(param1,param2, ..., paramN) -> {  statment1;  statment2;  //.............  return statmentM;}

2、 當lambda表示式的引數個數只有一個,可以省略小括號:
param1 -> {  statment1;  statment2;  //.............  return statmentM;}

3、 當lambda表示式只包含一條語句時,可以省略大括號、return和語句結尾的分號:
param1 -> statment

這個時候JVM會自動計算表示式值並返回,另外這種形式還有一種更簡寫法,方法引用寫法,具體可以看下面的方法引用的部分。

1.2.2 函式介面

函式介面是隻有一個抽象方法的介面, 用作 Lambda 表示式的返回型別。
介面包路徑為java.lang.function,然後介面類上面都有@FunctionalInterface這個註解。下面列舉幾個較常見的介面類。
Paste_Image.png
這些函式介面在使用Lambda表示式時做為返回型別,JDK定義了很多現在的函式介面,實際自己也可以定義介面去做為表示式的返回,只是大多數情況下JDK定義的直接拿來就可以用了。而且這些介面在JDK8集合類使用流操作時大量被使用。

1.2.3 型別檢查、型別推斷


Java編譯器根據 Lambda 表示式上下文資訊就能推斷出引數的正確型別。 程式依然要經過型別檢查來保證執行的安全性, 但不用再顯式宣告型別罷了。 這就是所謂的型別推斷。Lambda 表示式中的型別推斷, 實際上是 Java 7 就引入的目標型別推斷的擴充套件。
Image.png
有時候顯式寫出型別更易讀,有時候去掉它們更易讀。沒有什麼法則說哪種更好;對於如何讓程式碼更易讀,程式設計師必須做出自己的選擇。

1.2.4 區域性變數限制


Lambda表示式也允許使用自由變數(不是引數,而是在外層作用域中定義的變數),就像匿名類一樣。 它們被稱作捕獲Lambda。 Lambda可以沒有限制地捕獲(也就是在其主體中引用)例項變數和靜態變數。但區域性變數必須顯式宣告為final,或事實上是final。
為什麼區域性變數有這些限制?
(1)例項變數和區域性變數背後的實現有一個關鍵不同。例項變數都儲存在堆中,而區域性變數則儲存在棧上。如果Lambda可以直接訪問區域性變數,而且Lambda是在一個執行緒中使用的,則使用Lambda的執行緒,可能會在分配該變數的執行緒將這個變數收回之後,去訪問該變數。因此, Java在訪問自由區域性變數時,實際上是在訪問它的副本,而不是訪問原始變數。如果區域性變數僅僅賦值一次那就沒有什麼區別了——因此就有了這個限制。
(2)這一限制不鼓勵你使用改變外部變數的典型指令式程式設計模式。

1.2.5 使用示例


List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
long num = list.stream().filter( a -> a > 4 ).count();
System.out.println(num);

上面這段是統計list中大於4的值的個數,使用的lambda表示式為a-> a> 4,這裡引數a沒有定義型別,會自動判斷為Integer型別,而這個表示式的值會自動轉化成函式介面Predicate對應的物件(filter方法定義的輸入引數型別),至於stream及相關的操作則是下面要說的流操作。它們經常一起配合進行一起資料處理。

二、流

2.1 流介紹

流是Java API的新成員,它允許你以宣告性方式處理資料集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷資料集的高階迭代器。此外,流還可以透明地並行處理,你無需寫任何多執行緒程式碼了!

2.2 使用流

類別 方法名 方法簽名 作用
篩選切片 filter Stream<T> filter(Predicate<? super T> predicate) 過濾操作,根據Predicate判斷結果保留為真的資料,返回結果仍然是流
distinct Stream<T> distinct() 去重操作,篩選出不重複的結果,返回結果仍然是流
limit Stream<T> limit(long maxSize) 擷取限制操作,只取前 maxSize條資料,返回結果仍然是流
skip Stream<T> skip(long n) 跳過操作,跳過n條資料,取後面的資料,返回結果仍然是流
對映 map <R> Stream<R> map(Function<? super T, ? extends R> mapper) 轉化操作,根據引數T,轉化成R型別,返回結果仍然是流
flatMap <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) 轉化操作,根據引數T,轉化成R型別流,這裡會生成多個R型別流,返回結果仍然是流
匹配 anyMatch boolean anyMatch(Predicate<? super T> predicate) 判斷是否有一條匹配,根據Predicate判斷結果中是否有一條匹配成功
allMatch boolean allMatch(Predicate<? super T> predicate) 判斷是否全都匹配,根據Predicate判斷結果中是否全部匹配成功
noneMatch boolean noneMatch(Predicate<? super T> predicate) 判斷是否一條都不匹配,根據Predicate判斷結果中是否所有的都不匹配
查詢 findAny Optional<T> findAny() 查詢操作, 查詢當前流中的任意元素並返回Optional
findFirst Optional<T> findFirst() 查詢操作, 查詢當前流中的第一個元素並返回Optional
歸約 reduce T reduce(T identity, BinaryOperator<T> accumulator); 歸約操作,同樣兩個型別的資料進行操作後返回相同型別的結果。比如兩個整數相加、相乘等。
max Optional<T> max(Comparator<? super T> comparator) 求最大值,根據Comparator計算的比較結果得到最大值
min Optional<T> min(Comparator<? super T> comparator) 求最小值,根據Comparator計算的比較結果得到最小值
彙總統計 collect <R, A> R collect(Collector<? super T, A, R> collector) 彙總操作,彙總對應的處理結果。這裡經常與
count long count() 統計流中資料數量
遍歷 foreach void forEach(Consumer<? super T> action) 遍歷操作,遍歷執行Consumer 對應的操作

上面是Stream API的一些常用操作,按場景結合lambda表示式呼叫對應方法即可。至於Stream的生成方式,Stream的of方法或者Collection介面實現類的stream方法都可以獲得對應的流物件,再進一步根據需要做對應處理。

另外上述方法如果返回是Stream物件時是可以鏈式呼叫的,這個時候這個操作只是宣告或者配方,不產生新的集合,這種型別的方法是惰性求值方法;有些方法返回結果非Stream型別,則是及早求值方法。

“為什麼要區分惰性求值和及早求值? 只有在對需要什麼樣的結果和操 作有了更多瞭解之後, 才能更有效率地進行計算。 例如, 如果要找出大於 10 的第一個數字, 那麼並不需要和所有元素去做比較, 只要找出第一個匹配的元素就夠了。 這也意味著可以在集合類上級聯多種操作, 但迭代只需一次。這也是函式程式設計中惰性計算的特性,即只在需要產生表示式的值時進行計算。這樣程式碼更加清晰,而且省掉了多餘的操作。

這裡還對上述列表操作中相關的Optional與Collectors類做下說明。

Optional類是為了解決經常遇到的NullPointerException出現的,這個類是一個可能包含空值的容器類。用Optional替代null可以顯示說明結果可能為空或不為空,再使用時使用isPresent方法判斷就可以避免直接呼叫的空指標異常。

Collectors類是一個非常有用的是歸約操作工具類,工具類中的方法常與流的collect方法結合使用。比如
groupingBy方法可以用來分組,在轉化Map時非常實用;partitioningBy方法可以用來分割槽(分割槽可以當做一種特殊的分組,真假值分組),joining方法可以用來連線,這個應用在比如字串拼接的場景。

2.3 並行流

Collection介面的實現類呼叫parallelStream方法就可以實現並行流,相應地也獲得了平行計算的能力。或者Stream介面的實現呼叫parallel方法也可以得到並行流。並行流實現機制是基於fork/join 框架,將問題分解再合併處理。

不過平行計算是否一定比序列快呢?這也不一定。實際影響效能的點包括:
(1)資料大小輸入資料的大小會影響並行化處理對效能的提升。 將問題分解之後並行化處理, 再將結果合併會帶來額外的開銷。 因此只有資料足夠大、 每個資料處理管道花費的時間足夠多
時, 並行化處理才有意義。
(2) 源資料結構
每個管道的操作都基於一些初始資料來源, 通常是集合。 將不同的資料來源分割相對容易,這裡的開銷影響了在管道中並行處理資料時到底能帶來多少效能上的提升。
(3) 裝箱
處理基本型別比處理裝箱型別要快。
(4) 核的數量
極端情況下, 只有一個核, 因此完全沒必要並行化。 顯然, 擁有的核越多, 獲得潛在效能提升的幅度就越大。 在實踐中, 核的數量不單指你的機器上有多少核, 更是指執行時你的機器能使用多少核。 這也就是說同時執行的其他程序, 或者執行緒關聯性( 強制執行緒在某些核或 CPU 上執行) 會影響效能。
(5) 單元處理開銷
比如資料大小, 這是一場並行執行花費時間和分解合併操作開銷之間的戰爭。 花在流中
每個元素身上的時間越長, 並行操作帶來的效能提升越明顯

實際在考慮是否使用並行時需要考慮上面的要素。在討論流中單獨操作每一塊的種類時, 可以分成兩種不同的操作: 無狀態的和有狀態的。無狀態操作整個過程中不必維護狀態, 有狀態操作則有維護狀態所需的開銷和限制。如果能避開有狀態, 選用無狀態操作, 就能獲得更好的並行效能。 無狀態操作包括 map、filter 和 flatMap, 有狀態操作包括 sorted、 distinct 和 limit。這種理解在理論上是更好的,當然實際使用還是以測試結果最為可靠 。

三、方法引用

方法引用的基本思想是,如果一個Lambda代表的只是“直接呼叫這個方法”,那最好還是用名稱來呼叫它,而不是去描述如何呼叫它。事實上,方法引用就是讓你根據已有的方法實現來建立Lambda表示式。但是,顯式地指明方法的名稱,你的程式碼的可讀性會更好。所以方法引用只是在內容中只有一個表示式的簡寫。

當 你 需 要使用 方 法 引用時 , 目 標引用 放 在 分隔符::前 ,方法 的 名 稱放在 後 面 ,即ClassName :: methodName 。例如 ,Apple::getWeight就是引用了Apple類中定義的方法getWeight。請記住,不需要括號,因為你沒有實際呼叫這個方法。方法引用就是Lambda表示式(Apple a) -> a.getWeight()的快捷寫法。

這裡有種情況需要特殊說明,就是類的建構函式情況,這個時候是通過ClassName::new這種形式建立Class建構函式對應的引用,例如:

Image.png

四、預設方法

4.1 介紹


為了以相容方式改進API,Java 8中加入了預設方法。主要是為了支援庫設計師,讓他們能夠寫出更容易改進的介面。具體寫法是在介面中加default關鍵字修飾。

4.2 使用說明


預設方法由於是為了避免相容方式改進API才引入,所以一般正常開發中不會使用,除非你也想改進API,而不影響老的介面實現。當然在JDK8有大量的地方用到了預設方法,所以對這種寫法有一定的瞭解還是有幫助的。
採用預設方法之後,你可以為這個方法提供一個預設的實現,這樣實體類就無需在自己的實現中顯式地提供一個空方法,而是預設就有了實現。

4.3 注意事項


由於類可以實現多個介面,也可以繼承類,當介面或類中有相同函式簽名的方法時,這個時候到底使用哪個類或介面的實現呢?
這裡有三個規則可以進行判斷:
(1) 類中的方法優先順序最高。類或父類中宣告的方法的優先順序高於任何宣告為預設方法的優先順序。
(2) 如果無法依據第一條進行判斷,那麼子介面的優先順序更高:函式簽名相同時,優先選擇擁有最具體實現的預設方法的介面,即如果B繼承了A,那麼B就比A更加具體。
(3) 最後,如果還是無法判斷,繼承了多個介面的類必須通過顯式覆蓋和呼叫期望的方法,顯式地選擇使用哪一個預設方法的實現。不然編譯都會報錯。

五、方法引數反射


官方教程地址

JDK8 新增了Method.getParameters方法,可以獲取引數資訊,包括引數名稱。不過為了避免.class檔案因為保留引數名而導致.class檔案過大或者佔用更多的記憶體,另外也避免有些引數( secret/password)洩露安全資訊,JVM預設編譯出的class檔案是不會保留引數名這個資訊的。

這一選項需由編譯開關 javac -parameters 開啟,預設是關閉的。在Eclipse(或者基於Eclipse的IDE)中可以如下圖勾選儲存:

Image.png

六、日期/時間改進


1.8之前JDK自帶的日期處理類非常不方便,我們處理的時候經常是使用的第三方工具包,比如commons-lang包等。不過1.8出現之後這個改觀了很多,比如日期時間的建立、比較、調整、格式化、時間間隔等。
這些類都在java.time包下。比原來實用了很多。

6.1 LocalDate/LocalTime/LocalDateTime

LocalDate為日期處理類、LocalTime為時間處理類、LocalDateTime為日期時間處理類,方法都類似,具體可以看API文件或原始碼,選取幾個代表性的方法做下介紹。

now相關的方法可以獲取當前日期或時間,of方法可以建立對應的日期或時間,parse方法可以解析日期或時間,get方法可以獲取日期或時間資訊,with方法可以設定日期或時間資訊,plus或minus方法可以增減日期或時間資訊;

6.2 TemporalAdjusters

這個類在日期調整時非常有用,比如得到當月的第一天、最後一天,當年的第一天、最後一天,下一週或前一週的某天等。

6.3 DateTimeFormatter


以前日期格式化一般用SimpleDateFormat類,但是不怎麼好用,現在1.8引入了DateTimeFormatter類,預設定義了很多常量格式(ISO打頭的),在使用的時候一般配合LocalDate/LocalTime/LocalDateTime使用,比如想把當前日期格式化成yyyy-MM-dd hh:mm:ss的形式:

LocalDateTime dt = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");       
System.out.println(dtf.format(dt));

七、參考資料

《Java 8實戰》

《Java 8函數語言程式設計》