這篇文章適合誰?
- 對於那些閱讀本文第一部分的人;
- 對於那些認為自己已經很了解 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("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
}
}
儘管該方法staticMethod()
是私有的,但它是“可訪問的”,可以在方法內部調用main()
,因此也可以從方法中創建的 lambda 內部進行調用main
。
lambda表達式程式碼執行的時刻
這個問題對你來說似乎太簡單了,但值得一問:lambda 表達式內的程式碼何時被執行?在創作的那一刻?或在什麼時候(仍然未知在哪裡)它會被呼叫?這很容易檢查。System.out.println("Запуск программы");
// много всякого разного codeа
// ...
System.out.println("Перед объявлением лямбды");
Runnable runnable = () -> System.out.println("Я - лямбда!");
System.out.println("После объявления лямбды");
// много всякого другого codeа
// ...
System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
顯示輸出:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
可以看到,lambda表達式程式碼是在最末尾、執行緒建立之後、程式執行過程到達方法實際執行時才執行的run()
。而且在其宣佈時根本沒有。透過聲明 lambda 表達式,我們僅建立了該類型的物件Runnable
並描述了其方法的行為run()
。該方法本身的推出要晚得多。
方法參考?
與 lambda 本身沒有直接關係,但我認為在本文中對此說幾句話是合乎邏輯的。假設我們有一個 lambda 表達式,它不做任何特殊的事情,只是呼叫一些方法。x -> System.out.println(x)
他們遞給他一些東西х
,它只是召喚他System.out.println()
並把他送到那裡х
。在這種情況下,我們可以將其替換為我們需要的方法的連結。像這樣:
System.out::println
是的,最後沒有括號!更完整的例子:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");
strings.forEach(x -> System.out.println(x));
在最後一行,我們使用forEach()
一個接受介面物件的方法Consumer
。這又是一個只有一種方法的函數式介面void accept(T t)
。因此,我們寫一個帶有一個參數的 lambda 表達式(因為它是在介面本身中鍵入的,所以我們不指示參數的類型,而是指示它將被呼叫х)
。在lambda 表達式的主體中,我們寫程式碼當方法被呼叫時將會執行accept()
。這裡我們只是在螢幕上顯示變數中的內容х
。方法本身forEach()
會遍歷集合的所有元素,呼叫Consumer
傳遞給它的介面物件的方法(我們的 lambda)accept()
,它傳遞集合中的每個元素。正如我已經說過的,這是一個lambda 表達式(簡單地調用另一個方法),我們可以用對我們需要的方法的引用替換。然後我們的程式碼將如下所示:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");
strings.forEach(System.out::println);
最主要的是方法(println()
和接受的參數accept())
。由於該方法println()
可以接受任何內容(它對所有基元和任何物件都重載),因此我們可以forEach()
只傳入對該方法的引用,而不是 lambda 表達式println()
。然後forEach()
它將獲取集合的每個元素並將其直接傳遞給方法println()
。對於第一次遇到此問題的人,請注意請注意,我們不會呼叫該方法System.out.println()
(單字之間有點,末尾有括號),而是傳遞對此方法本身的引用。
strings.forEach(System.out.println());
我們會遇到編譯錯誤。因為在調用之前forEach()
,Java 會看到它正在被調用System.out.println()
,它會理解它正在被返回void
,並將嘗試void
將其傳遞給forEach()
在那裡等待的類型的物件Consumer
。
使用方法引用的語法
這很簡單:-
傳遞對靜態方法的引用
NameКласса:: NameСтатическогоМетода?
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Mother"); strings.add("soap"); strings.add("frame"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // do something } }
-
使用現有物件傳遞對非靜態方法的引用
NameПеременнойСОбъектом:: method name
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Mother"); strings.add("soap"); strings.add("frame"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // do something } }
-
我們使用實作此類方法的類別傳遞對非靜態方法的引用
NameКласса:: method name
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add(new User("Vasya")); users.add(new User("Коля")); users.add(new User("Петя")); 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); } } }
-
將連結傳遞給
NameКласса::new
建構函式 當您有一個完全滿意的現成方法並且希望將其用作回調時,使用方法連結非常方便。在這種情況下,我們不需要使用該方法的程式碼來編寫 lambda 表達式,或直接呼叫該方法的 lambda 表達式,而只需傳遞對其的參考。就這樣。
匿名類別和 lambda 表達式之間有趣的區別
在匿名類別中,關鍵字this
指向該匿名類別的物件。如果我們this
在 lambda 中使用它,我們將可以存取框架類別的物件。我們實際寫這個表達式的地方。發生這種情況是因為 lambda 表達式在編譯時會成為編寫它們的類別的私有方法。我不建議使用這個“功能”,因為它有副作用,與函數式程式設計的原則相矛盾。但這種做法非常符合OOP。;)
我從哪裡獲得資訊或還有什麼可閱讀的
- Oracle 官方網站上的教學。很多,很詳細,有例子,但是是英文的。
- 相同的“Oracle”教程,但本章是關於方法引用的。
- 關於 Habré 的一篇關於函數式程式設計的文章(另一篇文章的翻譯)。關於 lambda 的多本書很少,因為它們一般都是關於函數式程式設計的。
- 對於那些喜歡陷入維基百科的人。
- 當然,我在谷歌上找到了很多東西:)
GO TO FULL VERSION