注:之前關(guān)于Java8的認知一直停留在知道有哪些修改和新的API上,對Lambda的認識也是僅僅限于對匿名內(nèi)部類的改進。最近同事有分享了Java8 Stream的用法,講得十分詳盡,激發(fā)了我對Java8濃厚的興趣,找到了JSR-335 Lambda負責人Brian Goetz關(guān)于Lambda的一些文檔,嘗試進行翻譯權(quán)當練習(xí)加深理解和個人筆記。
openjdk 原文鏈接 http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html
參考文章 http://lucida.me/blog/java-8-lambdas-insideout-language-features/
(以下為原文)
Java SE8
本文是對OpenJDK Lambda項目JSR 335所增加功能的非正式概述,對2011年12月發(fā)布的上一次迭代進行了改進窿吩。一些語言變化的正式描述和OpenJDK開發(fā)人員預(yù)覽可以在JSR的早期草案中找到茎杂。額外的歷史設(shè)計文檔可以在OpenJDK項目頁面找到。還有一個附帶Lambda庫版本增強的介紹文檔纫雁,描述了作為JSR 335的一部分添加的庫增強功能煌往。
Lambda項目的主要目標是讓"代碼即數(shù)據(jù)"這個模式在Java中方便易用并且使其深入人心。主要的新語言特點包括:
1.Lambda表達式(非官方的,"閉包"(closures)或者"匿名方法"(anonymous methods))
2.方法和構(gòu)造器引用
3.拓展的目標類型和目標引用
4.接口中的默認和靜態(tài)方法
詳細描述請參照下文:
1. 背景
Java是一門面向?qū)ο笳Z言轧邪。 在函數(shù)式和面向?qū)ο笳Z言中, 基礎(chǔ)元素都可以動態(tài)封裝程序行為: 面向?qū)ο笳Z言使用含有方法(method)的對象(object), 函數(shù)式語言則使用函數(shù)(function)刽脖。看起來他們似乎沒有什么共同點或者相似度, 這是因為java的對象相對比較"重量級":對于單獨聲明的類的實例化 (instantiations)包含了大量的字段和方法忌愚。
我們經(jīng)城埽可以見到某個對象只是簡單的封裝了一個函數(shù), 最典型的例子就是 Java API 定義的接口(Interface)硕糊, 有時被稱為"回調(diào)接口"院水。用戶需要通過提供一個實例來調(diào)用這個API
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
對于只是為了在使用的時候?qū)嵗淮蔚那闆r, 我們通常實例化一個匿名內(nèi)部類來實現(xiàn)這個接口而不是再去額外聲明一個類。
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});
很多常用的類庫都依賴于這種特性,尤其是在并發(fā)API中, 代碼在執(zhí)行的時候必須在線程間保持獨立简十。 根據(jù)摩爾定律,我們總是容易得到更多而不是更快的處理器,而串行API的處理能力非常有限,因此并發(fā)編程變得異常重要檬某。
由于回調(diào)的方式的編碼風格越來越貼近于函數(shù)式編程,Java中的代碼輕量化變得非常重要勺远。因此匿名內(nèi)部類并不是一個很好的選擇, 原因主要有以下幾點:
- 語法冗余
- 匿名類中成員名稱和this非常容易跟外部混淆
- 不靈活的類加載以及實例創(chuàng)建機制
- 不能夠捕獲非final的局部變量
- 在流程控制上不夠抽象化
這個項目解決了以上的很多問題橙喘。引入了由作用域規(guī)則組成的新的更精確的表達式解決了1)和2)
定義了更為靈活以及易于優(yōu)化的表達式機制回避了3),通過允許編譯器推斷變量的不可變性(允許捕獲有效的final局部變量,并不一定要有final關(guān)鍵字)改善了問題4)
不過此項目的目的并不是解決內(nèi)部類帶來的所有問題, 比如捕獲可變變量4)或者非局部的流程控制5)并不在范疇以內(nèi)(未來可能會提供對這些特性的支持)
2.函數(shù)式接口
盡管有著自身的局限性,匿名內(nèi)部類對Java的類型系統(tǒng)有很好的適應(yīng)性:一個函數(shù)對應(yīng)了一個接口類型。這個特性使用起來非常便利:
接口已經(jīng)是Java類型系統(tǒng)(type system)的一部分
接口天然具有運行時表示( runtime representation)的特性
接口還可以通過Javadoc注釋來表達一些非正式的約定胶逢,比如斷言操作是可交換的
ActionListener這個接口只有一個方法, 很多回調(diào)式接口都有這個特性, 比如Runnable和Comparator厅瞎。 我們給這種只有一個方法的接口統(tǒng)一命名為函數(shù)式接口(之前被叫做SAM 類型, "單一抽象方法" Single Abstract Method)
聲明函數(shù)式接口的時候不需要做額外的工作, 編譯器會根據(jù)接口的結(jié)構(gòu)進行識別(識別過程不是簡單去數(shù)方法個數(shù),接口還有可能冗余的聲明了Object提供的方法,比如toString(),或者聲明了靜態(tài)或者默認方法,這些都不在"只有一個方法"這個限定條件中)。但是API的作者可以根據(jù)@FunctionalInterface 來得知接口是設(shè)計為函數(shù)式的 (而不是恰好只有一個方法), 有了這個注釋, 編譯器會驗證接口是不是滿足函數(shù)式接口的結(jié)構(gòu)初坠。
之前有一種代替(或者是補充)函數(shù)式類型的提議是引入新的結(jié)構(gòu)化類型,被叫做”箭頭類型”(arrow types)和簸。例如一個可以將String和Object轉(zhuǎn)為一個int的函數(shù)可以表達如下 (String,Object)->int。這個想法在充分考慮后放棄了,至少現(xiàn)在是這樣, 因為它有以下不足:
- 增加了類型系統(tǒng)的復(fù)雜度, 以及混合使用結(jié)構(gòu) (structural) 類型和名義 (nominal) 類型 (Java幾乎全部是名義類型)
- 導(dǎo)致不同庫的代碼風格差異, 一些庫會繼續(xù)使用回調(diào)接口, 與此同時另外一些庫會使用結(jié)構(gòu)函數(shù)類型
- 語法變的非常笨拙, 尤其是包含了受檢異常 (checked exceptions) 以后
- 對于每個不同的函數(shù)類型, 不太可能會有一個運行時表示, 這意味著開發(fā)者會受到類型擦除的困擾和限制碟刺。比如我們不太可能對方法m(T->U)和m(X->Y) 方法重載
因此,我們采用了"使用你所知"(use what you know)的方式锁保。 因為現(xiàn)有的庫廣泛使用函數(shù)式接口,所以我們整理并利用了這個模式。這使得現(xiàn)有的庫可以和Lambda表達式一起使用半沽。
為了說明這一點爽柒,這里有一些已經(jīng)在Java7中存在的接口怎樣適用于新的特性的例子
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener
除此之外, Java 8 新增了一個新的包 java.util.function
包含了一些常用的函數(shù)式接口,例如:
Predicate<T> -- 輸入T,返回boolean
Consumer<T> -- 輸入T, 進行邏輯操作, 沒有返回值
Function<T,R> -- 輸入T, 返回R
Supplier<T> -- 提供一個T的實例 (比如工廠)
UnaryOperator<T> --輸入T者填,返回T
BinaryOperator<T> -- 輸入 (T, T) 浩村,返回 T
除了這些基本的接口,還有一些對于基本類型的特殊處理接口,比如 IntSupplier 或LongBinaryOperator(我們并沒有提供所有基礎(chǔ)類型的特殊處理,只有int, long和double,其他的基本類型可以通過其轉(zhuǎn)化得來)。與此類似的還有一些對于多個參數(shù)的特殊處理, 比如BiFunction<T,U,R>, 代表了將輸入(T,U)轉(zhuǎn)換為返回結(jié)果R
3. Lamda表達式
匿名內(nèi)部類帶來的最大的問題就是笨重,也可以叫做代碼的”高度”問題(vertical problem), 比如前面的ActionListener接口使用了5行來封裝了一個很簡單的行為占哟。
Lamda表達式是一種匿名方法心墅,使用更為輕量級的機制替代匿名內(nèi)部類解決這個問題酿矢。下面列出了一些Lambda表達式的例子:
(int x, int y) -> x + y x, y作為入?yún)ⅲ祷豿+y
() -> 42 沒有入?yún)? 返回42
(String s) -> { System.out.println(s); } s作為入?yún)?打印s, 沒有返回值
Lambda在語法上通常包含了一個參數(shù)列表 (argument list), 一個箭頭符號 -> 和函數(shù)體 (body) 怎燥。函數(shù)體可以是一個表達式也可以是一個聲明語句瘫筐。在表達式中, 函數(shù)體被計算并且返回。在函數(shù)體中的計算類似于在方法中,return關(guān)鍵字會把控制權(quán)交給匿名函數(shù)的調(diào)用者;break和continue只能使用在循環(huán)(loop)中;如果函數(shù)體需要計算出一個結(jié)果,函數(shù)內(nèi)每一條邏輯路徑必須提供一個返回值或者拋出異常铐姚。
在通常情況下,Lambda表達式的語法被優(yōu)化的非常精簡策肝。比如消除了 “return” 關(guān)鍵字, 相對于Lambda表達式的大小而言,return在語法上已經(jīng)十分復(fù)雜了。
Lambda表達式會經(jīng)常出現(xiàn)在嵌套的上下文 (nested contexts) 中, 比如作為方法調(diào)用的參數(shù)或者另外一個Lambda表達式的結(jié)果隐绵。為了減少這種情況帶來的干擾驳糯,我們刪去了一些不必要的分隔符。但是在一些我們需要將Lambda表達式分離開來的情況下, 我們可以將像其他表達式那樣將它放在括號里氢橙。
下面是一些Lambda表達式在聲明中出現(xiàn)的例子
FileFilter java = (File f) -> f.getName().endsWith(".java");
String user = doPrivileged(() -> System.getProperty("user.name"));
new Thread(() -> {
connectToService();
sendNotification();
}).start();
4.目標類型
需要注意的是函數(shù)式接口本身并不是Lambda表達式語法的一部分酝枢。所以Lambda表達式到底代表了什么?它所代表的類型是從上下文中推斷出來的悍手。舉例說明, 下面的Lambda表達式就代表了一個ActionListener:
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
這種做法意味著同一個Lambda表達式在不同的上下文中可以表示不同的類型帘睦。
Callable<String> c = () -> "done";
PrivilegedAction<String> a = () -> "done";
() -> "done"; 在第一個例子中表示了一個Callable接口的實例, 而在第二個例子中則表示了一個PrivilegedAction的實例。
編譯器負責推斷Lambda表達式的類型,使用表達式所在上下文中的期待(expected)類型, 我們把它叫做目標類型 (target type) 坦康。Lambda表達式只能出現(xiàn)在目標類型是一個函數(shù)式接口的上下文中竣付。
當然,沒有一個Lambda表達式可以適用于每一種可能的目標類型。編譯器會去檢查Lambda表達式使用的類系是否和目標類型的方法簽名 (method signature) 一致滞欠。也就是說, 如果下列條件滿足的話, Lambda表達式的目標類型可以推斷為T
- T是函數(shù)式接口類型
- Lambda表達式和T的參數(shù)數(shù)量相同, 并且參數(shù)類型也相同古胆。
- Lambda表達式的返回類型和T的返回類型一樣
- Lambda表達式拋出的異常和T的throws 拋出的異常兼容
一個函數(shù)式接口已經(jīng)知曉Lambda表達式應(yīng)該具有哪些參數(shù)類型,因此我們無須贅述。Lambda的參數(shù)類型可以由目標類型推斷出來筛璧。
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
編譯器可以推斷出s1和s2是string類型逸绎。此外, 當只有一個參數(shù)需要推斷的時候, () 就可以省略掉。
FileFilter java = f -> f.getName().endsWith(".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));
這些改進體現(xiàn)了我們的設(shè)計目標: 不把垂直問題轉(zhuǎn)化為水平問題夭谤。我們希望讀者在閱讀和使用Lambda的時候盡可能的閱讀少的代碼棺牧。
Lambda表達式不是第一個上下文相關(guān)類型的Java表達式: 范型方法 (generic method) 調(diào)用和<> 構(gòu)造函數(shù)調(diào)用, 這些都是類似的的基于賦值類型的目標類型檢查的例子。
List<String> ls = Collections.emptyList();
List<Integer> li = Collections.emptyList();
Map<String,Integer> m1 = new HashMap<>();
Map<Integer,String> m2 = new HashMap<>();
5. 目標類型的上下文
我們在之前提到Lambda表達式只能出現(xiàn)在具有目標類型的上下文中,列舉如下:
- 變量聲明
- 賦值
- 返回語句
- 數(shù)組的初始化
- 方法或者構(gòu)造器參數(shù)
- Lambda表達式體
- 條件表達式(:?)
- 類型(Cast)轉(zhuǎn)換表達式
在開始的3個例子中,目標類型只是簡單的被賦值或者被返回的類型
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
public Runnable toDoLater() {
return () -> {
System.out.println("later");
};
}
數(shù)組初始化的上下文有點像賦值,除了"變量" (variable) 是一個數(shù)組元素并且它的類型從數(shù)組類型中推斷而出朗儒。
filterFiles(new FileFilter[] {
f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")
});
在方法的參數(shù)這個例子中情況變得更為復(fù)雜,目標類型的推斷還需要依據(jù)其他兩個語言特性,重載的解析(overload resolution)和類型參數(shù)推斷(type argument inference)颊乘。
重載解析包括了找到特定方法調(diào)用(method invocation)的最合適的方法聲明。因為不同的聲明可以有不同的特征, 這點可以影響到Lambda表達式使用的參數(shù)的目標類型醉锄。編譯器會使用已知的Lambda表達式的信息來做出選擇乏悄。如果Lambda表達式參數(shù)是顯式類型 (explicitly typed) (明確指定了參數(shù)類型), 那么編譯器不僅僅知道參數(shù)的類型, 同時也知道了表達式體內(nèi)的所有返回表達式的類型。如果Lambda是隱型類型(implicitly typed)(參數(shù)類型推斷而來) ,重載解析會忽略掉Lambda主體而只使用Lambda的參數(shù)數(shù)量來判斷恳不。
如果在尋找最合適的方法聲明時存在歧義(ambiguous),類型轉(zhuǎn)換或者顯式Lambda可以給編譯器提供額外的類型信息檩小。如果Lambda的返回目標類型依賴于推斷出的參數(shù)類型, 那么Lambda函數(shù)體也可能給編譯器提供信息用于推斷參數(shù)類型
List<Person> ps = ...String<String> names = ps.stream().map(p -> p.getName());
ps 是一個Person的List, 所以ps.stream()是一個Person類型的Stream。 map()方法是一個R的泛型,它的參數(shù)是一個Function<T,R>, T是這個stream的元素類型(在這里我們已經(jīng)知道T是Person)妆够。當我們確定了重載類型并且知道了Lambda的目標類型,我們接下來需要去推斷R;經(jīng)過我們對Lambda表達式的類型檢查,發(fā)現(xiàn)返回類型是String, R也就是String,因此這個map()表達式的類型是一個String的Stream识啦。大多數(shù)情況下, 編譯器已經(jīng)可以完成推斷工作, 但是如果依舊不行的話, 我們可以通過一個顯式的Lambda (給參數(shù)p賦予一個顯式類型) 來提供額外的類型信息。比如將Lambda轉(zhuǎn)換為一個顯式的目標類型Function<Person,String>或者為泛型參數(shù)R提供一個顯式的參數(shù)類型(.<String>map(p -> p.getName()))神妹。
Lambda的方法體本身也可提供目標類型, 我們通過外部的目標類型來得出內(nèi)部的返回類型颓哮。這樣我們可以很方便的寫出一些可以返回其他函數(shù)的函數(shù)。
Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };
與此類似,條件表達式可以從上下文中”向下”傳遞目標類型鸵荠。
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);
最后,如果從上下文中很難推斷出目標類型,表達式的類型轉(zhuǎn)換(cast)提供了顯式指定Lambda的目標類型的機制冕茅。
// Illegal: Object o = () -> { System.out.println("hi"); };
Object o = (Runnable) () -> { System.out.println("hi"); };
對于方法聲明被不相關(guān)的函數(shù)接口類型重載引起的歧義,類型轉(zhuǎn)換會非常有幫助。
編譯器的目標類型推斷并不僅限于Lambda, 泛型方法調(diào)用和<>構(gòu)造器調(diào)用同樣利用了這個機制蛹找。下列語法在JAVA7中非法, 但是在JAVA8中可以使用姨伤。
List<String> ls = Collections.checkedList(new ArrayList<>(), String.class);
Set<Integer> si = flag ? Collections.singleton(23): Collections.emptySet();
6. 詞法作用域
在內(nèi)部類中確定命名(name)(和this關(guān)鍵字)的含義顯然更加困難而且容易出錯。繼承的成員變量, 包括類的方法可能會覆蓋外部類的聲明, 并且未經(jīng)限定的this關(guān)鍵字的引用會指向內(nèi)部類本身庸疾。
Lambda表達式則相對簡單, 它不從父類 (supertype) 繼承任何變量名, 也不會引入新的作用域乍楚。它基于詞法作用域,表達式中的變量名在封閉的上下文環(huán)境下解釋執(zhí)行(也包括了Lambda表達式中的形式參數(shù))。作為自然的拓展, this關(guān)鍵字和對于它的成員的引用具有相同的含義届慈。 (As a natural extension, the this keyword and references to its members have the same meaning as they would immediately outside the Lambda expression.)
舉例說明,以下程序打印了兩次 "Hello, world!"
public class Hello {
Runnable r1 = () -> { System.out.println(this); }
Runnable r2 = () -> { System.out.println(toString()); }
public String toString() { return "Hello, world!"; }
public static void main(String... args) {
new Hello().r1.run();
new Hello().r2.run();
}
}
如果使用匿名內(nèi)部類來做同樣的事情,令人驚訝的是,打印出來的是外部類的引用,比如Hello$1@5b89a773 和Hello$2@537a7706徒溪。
和詞法作用域的方式保持一致,遵循 其他參數(shù)化構(gòu)造器(比如for循環(huán)和catch塊類似)的模式,Lambda表達式的參數(shù)不可以覆蓋上下文中的任何局部變量。
7.變量捕獲
在JAVA7中,編譯器對于內(nèi)部類上下文中局部變量引用的檢查能力非常有限(變量捕獲)金顿。如果變量被聲明為非 final 的話, 就會發(fā)生編譯錯誤臊泌。在JAVA8中,內(nèi)部類和Lambda都沒有了這種限制,當然final的局部變量也可以被有效的捕獲。
局部變量如果不會改變,它實際上就是一個有效的final變量,換句話來說,聲明一個final并不會導(dǎo)致編譯錯誤揍拆。
Callable<String> helloCallable(String name) {
String hello = "Hello";
return () -> (hello + ", " + name);
}
this 關(guān)鍵字的引用, 包括(通過未限定字段的引用和方法調(diào)用)隱性引用本質(zhì)上都是對一個final本地變量(local variable)的引用渠概。包含此類引用的Lambda函數(shù)體實際上捕獲了this的實例。其他情況下, object并不保留對于this的引用嫂拴。
注: 未限定原文是unqualified, 有無限定主要是指是否對類所在的包名進行明確說明
qualified 的例子如java.util.Date,
unqualified的例子是 Date(這里Date就有可能是java.sql.Date)
這種機制有利于內(nèi)存管理:因為內(nèi)部類的實例總是持有外部類實例的強引用,如果Lambda沒有捕獲到外部類的成員變量,它就不會持有對外部類的引用播揪。內(nèi)部類的這個特性往往會導(dǎo)致內(nèi)存泄漏。
盡管我們放寬了對捕獲變量的語法限制筒狠,我們?nèi)匀徊辉试S捕獲可變的的局部變量剪芍。下面就是一個錯誤的示范。
int sum = 0;
list.forEach(e -> { sum += e.size(); }); // 非法,編譯錯誤
List<Integer> aList = new List<>();
list.forEach(e -> { aList.add(e); }); // 合法,編譯通過
除非這個是完全串行的窟蓝。我們很難保證這樣的Lambda表達式不存在競爭條件罪裹。除非我們強制將其扼殺在編譯階段,讓這樣的方法不能逃出它所在的線程中,否則會帶來更多問題。Lambda表達式對值而不是變量封閉运挫。
另一個不支持捕獲可變變量的原因是有一種更好的方法去解決累加問題,我們把它叫做歸約(reduction)状共。java.util.stream package,這個包提供了對于集合(collections)和其他數(shù)據(jù)結(jié)構(gòu)的通用和特定的歸約方法(sum,min,max…)。比如除了使用forEach和可變變量,我們可以用以下方法來做歸約,并且在串行和并行下均能保證線程安全谁帕。
int sum = list.stream()
.mapToInt(e -> e.size())
.sum();
sum()方法只是為了計算方便而提供的一個方法,等價于下面這個更通用的歸約方式峡继。
int sum = list.stream()
.mapToInt(e -> e.size())
.reduce(0, (x,y) -> x+y);
歸約獲取到一個初始值(以防輸入為空)和一個操作數(shù)(這里是加法),并且以以下方式進行計算。
0 + list[0] + list[1] + list[2] + ...
歸約也可以使用其他的操作符進行計算,比如最小值(minimum),最大值(maximum)和乘積(product)等等匈挖。而且如果操作滿足結(jié)合律 (associative) ,我們很容易并且很安全的將其并行化計算碾牌。所以我們最終選擇提供一個更利于并行化和更不易出錯的類庫來實現(xiàn)累積運算(accumulation)而不是提供某種本質(zhì)上是串行而且在多處理器情況下容易引起競爭的語法康愤。
8.方法引用 (Method references)
Lambda表達式允許我們定義匿名方法并且將其作為函數(shù)式接口的一個實例。對于現(xiàn)有的方法(method)我們也想采用同樣的方式舶吗。
方法引用是一種類似Lambda的表達式(比如也需要目標類型和對函數(shù)式接口的實現(xiàn)),不同之處在于它
不需要提供函數(shù)體,而是通過函數(shù)命名來指向已有的方法征冷。
比如我們有一個Person類,并可以按照姓名或者年齡排序,
class Person {
private final String name;
private final int age;
public int getAge() { return age; }
public String getName() { return name; }
...
}
Person[] people = ...
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
Arrays.sort(people, byName);
我們也可以直接使用方法引用來指向Person.getName()。
Comparator<Person> byName = Comparator.comparing(Person::getName);
Person::getName表達式可以被認為是一個簡化的Lambda表達式, 只是簡單的方法調(diào)用并且返回所需要的值誓琼。盡管方法引用在語法上看起來沒有變的更加簡潔(此例中),但是它更清晰,我們可以直接根據(jù)方法的名字進行調(diào)用检激。
因為函數(shù)式接口方法的參數(shù)充當了隱式調(diào)用的參數(shù),引用方法簽名(signature)可以通過放寬條件,裝箱,以及作為可變長度的數(shù)組分組來操作參數(shù),就像實際的方法調(diào)用。(方法簽名指的是方法名和一系列參數(shù)列表和其順序)
Consumer<Integer> b1 = System::exit; // void exit(int status)
Consumer<String[]> b2 = Arrays::sort; // void sort(Object[] a)
Consumer<String> b3 = MyProgram::main; // void main(String... args)
Runnable r = MyProgram::main; // void main(String... args)
9.不同種類的方法引用
方法的引用有很多種,它們之間的語法有著細微的差別腹侣。
- 靜態(tài)方法(ClassName::methName))
- 某個類的實例的方法(instanceReference::methName)
- 某個類的父類方法(super::methName)
- 一個特定類型的任意對象的實例方法(ClassName::methName)
- 類的構(gòu)造器引用(ClassName::new)
- 數(shù)組構(gòu)造器引用(TypeName[]::new)
對于靜態(tài)方法引用,該方法所在的類在::分隔符前,比如Integer::sum
對于某個類的實例的引用,表達式引用的對象位于::分隔符之前叔收。
Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;
這個隱式Lambda表達式會捕獲knownNames引用的String類,表達式體會去調(diào)用Set.contains方法使用該對象。
有了引用特定類的方法這個特性,我們可以非常方便的對不同的函數(shù)式接口類型進行轉(zhuǎn)換傲隶。
Callable<Path> c = ...
PrivilegedAction<Path> a = c::call;
對于任意對象的方法引用,方法所屬的類型在分隔符之前,并且調(diào)用的接收者是函數(shù)式接口方法的第一個參數(shù):
Function<String, String> upperfier = String::toUpperCase;
這個隱式的Lambda表達式有一個參數(shù), 等待被轉(zhuǎn)換為大寫的String,也是toUpperCase方法的接收者(入?yún)?饺律。
如果實例方法是屬于一個泛型類,它的參數(shù)類型可以寫在::分隔符之前,或者直接由編譯器推斷出來(通常情況)。
需要注意的是,從語法上靜態(tài)方法引用也可能被解釋為類的實例方法的引用跺株。編譯器通過嘗試識別每種類型的適用方法來確定到底是哪一種(實例方法比靜態(tài)方法要少一個參數(shù)) 蓝晒。
注:對于實例方法引用,這個少的參數(shù)是一個隱含的this, 例子如下
public class StaticVsInstance {
public int value = 1;
public int add(int i){
return this.value + i;
}
public static int add(int value, int i){
return value + i;
}
對于所有形式的方法引用,方法類型參數(shù)都可以按需推斷,或者可以在::分隔符之后顯式提供。
構(gòu)造器也可以通過"new"關(guān)鍵字,以類似靜態(tài)方法的方式被引用帖鸦。
SocketImplFactory factory = MySocketImpl::new;
如果一個類有多個構(gòu)造器, 那么我們就會根據(jù)目標類型的方法簽名來選擇最合適的一個芝薇。
對于內(nèi)部類, 語法上并不支持顯式提供封閉實例的參數(shù)給構(gòu)造器的引用。(For inner classes, no syntax supports explicitly providing an enclosing instance parameter at the site of the constructor reference.)
如果要實例化一個泛型,參數(shù)類型可以寫在類名之后,也可以直接使用<>讓編譯器來做推斷工作作儿。
數(shù)組類型的構(gòu)造器引用的語法比較特殊,我們認為它有一個允許傳入int參數(shù)的構(gòu)造器,例子如下:
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10); // creates an int[10]
10.默認和靜態(tài)方法
Lambda表達式和方法引用極大的豐富了Java語言, 但是真正要實現(xiàn)我們"代碼即數(shù)據(jù)"這個目標的關(guān)鍵是通過合適的類庫來利用這些新特性洛二。
在Java7中,給已有的類庫增加新功能十分困難。特別是接口在發(fā)布(publish)以后就會定型, 除非有人可以把接口的所有實現(xiàn)同時更改, 否則向接口中新加一個方法會導(dǎo)致已有的實現(xiàn)出問題攻锰。增加默認方法(default methods)的目的是為了使接口在發(fā)布以后還可以做修改晾嘶。
舉例說明,標準的集合類(collection)API應(yīng)該提供對Lambda操作的支持。比如removeAll 方法可以被概括為刪除collection中的所有元素,無論元素類型是什么 ,這個類型應(yīng)該是函數(shù)式接口Predicate的一個實例娶吞。但是這個新的方法(default boolean removeIf(Predicate<? super E> filter))要在哪里定義?我們不能直接在Collection接口中直接新增一個抽象方法, 因為已有的實現(xiàn)類并不知道這些垒迂。我們可以在Collections 工具類中增加一個靜態(tài)方法,但是這樣就會把這個新方法降為"次級地位"。
注: 在Java8中不推薦類似Maps,Collections,Lists這種工具類做法,鬼知道里面做了什么,而且有些人可能根本不知道這些輔助類妒蛇。
默認方法提供了一個更"面向?qū)ο?的方法來給接口增加一個具體的實現(xiàn)机断。我們給接口增加了新的類型方法, 可以是抽象或者默認的。默認方法有具體的實現(xiàn),并且接口的實現(xiàn)類不需要重寫(override)這個方法绣夺。默認方法并不算在函數(shù)式接口的抽象方法的數(shù)目限制中吏奸。
比如我可以增加一個skip方法到Iterator中,如下所示:
interface Iterator<E> {
boolean hasNext();
E next();
void remove();
default void skip(int i) {
for (; i > 0 && hasNext(); i--) next();
}
}
對于以上Iterator的定義, 所有實現(xiàn)了Iterator的類會自動繼承這個skip方法。從使用者的角度來看, skip只是接口提供的另外一個虛方法 (virtual method) 陶耍。調(diào)用這個子類實例的skip方法并不需要類本身有skip的實現(xiàn),而會直接調(diào)用默認方法奋蔚。當然如果子類有更為優(yōu)雅或優(yōu)化的實現(xiàn), 也可以對這個方法進行重寫。
當子接口繼承父接口的時候,可以用default方法重寫父接口中的default以及抽象方法 ,也可以將原有的default方法重新抽象化。
除了默認方法以外,Java8還允許在接口中使用靜態(tài)(static)方法,這也使我們可以把工具類方法放置在接口中而不是放在它的輔助類中(這種類通常命名為接口的復(fù)數(shù)形式, 比如Collections, Arrays)泊碑。舉例來說, Comparator接口可以定義一個靜態(tài)工具方法去生成比較器坤按。
Comparator<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
11. 默認方法的繼承
默認方法的繼承和其他方法幾乎沒有什么區(qū)別。但是當一個類或者接口的父類提供了多個有相同方法簽名的方法的時候馒过,繼承的規(guī)則(inheritance rules)會嘗試解決沖突臭脓。我們有以下兩個準則:
- 類的方法聲明優(yōu)先于接口默認方法, 無論類的方法是具體還是抽象 (因此接口的默認方法是在類的整個層級沒有提供任何有效信息的情況下的備選方案)
- 在多個父接口共有一個”祖父”接口的情況下,被其他接口覆蓋的方法會被忽略,
第二條規(guī)則的解釋我們有這個例子,比如Collection和List接口提供了不同的removeAll的默認方法,然后Queue繼承了Collection的默認方法;在下面的實現(xiàn)中,List的聲明的優(yōu)先級高于Queue中的聲明。
class LinkedList<E> implements List<E>, Queue<E> { ... }
當兩個獨立的默認方法沖突的時候,或者一個默認方法和一個抽象方法沖突的時候,會出現(xiàn)編譯錯誤沉桌。這時我們必須顯式的覆蓋父類中的方法。通常這也意味著需要選擇一個優(yōu)先的默認值并且在定義函數(shù)體的時候調(diào)用它算吩。我們對super關(guān)鍵字在語法上也有所加強, 可以使用特定父類的實現(xiàn)留凭。
interface Robot extends Artist, Gun {
default void draw() { Artist.super.draw(); }
}
super 前的名字必須指向一個定義了這個default方法的父類 (接口)。這種形式的方法調(diào)用不僅僅局限于消除歧義 ,也可以在類和接口中直接使用偎巢。
在任何情況下,我們在實現(xiàn)多個接口的時候,接口的順序并不會有任何影響蔼夜。
12.組合應(yīng)用
Lambda語言和庫特性協(xié)同工作, 我們以下面的例子說明: 根據(jù)姓將人的list排序
之前我們這么寫,看起來十分臃腫。
List<Person> people = ...
Collections.sort(people, new Comparator<Person>() {
public int compare(Person x, Person y) {
return x.getLastName().compareTo(y.getLastName());
}
});
有了Lambda,我們可以更加精確压昼。
Collections.sort(people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
但是這種方法并不抽象,我們?nèi)匀恍枰稣嬲膶Ρ?當對比的key是基本類型的時候會更糟糕)求冷。類庫中小小的改動可以幫助改進, 比如在Comparator中增加一個靜態(tài)的comparing 方法。
Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));
我們還可以靜態(tài)引入Compatator.comparing,以及讓編譯器進行類型推斷來使其更精減窍霞。
Collections.sort(people, comparing(p -> p.getLastName()));
這個Lambda表達式只是一個對于已有方法getLastName的轉(zhuǎn)發(fā)器, 我們也可以使用方法引用來代替Lambda匠题。
Collections.sort(people, comparing(Person::getLastName));
我們并不想使用像Collections.sort這種的輔助方法,它十分的繁瑣,而且也沒有辦法給實現(xiàn)list的類做定制化處理,而且用戶在使用List接口和閱讀對應(yīng)的java文檔的時候很難找到這個方法。
默認方法提供了一個更加"面向?qū)ο?的解決方案但金,我們在List中增加了一個默認sort方法韭山。
people.sort(comparing(Person::getLastName));
這樣看起來和我們的題設(shè)更加貼切: 根據(jù)姓將人的列表排序。
如果我們在Comparator中加一個默認的reverse方法,我們可以以降序的方式排序冷溃。
people.sort(comparing(Person::getLastName).reversed());
13.總結(jié)
Java8增加了一些新的特性, 比如Lambda表達式, 方法引用, 接口中的默認和靜態(tài)方法, 還有更為廣泛的類型推斷钱磅。這些特性可以更簡潔和精確的表達我們的意圖,并且使我們可以更容易的開發(fā)功能更為強大和易于并行的類庫。