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

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

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

    • 類、對像以及它們之間的區別;
    • 接口,它們與類的區別,以及接口和類之間的關係;
    • 方法,如何調用它們,抽象方法(即沒有實現的方法),方法參數,方法參數以及如何傳遞它們;
    • 訪問修飾符、靜態方法/變量、final 方法/變量;
    • 類和接口的繼承,接口的多重繼承。
  2. Java核心知識:泛型類型(generics)、集合(lists)、線程。
好吧,讓我們開始吧。

一點歷史

Lambda 表達式來自函數式編程,又來自數學。20世紀中葉的美國,酷愛數學和各種抽象概念的Alonzo Church在普林斯頓大學工作。發明 lambda 演算的是 Alonzo Church,它最初是一組與編程完全無關的抽象思想。艾倫圖靈和約翰馮諾依曼等數學家同時在普林斯頓大學工作。萬事俱備:Church 提出了 lambda 演算。圖靈開發了他的抽象計算機,現在被稱為“圖靈機”。馮諾依曼提出了一種計算機體系結構,它構成了現代計算機的基礎(現在稱為“馮諾依曼體系結構”)。當時,Alonzo Church' 他的想法並沒有像他的同事們的作品那樣廣為人知(純數學領域除外)。然而,不久之後約翰麥卡錫(也是普林斯頓大學的畢業生,在我們的故事發生時,他是麻省理工學院的一名僱員)開始對丘奇的想法產生興趣。1958 年,他基於這些想法創建了第一個函數式編程語言 LISP。58 年後,函數式編程的思想滲透到 Java 8 中。甚至還不到 70 年……老實說,這不是將數學思想應用到實踐中花費的最長時間。麻省理工學院的一名僱員)對丘奇的想法產生了興趣。1958 年,他基於這些想法創建了第一個函數式編程語言 LISP。58 年後,函數式編程的思想滲透到 Java 8 中。甚至還不到 70 年……老實說,這不是將數學思想應用到實踐中花費的最長時間。麻省理工學院的一名僱員)對丘奇的想法產生了興趣。1958 年,他基於這些想法創建了第一個函數式編程語言 LISP。58 年後,函數式編程的思想滲透到 Java 8 中。甚至還不到 70 年……老實說,這不是將數學思想應用到實踐中花費的最長時間。

事件的核心

lambda 表達式是一種函數。您可以將其視為普通的 Java 方法,但具有作為參數傳遞給其他方法的獨特能力。這是正確的。不僅可以將數字、字符串和貓傳遞給方法,還可以傳遞其他方法!我們什麼時候可能需要這個?這將很有幫助,例如,如果我們想傳遞一些回調方法。也就是說,如果我們需要我們調用的方法能夠調用我們傳遞給它的其他方法。換句話說,我們有能力在某些情況下傳遞一個回調,而在其他情況下傳遞不同的回調。這樣我們接收回調的方法就會調用它們。排序是一個簡單的例子。假設我們正在編寫一些聰明的排序算法,如下所示:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
if語句中,我們調用compare()方法,傳入兩個要比較的對象,我們想知道這些對像中哪個“更大”。我們假設“大”的先於“小”的。我將“更大”放在引號中,因為我們正在編寫一個通用方法,它不僅知道如何按升序排序,而且還知道如何按降序排序(在這種情況下,“大”對象實際上是“小”對象,反之亦然)。要為我們的排序設置特定的算法,我們需要一些機制將其傳遞給我們的mySuperSort()方法。這樣我們就可以在調用時“控制”我們的方法。當然,我們可以編寫兩個單獨的方法——mySuperSortAscend()並且mySuperSortDescend()— 用於按升序和降序排序。或者我們可以將一些參數傳遞給該方法(例如,一個布爾變量;如果為真,則按升序排序,如果為假,則按降序排序)。但是,如果我們想對一些複雜的東西(例如字符串數組列表)進行排序怎麼辦?我們的方法如何mySuperSort()知道如何對這些字符串數組進行排序?按尺寸?通過所有單詞的累積長度?也許基於數組中的第一個字符串按字母順序排列?如果我們需要在某些情況下按數組大小對數組列表進行排序,而在其他情況下按每個數組中所有單詞的累積長度怎麼辦?我希望您已經聽說過比較器,在這種情況下,我們只需將描述所需排序算法的比較器對像傳遞給我們的排序方法。因為標準sort()方法是基於與 相同的原理實現的,我將在示例中 mySuperSort()使用。sort()

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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[]> sortByCumulativeWordLength = 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. Dota GTA5 Halo
  2. if then else
  3. I really love Java
這裡的數組按每個數組中的單詞數排序。單詞較少的數組被認為是“較少”。這就是為什麼它是第一位的。包含更多單詞的數組被認為“更大”並放在​​最後。如果我們將不同的比較器傳遞給該sort()方法,例如sortByCumulativeWordLength,那麼我們將得到不同的結果:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
現在 are 數組按數組單詞中的字母總數排序。在第一個數組中,有 10 個字母,在第二個中有 12 個,在第三個中有 15 個。如果我們只有一個比較器,那麼我們不必為它聲明一個單獨的變量。相反,我們可以在調用方法時簡單地創建一個匿名類sort()。是這樣的:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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; 
    } 
});
這裡我們獲取我們的數​​組列表並調用它的sort()方法,我們用一個方法將一個比較器對像傳遞給compare()它(它的名字對我們來說並不重要——畢竟,它是這個對象的唯一方法,所以我們不會出錯)。此方法有兩個我們將使用的參數。如果您在 IntelliJ IDEA 中工作,您可能會看到它可以顯著壓縮代碼,如下所示:

