「lambda表達式」函數式接口、方法引用與構造器引用
函數式接口
Java 中已經有很多封裝代碼塊的接口,如 ActionListener 或 Comparator。 lambda 表達式與這些接口是兼容的。對于只有一個抽象方法的接口, 需要這種接口的對象時, 就可以提供一個 lambda 表達式。這種接口稱為函數式接口( functional interface ) 。
為什么函數式接口必須有一個抽象方法。不是接口中的所有方法都是抽象的嗎? 實際上,接口完全有可能重新聲明 Object 類的方法, 如 toString 或 clone, 這些聲明有可能會讓方法不再是抽象的。( Java API 中的一些接口會重新聲明 Object 方法 來附加 javadoc 注釋。Comparator API 就是這樣一個例子。)更重要的是, 正如 6.1.5 節所述, 在 JavaSE 8 中, 接口可以聲明非抽象方法。
最好把 lambda 表達式看作是一 個函數,而不是一個對象, 另外要接受 lambda 表達式可以傳遞到函數式接口。 lambda 表達式可以轉換為接口, 這一點讓 lambda 表達式很有吸引力。具體的語法很簡短。
實際上,在 Java 中, 對 lambda 表達式所能做的也只是能轉換為函數式接口。在其他支持函數字面量的程序設計語言中,可以聲明函數類型(如(String, String) -> int) 、 聲明這些類型的變量,還可以使用變量保存函數表達式。不過,Java 設計者還是決定保持我們熟悉的接口概念, 沒有為 Java 語言增加函數類型。
甚至不能把 lambda 表達式賦值給類型為 Object 的變量,Object 不是一個函數式接口。
Java API 在 java.util.function 包中定義了很多非常通用的函數式接口。其中一個接口 BiFunction<T,U,R> 描述了參數類型為 T 和 U 而且返回類型為 R 的函數。可以把我們的字符串比較 lambda 表達式保存在這個類型的變量中:
BiFunction<String,String,Integer> comp = (first,second) -> first.length() - second.length();
類似 Comparator 的接口往往有一個特定的用途, 而不只是提供一個有指定參數和返回類型的方法。Java SE 8 沿襲了這種思路。想要用 lambda 表達式做某些處理,還是要謹記表達式的用途,為它建立一個特定的函數式接口。
java.util.function 包中有一個尤其有用的接口 Predicate:
ArrayList 類有一個 removelf 方法, 它的參數就是一個 Predicate 。這個接口專門用來傳遞 lambda 表達式。例如,下面的語句將從一個數組列表刪除所有 null 值: list.removelf(e -> e == null);
方法引用
有時, 可能已經有現成的方法可以完成你想要傳遞到其他代碼的某個動作。例如,假設你希望只要出現一個定時器事件就打印這個事件對象。 當然,為此也可以調用:
Timer t = new Timer(1000, event -> System.out .println(event)):
但是,如果直接把 println 方法傳遞到 Timer 構造器就更好了。具體做法如下:
Timer t = new Timer(1000, System.out::println);
表達式 System.out::println 是一個方法引用( method reference ), 它等價于 lambda 表達式
x -> System.out.println(x) 。
再來看一個例子,假設你想對字符串排序,而不考慮字母的大小寫。可以傳遞以下方法表達式:
Arrays.sort(strings , String::compareToIgnoreCase)
從這些例子可以看出, 要用 :: 操作符分隔方法名與對象或類名。主要有 3 種情況:
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
在前 2 種情況中,方法引用等價于提供方法參數的 lambda 表達式。前面已經提到的,System.out::println等價于 x -> System.out.println(x) 。類似地,Math::pow 等價于(x , y) -> Math.pow(x , y)。
對于第 3 種情況, 第 1 個參數會成為方法的目標。例如,
String::compareToIgnoreCase 等同于 (x, y) -> x.compareToIgnoreCase(y) 。
如果有多個同名的重栽方法, 編譯器就會嘗試從上下文中找出你指的那一個方法。 例如, Math.max 方法有兩個版本, 一個用于整數, 另一個用于 double 值。選擇哪一個版本取決于 Math::max 轉換為哪個函數式接口的方法參數。 類似于 lambda 表達式,方法引用不能獨立存在,總是會轉換為函數式接口的實例。
可以在方法引用中使用 this 參數。例如,this::equals 等同于 x -> this.equals(x) 。 使用 super 也是合法的。 下面的方法表達式:
super::instanceMethod
使用 this 作為目標,會調用給定方法的超類版本。
TimedGreeter.greet 方法開始執行時,會構造一個 Timer, 它會在每次定時器滴答時執行 super::greet 方法。這個方法會調用超類的 greet 方法。
構造器引用
構造器引用與方法引用很類似,只不過方法名為 new 。例如,Person::new 是 Person 構造器的一個引用。哪一個構造器呢? 這取決于上下文。假設你有一個字符串列表。可以把它轉換為一個 Person 對象數組,為此要在各個字符串上調用構造器,調用如下:
map 方法會為各個列表元素調用 Person(String) 構造器。如果有多個 Person 構造器, 編譯器會選擇有一個 String 參數的構造器, 因為它從上下文推導出這是在對一個字符串調用構造器。
可以用數組類型建立構造器引用。例如, int[]::new 是一個構造器引用,它有一個參數: 即數組的長度。這等價于 lambda 表達式 x -> new int[x] ;
Java 有一個限制,無法構造泛型類型 T 的數組。數組構造器引用對于克服這個限制很有用。表達式 new T[n] 會產生錯誤,因為這會改為 new Object[n] 。 對于開發類庫的人來說,這是一個問題。例如,假設我們需要一個 Person 對象數組。Stream 接口有一個 toArray 方法可以返回 Object 數組:
Object[] people = stream.toArray();
不過,這并不讓人滿意。用戶希望得到一個 Person 引用數組,而不是 Object 引用數組。 流庫利用構造器引用解決了這個問題。可以把 Person[]::new 傳入 toArray 方法:
Person[] people = stream.toArray(Person[]::new);
toArray 方法調用這個構造器來得到一個正確類型的數組。然后填充這個數組并返回。