這篇文章是為誰準備的?
該材料並沒有聲稱具有任何學術價值,更不用說新穎性了。恰恰相反:我將嘗試盡可能簡單地描述(對某些人而言)複雜的事物。解釋 Stream API 的請求啟發了我寫這篇文章。我考慮了一下,決定如果不了解 lambda 表達式,我的一些流示例將難以理解。所以我們將從 lambda 表達式開始。
- 適用於閱讀本文第一部分的人;
- 它適用於那些認為自己已經很了解 Java Core 但對 Java 中的 lambda 表達式一無所知的人。或者他們可能聽說過有關 lambda 表達式的一些信息,但缺少詳細信息。
- 適合對lambda表達式有一定了解,但仍然望而卻步,不習慣使用的人。

訪問外部變量
此代碼是否使用匿名類編譯?
int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
不,counter
變量必須是final
. 或者如果不是final
,那麼至少它不能改變它的價值。同樣的原則也適用於 lambda 表達式。他們可以從聲明的地方訪問他們可以“看到”的所有變量。但是 lambda 不得更改它們(為它們分配新值)。然而,在匿名類中有一種方法可以繞過這個限制。只需創建一個引用變量並更改對象的內部狀態。這樣做時,變量本身不會改變(指向同一個對象)並且可以安全地標記為final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
這裡我們的counter
變量是對對象的引用AtomicInteger
。並且該incrementAndGet()
方法用於更改此對象的狀態。變量本身的值在程序運行時不會改變。它總是指向同一個對象,這讓我們可以用 final 關鍵字聲明變量。以下是相同的示例,但使用了 lambda 表達式:
int counter = 0;
Runnable r = () -> counter++;
由於與具有匿名類的版本相同的原因,這不會編譯: counter
在程序運行時不得更改。但如果我們這樣做,一切都很好:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
這也適用於調用方法。在 lambda 表達式中,您不僅可以訪問所有“可見”變量,還可以調用任何可訪問的方法。
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
雖然staticMethod()
是私有的,但它可以在main()
方法內部訪問,因此也可以從方法中創建的 lambda 內部調用它main
。
什麼時候執行 lambda 表達式?
你可能會覺得下面的問題太簡單了,但你還是應該問:lambda 表達式裡面的代碼什麼時候執行?什麼時候創建的?或者什麼時候被調用(目前還不知道)?這很容易檢查。
System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
屏幕輸出:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
您可以看到 lambda 表達式是在線程創建之後的最後執行的,並且僅在程序執行到方法時執行run()
。當然不是在宣布的時候。通過聲明 lambda 表達式,我們僅創建了一個Runnable
對象並描述了其run()
方法的行為方式。該方法本身的執行時間要晚得多。
方法參考?
方法引用與 lambda 沒有直接關係,但我認為在本文中對它們說幾句是有意義的。假設我們有一個 lambda 表達式,它不執行任何特殊操作,只是簡單地調用一個方法。
x -> System.out.println(x)
它接收一些x
只是調用System.out.println()
,傳入x
。在這種情況下,我們可以將其替換為對所需方法的引用。像這樣:
System.out::println
沒錯——末尾沒有括號!這是一個更完整的例子:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
在最後一行,我們使用forEach()
方法,它接受一個實現接口的對象Consumer
。同樣,這是一個函數式接口,只有一個void accept(T t)
方法。因此,我們編寫了一個只有一個參數的 lambda 表達式(因為它是在接口本身中鍵入的,所以我們不指定參數類型;我們只表示我們將調用它x
)。accept()
在 lambda 表達式的主體中,我們編寫了調用方法時將執行的代碼。這裡我們簡單地顯示變量中的結果x
。這個相同的forEach()
方法遍歷集合中的所有元素並調用accept()
方法的實現Consumer
接口(我們的 lambda),傳入集合中的每個項目。正如我所說,我們可以用對所需方法的引用替換這樣的 lambda 表達式(一個簡單地對不同方法進行分類的表達式)。那麼我們的代碼將如下所示:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
主要是println()
和accept()
方法的參數要匹配。因為該方法可以接受任何東西(它為所有基元類型和所有對象重載),而不是 lambda 表達式,我們可以簡單地將對該方法的println()
引用傳遞給. 然後將獲取集合中的每個元素並將其直接傳遞給方法。對於第一次遇到這種情況的任何人,請注意我們不是在調用(單詞之間有點,末尾有括號)。相反,我們正在傳遞對此方法的引用。如果我們這樣寫 println()
forEach()
forEach()
println()
System.out.println()
strings.forEach(System.out.println());
我們將有一個編譯錯誤。在調用 之前forEach()
,Java 看到它System.out.println()
正在被調用,因此它知道返回值是void
並將嘗試傳遞void
給forEach()
,而後者期待的是一個Consumer
對象。
方法引用的語法
這很簡單:-
我們像這樣傳遞對靜態方法的引用:
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
我們使用現有對像傳遞對非靜態方法的引用,如下所示:
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
我們使用實現它的類傳遞對非靜態方法的引用,如下所示:
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
我們像這樣傳遞對構造函數的引用:
ClassName::new
當您已經擁有可以完美用作回調的方法時,方法引用非常方便。在這種情況下,不是編寫包含方法代碼的 lambda 表達式,也不是編寫僅調用該方法的 lambda 表達式,我們只是傳遞對它的引用。就是這樣。
匿名類和 lambda 表達式之間有趣的區別
在匿名類中,this
關鍵字指向匿名類的一個對象。但是如果我們在 lambda 中使用它,我們就可以訪問包含類的對象。我們實際編寫 lambda 表達式的地方。發生這種情況是因為 lambda 表達式被編譯到它們所在類的私有方法中。我不建議使用此“功能”,因為它有副作用並且與函數式編程的原則相矛盾。也就是說,這種方法完全符合 OOP。;)
我從哪裡得到我的信息,你還應該閱讀什麼?
- Oracle官網教程。很多詳細信息,包括示例。
- 同一 Oracle 教程中有關方法參考的章節。
- 如果您真的很好奇,請進入維基百科。
GO TO FULL VERSION