arrays.sort((o1, o2) -> o1.length - o2.length);
這將六行減少為一個短行。6行改寫為一小段。有些東西消失了,但我保證這不是什麼重要的東西。此代碼的工作方式與匿名類的工作方式完全相同。 任務 2.猜猜使用 lambda 表達式重寫任務 1 的解決方案(至少,要求 IntelliJ IDEA 將您的匿名類轉換為 lambda 表達式)。

讓我們談談接口

原則上,接口只是抽象方法的列表。當我們創建一個實現某個接口的類時,我們的類必須實現接口中包含的方法(或者我們必須使類抽象)。有包含許多不同方法的接口(例如 List),也有僅包含一種方法的接口(例如ComparatorRunnable)。有些接口沒有單一方法(所謂的標記接口,例如Serializable)。只有一種方法的接口也稱為功能接口。在 Java 8 中,它們甚至標有特殊的註解:@FunctionalInterface. 正是這些單一方法接口適合作為 lambda 表達式的目標類型。正如我上面所說,lambda 表達式是一種包裝在對像中的方法。當我們傳遞這樣一個對象時,我們實際上是在傳遞這個單一的方法。事實證明,我們不關心調用什麼方法。對我們來說唯一重要的是方法參數,當然還有方法的主體。本質上,lambda 表達式是函數式接口的實現。無論我們在何處看到具有單一方法的接口,都可以將匿名類重寫為 lambda。如果接口的方法多於或少於一個,那麼 lambda 表達式將不起作用,我們將改為使用匿名類甚至普通類的實例。現在是深入研究 lambda 的時候了。:)

句法

一般語法是這樣的:

(parameters) -> {method body}
即,方法參數周圍的圓括號、“箭頭”(由連字符和大於號組成),然後是大括號中的方法主體,一如既往。參數對應於接口方法中指定的參數。如果編譯器可以明確地確定變量類型(在我們的例子中,它知道我們正在使用字符串數組,因為我們的對像是List使用String[] 類型化的),那麼您不必指明它們的類型。
如果它們不明確,則指出類型。如果不需要,IDEA 會將其著色為灰色。
您可以在此Oracle 教程和其他地方閱讀更多內容。這稱為“目標類型”。您可以隨意命名變量——您不必使用界面中指定的相同名稱。如果沒有參數,則僅指示空括號。如果只有一個參數,只需指明變量名即可,不帶括號。現在我們了解了參數,是時候討論 lambda 表達式的主體了。在大括號內,您可以像編寫普通方法一樣編寫代碼。如果您的代碼只有一行,那麼您可以完全省略花括號(類似於 if 語句和 for 循環)。如果您的單行 lambda 返回某些內容,則您不必包含return陳述。但是如果你使用大括號,那麼你必須顯式地包含一個return語句,就像你在普通方法中所做的那樣。

例子

示例 1。

() -> {}
最簡單的例子。最沒有意義的 :),因為它什麼都不做。 示例 2。

() -> ""
另一個有趣的例子。它什麼都不帶,返回一個空字符串(return被省略,因為它是不必要的)。這是同樣的事情,但是有return

() -> { 
    return ""; 
}
示例 3. “你好,世界!” 使用 lambda

() -> System.out.println("Hello, World!")
它什麼都不接受,什麼也不返回(我們不能return在對 的調用之前放置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 表達式保存為一個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. 因此,此代碼將創建一個對象,其方法不接受或返回任何內容。Runnable這與接口的方法完美匹配run()。這就是我們能夠將此 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]); 
}
同樣,這是一個沒有參數和void返回類型的 lambda 表達式(因為沒有return語句)。  例 6

x -> x
這裡我們獲取一個x變量並返回它。請注意,如果只有一個參數,則可以省略它周圍的括號。這是同樣的事情,但有括號:

(x) -> x
這是一個帶有顯式返回語句的示例:

x -> { 
    return x;
}
或者像這樣帶有括號和返回語句:

(x) -> { 
    return x;
}
或者使用類型的明確指示(因此帶有括號):

(int x) -> x
例 7

x -> ++x
我們獲取x並返回它,但僅在加 1 之後。您可以像這樣重寫該 lambda:

x -> x + 1
在這兩種情況下,我們都省略了參數和方法主體以及語句周圍的括號return,因為它們是可選的。示例 6 中給出了帶有括號和 return 語句的版本。 示例 8

(x, y) -> 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對象、一個String名稱和一個整數年齡。在方法本身中,我們使用傳遞的名稱和年齡來設置貓的變量。因為我們的cat對像是引用類型,它會在 lambda 表達式之外被改變(它會得到傳遞的名字和年齡)。這是一個使用類似 lambda 的稍微複雜的版本:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

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

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

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

class Cat implements HasNameAndAge { 
    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='Smokey', age=3}
如您所見,Cat對像有一個狀態,然後在我們使用 lambda 表達式後狀態發生了變化。Lambda 表達式與泛型完美結合。如果我們需要創建一個Dog也實現了的類,那麼我們可以在方法HasNameAndAge中執行相同的操作, 而無需更改 lambda 表達式。 任務 3.編寫一個函數式接口,其中包含一個接受數字並返回布爾值的方法。將此類接口的實現編寫為 lambda 表達式,如果傳遞的數字可被 13 整除,則返回 true。 任務 4。Dogmain()使用接受兩個字符串並返回一個字符串的方法編寫函數式接口。將此類接口的實現編寫為返回較長字符串的 lambda 表達式。 任務 5.編寫一個函數式接口,其方法接受三個浮點數:a、b 和 c,並返回一個浮點數。將此類接口的實現編寫為返回判別式的 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