【java基礎(四十一)】lambda表達式(三)

構造器引用

構造器引用與方法引用很類似,只不過方法名爲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的含義並沒有變化。

捐贈

若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章