2018.12.02
文章目錄
前言
雖然Java 8早在2014年就已經發布了,但得益於它所帶來的新特性,使得Java重新煥發生機。
Java 8包含如下新特性1:
- Lambda表達式
- 方法引用
- 默認方法
- 新的
Streams
API Optional
- 新的
Date/Time
API - Narshorn,新的JS引擎
- 移除永久代(Permanent Generation)
- … …
Lambda表達式
定義
Lambda表達式,可以說就是爲單個方法的匿名類所提供的語法糖。Lambda表達式的支持有助於簡化Java代碼。編譯器會根據Lambda表達式的上下文來判斷所使用的***函數式接口***和參數的類型。對於特定方法,還能用***方法引用***進一步地簡化Lambda表達式的寫法。
語法
主要的語法就是“參數 -> 方法體”。Lambda表達式還有四條較爲重要的語法:
- 參數類型的聲明是可選的
- 當只有一個參數時,參數兩側的括號是可選的
- 方法體使用花括號是可選的(除非方法體包含多條語句)
- 當使用單個表達式返回返回值時,
return
關鍵字是可選的
Arrays.sort(strArray, (foo, bar) -> foo.length() - bar.length());
上面的例子中,Lambda表達式實現了Comparator
接口完成排序。
作用域
Lambda表達式中能引用final
變量或或者實際final
變量,所謂實際final
變量就是變量僅被賦值一次。例如:
// 正例
String sql = "delete * from User";
getHibernateTemplate().execute(session ->
session.createSQLQuery(sql).uniqueResult());
// 反例
String sql = "delete * from User";
getHibernateTemplate().execute(session ->
session.createSQLQuery(~~sql~~ ).uniqueResult());
sql = "select * from User";
Lambda表達式的極簡模式——方法引用
定義
方法引用,引用的是已存在的方法,可以算是Lambda表達式的一種簡化寫法。對於方法體中只調用某個已存在方法的Lambda表達式,就可以改寫爲直接引用該方法。
語法
通過::
進行方法引用,以下方法可被引用2:
- 靜態方法:
ContainingClass::staticMethodName
- 類實例的實例方法:
containingObject::instanceMethodName
- 類的實例方法:全稱是“特定類型的任意對象的實例方法”,
ContainingType::methodName
- 類/數組構造器(ie. TreeSet::new/TreeSet[]:new):
ClassName::new
“特定類型的任意對象的實例方法”,這句話容易和“類實例的實例方法”混淆。先說“類實例的實例方法”,很直觀,就是通過某個類實例引用它的實例方法,比如要對一個數組Clazz[] clazzArr
排序,我們可以實例化了一個Comparator<Clazz> comparator
並引用它的comparator::compare
方法進行排序;再說“特定類型的任意對象的實例方法”,還是對Clazz[] clazzArr
排序,但此時Clazz
實現了Comparable
接口,定義了Clazz::compareTo
方法,那對於數組裏的任意對象,都可以直接地引用它們自身的compareTo
實例方法比較大小,也就是引用Clazz
類的任意對象的實例方法。
但從使用者的角度來說,“特定類型的任意對象的實例方法”,其實就是通過類名引用實例方法——ContainingType::methodName
,所以我們可以直觀地理解爲“類的實例方法”。
Lambda表達式的類型——函數式接口
定義
既然有了Lambda表達式,那在Java裏如果聲明它們呢?於是就有了函數式接口。函數式接口是隻包含一個方法的接口,每個Lambda表達式,編譯器最後都會根據上下文判斷它所對應的函數式接口。
語法
Java 8在java.util.function
包中定義幾個函數式接口:
Function<T, R>
:接收T
類型的對象,並返回R
Supplier<T>
:返回T
類型的對象Predicate<T>
:基於T
類型的輸入,返回一個布爾值Consumer<T>
:對T
類型的對象執行一定的動作BiFunction<T, U, R>
BiConsumer<T, U, R>
示例如下:
// Function<T, R>
Function<String, Integer> length = String::length;
System.out.println(length.apply("GHD")); // 輸出3
// Consumer<T>
User user = new User("GHD");
Consumer<User> userConsumer = foo -> foo.setName("HD G.");
userConsumer.accept(user; // public interface Consumer<T> { void accept(T t);}
System.out.println(user.getName); // 輸出HD G.
Lambda鐵蹄踏遍Java
最適合應用Lambda表達式的場景,莫過於集合類的操作。前朝功臣Iterator
利用hasNext
和next
也曾立下過汗馬功勞,但寫法不簡練,並且不易於並行化。Java 8要順利地推廣Lambda表達式,就需要先從集合類下手。然而事情沒有這麼簡單,java.util.Collection
是個接口,不能實現方法,於是Java 8就引入了***默認方法***來解決這個問題。集合類的操作就通過Stream
API來拓展新特性。
默認方法
定義
默認方法,就是接口方法的默認實現。Java 8之所以要引入接口的默認方法,原因是在擴展java.util.Collection
的特性時,考慮到可能產生的兼容性問題,例如如果直接給java.util.Collection
添加一個新的方法,那麼所有實現了該接口的類,都需要實現新方法。於是在擴展類似java.util.Collection
的接口時,Java 8允許在接口裏定義默認方法,這樣所有實現該接口的類都自動添加了新方法的實現,以此實現“向後兼容”(Backwards Compatibility / Virtual Extension Methods)。
那可否用靜態方法來實現“向後兼容”呢?答案是不行3。類的靜態方法不能被子類繼承,因此子類都無法實現靜態方法;調用靜態方法時,也就不能通過子類名調用父類的靜態方法。
示例如下:
public interface Example {
default void newMethod() {
System.out.println("new method is here");
}
}
多個接口的繼承問題——鑽石問題
如果一個類實現了兩個接口,而這兩個接口都包含一個相同方法簽名的默認方法,這種情況就類似於“鑽石問題”4。鑽石問題描述的是下圖所示的繼承關係,B、C類都覆蓋了A類的方法,而D類同時繼承了B、C類,那麼通過D實例在調用該方法時,調用的是B類還是C類的方法?
-
情況一:如果兩個父接口都包含一個相同方法簽名的默認方法,那在編譯過程就會直接報錯。要解決這個問題,就需要在子類裏顯式地覆蓋這個方法。覆蓋時,方法體內可以同時調用父接口的
super
來顯式地調用默認方法。 -
情況二:如果類實現了一個包含默認方法的接口,同時繼承一個包含相同方法簽名的類,那麼編譯是可以通過的,子類會調用父類的方法,原因是“類優先”。之所以採用“類優先”的策略,原因可能也和“向後兼容”有關5,升級到Java 8後,即使被擴展的接口引入了新的默認方法,也要保證不會影響到那些實現該接口的類的正常使用。
接口的默認方法 V.S. 抽象類
接口 | 抽象類 | 備註 | |
---|---|---|---|
覆蓋方法中引用子類成員變量 | 否 | 是 | |
提供便利方法(Convenience Methods)6 | 是 | 是 | 例如,Arrays 和Conllections 所提供的方法,以及工廠方法 |
有構造器 | 否 | 是 | |
包含成員變量 | 否 | 是 |
接口的默認方法,本質上還是爲了實現“向後兼容”而出現的,爲Java 8和Lambda編程提供橋樑,與抽象類還是有比較大的區別的。
Stream
API
Stream
API包含兩類操作7:中間操作(Intermediate Operation)和終結操作(Terminal Operation)。Stream
API是懶加載模式,中間操作不會立即執行,只有到遇到第一個終結操作,中間操作纔會執行。中間操作包括map/filter/flatmap
等。