JavaRush /Java Blog /Random-TW /Java 中流行的 lambda 表達式。帶有範例和任務。第2部分
Стас Пасинков
等級 26
Киев

Java 中流行的 lambda 表達式。帶有範例和任務。第2部分

在 Random-TW 群組發布
這篇文章適合誰?
  • 對於那些閱讀本文第一部分的人;

  • 對於那些認為自己已經很了解 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

使用方法引用的語法

這很簡單:
  1. 傳遞對靜態方法的引用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
        }
    }
  2. 使用現有物件傳遞對非靜態方法的引用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
        }
    }
  3. 我們使用實作此類方法的類別傳遞對非靜態方法的引用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);
            }
        }
    }
  4. 將連結傳遞給NameКласса::new
    建構函式 當您有一個完全滿意的現成方法並且希望將其用作回調時,使用方法連結非常方便。在這種情況下,我們不需要使用該方法的程式碼來編寫 lambda 表達式,或直接呼叫該方法的 lambda 表達式,而只需傳遞對其的參考。就這樣。

匿名類別和 lambda 表達式之間有趣的區別

在匿名類別中,關鍵字this指向該匿名類別的物件。如果我們this在 lambda 中使用它,我們將可以存取框架類別的物件。我們實際寫這個表達式的地方。發生這種情況是因為 lambda 表達式在編譯時會成為編寫它們的類別的私有方法。我不建議使用這個“功能”,因為它有副作用,與函數式程式設計的原則相矛盾。但這種做法非常符合OOP。;)

我從哪裡獲得資訊或還有什麼可閱讀的

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION