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

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

在 Random-TW 群組發布
這篇文章適合誰?
  • 對於那些認為自己已經很了解 Java Core,但對 Java 中的 lambda 表達式一無所知的人。或者,也許您已經聽說過有關 lambda 的一些內容,但沒有了解詳細資訊。
  • 適合那些對 lambda 表達式有一定了解,但仍然害怕和不習慣使用它們的人。
如果您不屬於這些類別之一,您很可能會發現這篇文章無聊、不正確,而且通常「不酷」。在這種情況下,請隨意瀏覽,或者,如果您精通該主題,請在評論中建議我如何改進或補充本文。該材料不具有任何學術價值,更不用說新穎性了。相反,相反:在其中我將嘗試盡可能簡單地描述複雜的(對某些人來說)事物。我受到解釋流 api 的請求的啟發而寫下這篇文章。我想了想,認為如果不理解 lambda 表達式,我的一些關於「流」的範例將難以理解。那麼就讓我們從 lambda 開始。 Java 中流行的 lambda 表達式。 帶有範例和任務。 第 1 - 1 部分理解本文需要哪些知識:
  1. 對物件導向程式設計(以下簡稱OOP)的理解,即:
    • 了解什麼是類別和對象,它們之間有什麼區別;
    • 了解介面是什麼、它們與類別有何不同、它們之間的聯繫(介面和類別)是什麼;
    • 了解什麼是方法,如何呼叫它,什麼是抽象方法(或沒有實現的方法),方法的參數/參數是什麼,如何將它們傳遞到那裡;
    • 存取修飾符、靜態方法/變數、最終方法/變數;
    • 繼承(類別、介面、介面的多重繼承)。
  2. Java 核心知識:泛型、集合(列表)、執行緒。
好吧,讓我們開始吧。

一點歷史

Lambda 表達式源自函數式程式設計和數學。20世紀中葉的美國,有一位在普林斯頓大學工作的阿朗佐·丘奇(Alonzo Church),非常喜歡數學和各種抽象概念。lambda 演算是 Alonzo Church 提出的,起初它只是一些抽象概念的集合,與程式設計無關。同時,阿蘭·圖靈和約翰·馮·諾依曼等數學家也在同一所普林斯頓大學工作。一切都走到了一起:丘奇提出了 lambda 演算系統,圖靈開發了他的抽象計算機,現在被稱為「圖靈機」。馮·諾依曼提出了電腦體系結構圖,它構成了現代電腦的基礎(現在稱為「馮·諾依曼體系結構」)。當時,阿朗佐·丘奇的思想並沒有像他的同事們的工作那樣享有盛譽(“純粹”數學領域除外)。然而,不久之後,約翰·麥卡錫(也是普林斯頓大學的畢業生,在故事發生時是麻省理工學院的員工)對丘奇的想法產生了興趣。基於它們,他於 1958 年創建了第一種函數式程式語言 Lisp。58年後,函數式程式設計的想法以數字8的形式滲透到Java中。甚至還不到70年……事實上,這並不是數學思想在實踐中應用的最長時期。

精華

lambda 表達式就是這樣一個函數。您可以將其視為 Java 中的常規方法,唯一的區別是它可以作為參數傳遞給其他方法。是的,不僅可以將數字、字串和貓傳遞給方法,還可以傳遞其他方法!我們什麼時候可能需要它?例如,如果我們想傳遞一些回調。我們需要我們所呼叫的方法能夠呼叫我們傳遞給它的其他方法。也就是說,這樣我們就有機會在某些情況下傳輸一個回調,而在其他情況下傳輸另一個回調。我們的方法將接受回調並調用它們。一個簡單的例子就是排序。假設我們寫了某種棘手的排序,如下所示:

public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
在哪裡,if我們呼叫方法compare(),傳遞兩個我們比較的對象,我們想找出這些對像中哪一個「更大」。我們將把「多」放在「小」之前。我在引號中寫了“更多”,因為我們正在編寫一個通用方法,它不僅能夠按升序排序,還能夠按降序排序(在這種情況下,“更多”將是本質上更小的對象,反之亦然) 。為了準確地設定我們想要的排序規則,我們需要以某種方式將其傳遞給我們的mySuperSort(). 在這種情況下,我們將能夠在呼叫我們的方法時以某種方式「控制」它。當然,您可以編寫兩個單獨的方法mySuperSortAsc()mySuperSortDesc()按升序和降序排序。或在方法內部傳遞一些參數(例如,booleanif true,按升序排序, if ,按false降序排序)。但是,如果我們想要排序的不是一些簡單的結構,而是字串數組列表,該怎麼辦?我們的方法如何mySuperSort()知道如何對這些字串陣列進行排序?要尺寸嗎?按單字總長度?也許按字母順序排列,取決於數組中的第一行?但是,如果在某些情況下,我們需要按數組的大小對數組列表進行排序,而在另一種情況下,則需要按數組中單字的總長度對數組列表進行排序,該怎麼辦?我想您已經聽說過比較器,在這種情況下,我們只需將比較器物件傳遞給我們的排序方法,在該方法中我們描述我們想要排序的規則。由於標準方法sort()的實作原理與標準方法相同,因此mySuperSort()在範例中我將使用標準方法sort()

String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);
結果:
  1. 媽媽洗了相框
  2. 和平工黨可能
  3. 我真的很喜歡java
這裡,數組按每個數組中的單字數排序。單字數較少的數組被認為是“較小”。這就是為什麼它在一開始就出現了。單字數較多的被認為是“更多”,並最終出現在最後。如果sort()我們將另一個比較器傳遞給方法(sortByWordsLength),那麼結果將會不同:
  1. 和平工黨可能
  2. 媽媽洗了相框
  3. 我真的很喜歡java
現在,數組會按照該數組的單字中的字母總數排序。第一個例子有10 個字母,第二個例子有12 個字母,第三個例子有15 個字母。如果我們只使用一個比較器,那麼我們就不能為它創建一個單獨的變量,而只需在呼叫該方法的時間sort()。像那樣:

String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
結果將與第一種情況相同。 任務1。重寫此範例,以便它不按數組中單字數的升序對數組進行排序,而是按降序排序。這一切我們都已經知道了。我們知道如何將物件傳遞給方法,我們可以根據我們當前的需要將這個或那個物件傳遞給方法,並且在我們傳遞這樣一個物件的方法內部,我們為其編寫實作的方法將會被呼叫。問題來了:lambda 表達式與它有什麼關係? 假設 lambda 是僅包含一個方法的物件。它就像一個方法物件。包裝在物件中的方法。它們只是有一個稍微不尋常的語法(稍後會詳細介紹)。 讓我們再看一下這個條目

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
在這裡,我們獲取列表arrays並呼叫其方法sort(),其中我們透過一個方法傳遞一個比較器物件compare()(它的名稱對我們來說並不重要,因為它是該物件中唯一的一個,我們不會錯過它)。這個方法需要兩個參數,我們接下來將使用這兩個參數。如果您使用IntelliJ IDEA,您可能已經看到它如何為您提供此程式碼以顯著縮短:

arrays.sort((o1, o2) -> o1.length - o2.length);
就這樣,六行變成了短短的一行。6行重寫為一小段。有些東西消失了,但我保證沒有任何重要的東西消失,並且此程式碼的運作方式與匿名類別完全相同。 任務2。弄清楚如何使用 lambda 重寫問題 1 的解決方案(作為最後的手段,請要求IntelliJ IDEA將您的匿名類別轉換為 lambda)。

我們來談談介面

基本上,介面只是抽象方法的列表。當我們創建一個類別並說它將實現某種介面時,我們必須在類別中編寫介面中列出的方法的實現(或者,作為最後的手段,不編寫它,而是使類別抽象)。有的介面具有多種不同的方法(例如List),有的介面僅具有一種方法(例如,相同的 Comparator 或 Runnable)。有些介面根本沒有單一方法(所謂的標記接口,例如 Serialized)。那些只有一種方法的介面也稱為函數式介面。在 Java 8 中,它們甚至用特殊的@FunctionalInterface註解進行標記。它是具有適合 lambda 表達式使用的單一方法的介面。正如我上面所說,lambda 表達式是包裝在物件中的方法。當我們在某處傳遞這樣一個物件時,實際上,我們傳遞的是一個方法。事實證明,這個方法叫什麼對我們來說並不重要。對我們來說重要的是該方法所採用的參數,實際上是方法程式碼本身。lambda 表達式本質上是。功能介面的實作。當我們看到一個只有一個方法的介面時,這意味著我們可以使用 lambda 重寫這樣的匿名類別。如果介面有多於/少於一個方法,那麼 lambda 表達式將不適合我們,我們將使用匿名類,甚至是常規類。是時候深入研究 lambda 了。:)

句法

一般語法是這樣的:

(параметры) -> {тело метода}
也就是說,括號內是方法參數,一個「箭頭」(這是連續的兩個字元:減號和大於號),之後方法的主體像往常一樣放在花括號中。參數與描述方法時在介面中指定的參數相對應。如果編譯器可以清楚地定義變數的類型(在我們的例子中,可以肯定我們正在使用字串數組,因為它是List由字串數組精確鍵入的),那麼變數的類型String[]不需要被寫下來。
如果不確定,請指定類型,如果不需要,IDEA 將以灰色突出顯示。
例如,您可以在Oracle 教學中閱讀更多內容。這稱為“目標打字”。您可以為變數指定任何名稱,不一定是介面中指定的名稱。如果沒有參數,則只需括號。如果只有一個參數,則只是變數名,不帶括號。我們已經整理了參數,現在介紹 lambda 表達式本身的主體。在大括號內,像常規方法一樣編寫程式碼。如果您的整個程式碼僅由一行組成,則根本不必編寫花括號(就像 if 和循環一樣)。如果您的 lambda 傳回某些內容,但其主體由一行組成,則return根本不需要編寫。但如果你有花括號,那麼,像通常的方法一樣,你需要顯式地寫return.

