CodeGym /Java Blog /Toto sisi /Java 中 lambda 表達式的解釋。有例子和任務。第2部分
John Squirrels
等級 41
San Francisco

Java 中 lambda 表達式的解釋。有例子和任務。第2部分

在 Toto sisi 群組發布
這篇文章是為誰準備的?
  • 適用於閱讀本文第一部分的人;
  • 它適用於那些認為自己已經很了解 Java Core 但對 Java 中的 lambda 表達式一無所知的人。或者他們可能聽說過有關 lambda 表達式的一些信息,但缺少詳細信息。
  • 適合對lambda表達式有一定了解,但仍然望而卻步,不習慣使用的人。
如果您不屬於這些類別之一,您可能會覺得這篇文章乏味、有缺陷,或者通常不是您的菜。在這種情況下,請隨意繼續做其他事情,或者,如果您精通該主題,請在評論中就我如何改進或補充這篇文章提出建議。 Java 中 lambda 表達式的解釋。 有例子和任務。 第 2 - 1 部分該材料並沒有聲稱具有任何學術價值,更不用說新穎性了。恰恰相反:我將嘗試盡可能簡單地描述(對某些人而言)複雜的事物。解釋 Stream API 的請求啟發了我寫這篇文章。我考慮了一下,決定如果不了解 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並將嘗試傳遞voidforEach(),而後者期待的是一個Consumer對象。

方法引用的語法

這很簡單:
  1. 我們像這樣傳遞對靜態方法的引用: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 
        } 
    }
    
  2. 我們使用現有對像傳遞對非靜態方法的引用,如下所示: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 
        } 
    }
    
  3. 我們使用實現它的類傳遞對非靜態方法的引用,如下所示: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); 
            } 
        } 
    }
    
  4. 我們像這樣傳遞對構造函數的引用:ClassName::new

    當您已經擁有可以完美用作回調的方法時,方法引用非常方便。在這種情況下,不是編寫包含方法代碼的 lambda 表達式,也不是編寫僅調用該方法的 lambda 表達式,我們只是傳遞對它的引用。就是這樣。

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

在匿名類中,this關鍵字指向匿名類的一個對象。但是如果我們在 lambda 中使用它,我們就可以訪問包含類的對象。我們實際編寫 lambda 表達式的地方。發生這種情況是因為 lambda 表達式被編譯到它們所在類的私有方法中。我不建議使用此“功能”,因為它有副作用並且與函數式編程的原則相矛盾。也就是說,這種方法完全符合 OOP。;)

我從哪裡得到我的信息,你還應該閱讀什麼?

當然,我在 Google 上找到了很多東西 :)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION