構造器引用
構造器引用與方法引用很類似,只不過方法名爲new。例如,Person:new是Person構造器的一個引用。哪一個構造器呢?這取決於上下文。假設你有一個字符串列表。可以把它轉換爲一個Person對象數組,爲此要在各個字符串上調用構造器。如:
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person:new);
List<Person> people = stream.collect(Collectors.toList());
這裏,我們暫時先理解到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方法調用這個構造器來得到一個正確類型的數組。然後填充這個數組並返回。
變量作用域
通常,你可以希望能夠在lambda表達式中訪問外圍方法或類中的變量。考慮下面這個例子:
public static void repeatMessage(String text, int delay) {
ActionListener listener = event -> {
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay, listener).start();
}
來看這樣一個調用:
repeatMessage("Hello", 1000);
現在來看lambda表達式中的變量text。注意這個變量並不是在這個lambda表達式中定義的。實際上,這是repeatMessage方法的一個參數變量。
如果再想想看,這裏好像會有問題,儘管不那麼明顯。lambda表達式的代碼可能會在repeatMessage調用返回很久以後才運行,而那時這個參數變量已經不存在了。如何保留text變量呢?
要了解到底會發生什麼,下面來鞏固我們對lambda的理解。lambda表達式有3個部分:
- 一個代碼塊
- 參數
- 自由變量的值,這是指非參數而且不在代碼中定義的變量
在我們的例子中,則個lambda表達式有1個自由變量text。表示lambda表達式的數據結構必須存儲自由變量的值。在這裏就是字符串“Hello”。我們說它被lambda表達式捕獲(captured)。
可以看到,lambda表達式可以捕獲外圍作用域中變量的值。在Java中,要確保捕獲的值是明確定義的,這裏有一個重要的限制。在lambda表達式中,只能引用值不會改變的變量,如:下面的做法是不合法的:
public static void countDown(int start, int delay) {
ActionListener listener = event -> {
start--; // 這裏不合法
System.out.println(start);
}
new Timer(delay, listener).start();
}
之所以有這個限制是有原因的。如果在lambda表達式中改變變量,併發執行多個動作時就會不安全。對於目前爲止我們看到的動作不會發生這種情況,不過一般來講,這確實是一個嚴重的問題。
另外,如果在lambda表達式中引用變量,而這個變量可能在外部改變,這也是不合法的,如:
public static void repeat(String text, int count) {
for (int i = 1; i <= count; i++) {
ActionListener listener = event -> {
System.out.println(i + ": " + text); // 這裏是不合法的
}
new Timer(1000, listener).start();
}
}
這裏有一條規則:lambda表達式中捕獲的變量必須實際上是最終變量(effectively final)。實際上的最終變量是指,這個變量初始化之後就不會再爲它賦新值。在這裏,text總數指示同一個String對象,所以捕獲這個變量是合法的。不過,i的值會改變,因此不能捕獲i。
lambda表達式的體與嵌套快有相同的作用域。這裏同樣適用命名衝突和遮蔽的有關規則。在lambda表達式中聲明與一個局部變量同名的參數或局部變量是不合法的。
Path first = Paths.get("/usr/bin");
Comparator<String> comp = (first, second) -> first.length() - second.length();
在方法中,不能有兩個同名的局部變量,因此,lambda表達式中同樣也不能又同名的局部變量。
在一個lambda表達式中使用this關鍵字時,是指創建這個lambda表達式的方法的this參數。例如:
public class application() {
public void init() {
ActionListener listener = event -> {
System.out.println(this.toString());
...
}
}
}
表達式this.toString()會調用Application對象的toString方法,而不是ActionListener實例的方法。在lambda表達式中,this的使用並沒有任何特殊之處。lambda表達式的作用域嵌套在init方法中,與出現在這個方法中的其他位置一樣,lambda表達式中this的含義並沒有變化。
捐贈
若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。