例子

範例 1。
() -> {}
最簡單的選擇。而且是最無意義的:).因為它什麼也沒做。 範例 2.
() -> ""
也是一個有趣的選擇。它不接受任何內容並傳回一個空字串(return因為不必要而被省略)。相同,但具有return

() -> {
    return "";
}
範例 3. 使用 lambda 的 Hello world

() -> System.out.println("Hello world!")
不接收任何內容,也不返回任何內容(我們不能放在returncall 之前System.out.println(),因為方法中的返回類型println() — void)只是在螢幕上顯示一個銘文。非常適合實現介面Runnable。相同的範例更完整:

public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
或者像這樣:

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
或者我們甚至可以將 lambda 表達式儲存為 type 的對象Runnable,然後將其傳遞給建構函數thread’а

public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
讓我們仔細看看將 lambda 表達式保存到變數中的那一刻。介面Runnable告訴我們它的物件必須有一個方法public void run()。根據接口,run 方法不接受任何參數作為參數。它不會返回任何東西(void)。因此,以這種方式編寫時,將使用某種不接受或傳回任何內容的方法來建立一個物件。run()與介面中的方法非常一致Runnable。這就是為什麼我們能夠將此 lambda 表達式放入諸如 之類的變數中Runnable實施例4

() -> 42
同樣,它不接受任何內容,但返回數字 42。這個 lambda 表達式可以放置在 類型的變數中Callable,因為這個介面只定義了一個方法,它看起來像這樣:

V call(),
其中V是傳回值的類型(在我們的例子中int)。因此,我們可以儲存這樣的 lambda 表達式,如下所示:

Callable<Integer> c = () -> 42;
範例 5. 多行中的 Lambda

() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);    
}
同樣,這是一個沒有參數及其返回類型的 lambda 表達式void(因為沒有return)。 實施例6

x -> x
在這裡,我們將一些內容放入變數中х並返回它。請注意,如果只接受一個參數,則不需要寫括號。相同,但有括號:

(x) -> x
這是一個明確的選項return

x -> {
    return x;
}
或者像這樣,用括號和return

(x) -> {
    return x;
}
或明確指示類型(並相應地使用括號):

(int x) -> x
實施例7
x -> ++x
我們接受х並退回,但要求1更多。你也可以這樣重寫:
x -> x + 1
在這兩種情況下,我們都不在參數、方法體和單字周圍指示括號return,因為這是不必要的。範例 6 中描述了帶括號和回車的選項。 範例 8
(x, y) -> x % y
我們接受一些х並返回除以у的餘數。這裡已經需要參數周圍的括號。只有當只有一個參數時,它們才是可選的。像這樣明確指示類型: xy
(double x, int y) -> x % y
實施例9

(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
我們接受一個 Cat 物件、一個帶有名稱和整數年齡的字串。在方法本身中,我們將傳遞的名稱和年齡設為 Cat。cat由於我們的變數是引用類型,因此 lambda 表達式外部的 Cat 物件將會發生變化(它將接收內部傳遞的姓名和年齡)。使用類似 lambda 的稍微複雜的版本:

public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Murzik", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
結果: Cat{name='null',age=0} Cat{name='Murzik',age=3} 可以看到,一開始 Cat 物件只有一個狀態,但使用 lambda 表達式後,狀態發生了變化。Lambda 表達式與泛型配合得很好。例如,如果我們需要建立一個類別Dog,它也將實現WithNameAndAge,那麼在方法中main()我們可以對 Dog 執行相同的操作,而無需更改 lambda 表達式本身。 任務3。使用接受數字並返回布林值的方法編寫函數介面。以 lambda 表達式的形式編寫此類介面的實現,true如果傳遞的數字可以被 13 整除而沒有餘數,則傳回該 表達式。任務 4。使用接受兩個字串並傳回相同字串的方法編寫一個函數介面。以傳回最長字串的 lambda 形式編寫此類介面的實作。 任務5。使用接受三個小數的方法寫一個函數介面:abc並傳回相同的小數。以傳回判別式的 lambda 表達式的形式編寫此類介面的實作。誰忘記了,D = b^2 - 4ac任務 6 . 使用任務 5 中的函數式接口,編寫一個返回運算結果的 lambda 表達式a * b^cJava 中流行的 lambda 表達式。帶有範例和任務。第2部分。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION