Java8新特性一:Lambda Expressions

Lambda表達式

匿名類存在的問題是: 如果匿名類的實現非常簡單,例如僅包含一個方法的接口,則匿名類的語法可能看起來很笨拙且不清楚。在這些情況下,您通常 new一個匿名內部類對象作為參數傳遞給方法,例如,當某人單擊按鈕時應採取什麼措施。Lambda表達式 能實現這樣的需求,它可以更緊湊更簡潔的表達單方法類的實例。

本篇文章從以下幾點介紹一下Lambda表達式:

  1. Lambda表達式用例搜索匹配一個特徵的用戶更通用的搜索方法在類中指定搜索條件在匿名類中指定搜索條件使用Lambda表達式指定搜索條件將functional interface與Lambda表達式一起使用更廣泛的使用Lambda表達式
  2. Lambda表達式的語法
  3. 訪問局部變量
  4. 目標類型

Lambda表達式的用例

假設您正在開發一個社交網絡程序。您想新增一個功能,使管理員可以對滿足特定條件的社交網絡用戶執行任何類型的操作,例如發送消息。

用戶用以下Person類表示:

<code>public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}/<code>

並且用戶存儲在一個List實例中。

我們先從最笨的實現開始,然後使用本地和匿名類對該方法進行改進,最後再使用lambda表達式以一種高效而簡潔的方式實現。

搜索匹配一個特定特徵的用戶

一種簡單的實現是:創建幾個方法,每個方法都會搜索和一個特徵(例如性別或年齡)相匹配的成員。以下方法將打印出超過指定年齡的成員:

<code>public static void printPersonsOlderThan(List roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}/<code>

這樣做可以滿足業務需求,但是他的可擴展性非常差,並且每個特徵的一種搜索需要寫一個方法,很麻煩。可以考慮以下幾個問題:

  1. 類的屬性有100個甚至1000個呢?
  2. 如果要打印小於的頂年齡的成員呢?

更通用的搜索方法

以下方法比前面的 printPersonsOlderThan方法更通用,它會打印指定年齡範圍內的成員:

<code>public static void printPersonsWithinAgeRange(List roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}/<code>

考慮以下幾個問題:

  1. 如果要打印指定性別或指定性別和年齡範圍的組合,該怎麼辦?
  2. 如果您決定更改Person班級並添加其他屬性,例如關係狀態或地理位置,該怎麼辦?
  3. 儘管此方法比上面的printPersonsOlderThan通用,但嘗試為每個特徵創建單獨的方法仍會導致代碼脆弱。

在類中指定搜索條件

下面的方法打印和指定的搜索條件匹配的用戶:

<code>public static void printPersons(List roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}/<code>

這個方法會遍歷List中的Person對象,通過CheckPerson檢查每個Person,如果滿足搜索條件,就會輸出Person信息。

要指定搜索條件,實現以下 CheckPerson接口:

<code>interface CheckPerson {
    boolean test(Person p);
}/<code>

下面的類實現了CheckPerson接口並且實現了接口中的test的方法,這個方法篩選男性且年齡在18至25歲之間的用戶。

<code>class CheckPersonEligibleForSelectiveService implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}/<code>

使用CheckPerson

<code>printPersons(
    roster, new CheckPersonEligibleForSelectiveService());/<code>

在匿名類中指定搜索條件

以下方法傳入的第二個參數是一個匿名類,該類篩選男性且年齡在18至25歲之間的用戶:

<code>printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge()  
<= 25; } } );/<code>

這種方法減少了代碼量,因為您不必為每個搜索條件創建一個類。但是,考慮到CheckPerson接口僅包含一個方法,並且匿名類的代碼相當龐大,在這種情況下,您可以使用lambda表達式代替匿名類。

使用Lambda表達式指定搜索條件

CheckPerson 接口是一個functional interface。functional interface是僅包含一個抽象方法的接口 。(functional interface可能包含一個或多個 默認方法或 靜態方法。)由於functional interface僅包含一個抽象方法,因此在實現該方法時可以省略該方法的名稱。因此,可以使用lambda表達式(而不是使用匿名類表達式)。

<code>printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);/<code>

可以使用functional interface來代替CheckPerson,這可以進一步減少所需的代碼量。

將functional interface與Lambda表達式一起使用

<code>interface CheckPerson {
    boolean test(Person p);
}/<code>

CheckPerson一個非常簡單的接口,是一個functional interface。因為JDK已經提供了一些通用的functional interface,可以在java.util.function包中找到它們。所以,我們可以直接使用這些functional interface,如果能夠滿足我們的需求,我們沒必要再定義這樣的接口。

例如,可以使用 Predicate 接口代替CheckPerson。該接口包含方法boolean test(T t):

<code>interface Predicate {
    boolean test(T t);
}/<code>

此接口僅包含一個參數類型T。當使用實際參數聲明或實例化泛型類型時,您將擁有一個參數化類型。例如,參數化類型Predicate如下:

<code>interface Predicate {
    boolean test(Person t);
}/<code>

此參數化類型包含一個方法,該方法的參數和返回類型與CheckPerson.boolean test(Person p)相同。因此,可以用Predicate代替CheckPerson:

<code>public static void printPersonsWithPredicate(
    List roster, Predicate tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}/<code>

因此,以下方法調用和在類中指定搜索條件有相同的效果:

<code>printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);/<code>

這不是使用lambda表達式的唯一方式。

更廣泛的使用Lambda表達式

重新看一下printPersonsWithPredicate方法,看看在什麼地方還可以使用lambda表達式:

<code>public static void printPersonsWithPredicate(
    List roster, Predicate tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}/<code>

我們接下來將printPerson方法用Lambda表達式代替,那麼我們需要一個functional interface,該方法可以傳入一個Person類型的參數並返回void。 很幸運,JDK提供的 Consumer 接口滿足這樣的需求。

<code>public static void processPersons(
    List roster,
    Predicate tester,
    Consumer block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}/<code>

調用該方法時的寫法如下:

<code>processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);/<code>

如果想對個人資料進行更多處理而不僅僅打印出來,該怎麼辦?假設要驗證用戶的個人資料或檢索他們的聯繫信息?在這種情況下,需要一個functional interface,其中包含一個有返回值的抽象方法。很幸運,JDK提供的 Function 接口接口滿足這樣的需求。

<code>public static void processPersonsWithFunction(
    List roster,
    Predicate tester,
    Function mapper,
    Consumer block) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = mapper.apply(p);
            block.accept(data);
        }
    }
}/<code>

以下調用先從符合條件的Person中獲取電子郵件信息,然後打印出來:

<code>processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);/<code>

Lambda表達式的語法

Lambda表達式包含以下內容:

  1. 參數以逗號分隔,用括號括起來。CheckPerson.test方法包含一個參數p, 它代表Person類的一個實例。

注意:可以省略lambda表達式中參數的數據類型。此外,如果只有一個參數,則可以省略括號。例如,以下lambda表達式也有效:

<code>p-> p.getGender()== Person.Sex.MALE 
    && p.getAge()> = 18
    && p.getAge()<= 25/<code> 
  1. 箭頭標記 ->
  2. 由單個表達式或語句塊組成。本示例使用以下表達式:
<code>p.getGender()== Person.Sex.MALE 
    && p.getAge()> = 18
    && p.getAge()<= 25/<code>

如果指定單個表達式,將計算表達式並返回其值。另外,可以使用return語句:

<code>p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
}/<code>

return語句不是表達式。在lambda表達式中,必須將語句括在大括號{}中。但是,對void方法的調用不用括在大括號中。例如,以下是有效的lambda表達式:

<code>email -> System.out.println(email)/<code>

注意,lambda表達式看起來很像方法聲明。可以將lambda表達式視為匿名方法,即沒有名稱的方法。

以下示例, Calculator定義多個參數的lambda表達式示例:

<code>public class Calculator {
  
    interface IntegerMath {
        int operation(int a, int b);   
    }
  
    public int operateBinary(int a, int b, IntegerMath op) {
        return op.operation(a, b);
    }
 
    public static void main(String... args) {
    
        Calculator myApp = new Calculator();
        IntegerMath addition = (a, b) -> a + b;
        IntegerMath subtraction = (a, b) -> a - b;
        System.out.println("40 + 2 = " +  myApp.operateBinary(40, 2, addition));
        System.out.println("20 - 10 = " + myApp.operateBinary(20, 10, subtraction));    
    }
}/<code>

該方法operateBinary對兩個整數進行數學運算。由IntegerMath的具體實現來計算。示例中定義了兩個Lambda表達式:addition 和 subtraction。示例輸出以下內容:

<code>40 + 2 = 42
20-10 = 10/<code> 

訪問局部變量

像本地和匿名類一樣,lambda表達式可以訪問變量。它們對局部變量具有相同的訪問權限。

<code>import java.util.function.Consumer;

public class LambdaScopeTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            
            // The following statement causes the compiler to generate
            // the error "local variables referenced from a lambda expression
            // must be final or effectively final" in statement A:
            //
            // x = 99;
            
            Consumer myConsumer = (y) -> 
            {
                System.out.println("x = " + x); // Statement A
                System.out.println("y = " + y);
                System.out.println("this.x = " + this.x);
                System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x);
            };

            myConsumer.accept(x);

        }
    }

    public static void main(String... args) {
        LambdaScopeTest st = new LambdaScopeTest();
        LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}/<code>

本示例輸出:

<code>x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0/<code>

如果在myConsumer聲明裡用參數x代替y,編譯器將報錯:

<code>Consumer myConsumer = (x) -> {
    // ...
}/<code>

錯誤信息:methodInFirstLevel(int)方法已經定義了變量x 。這是因為lambda表達式未引入新的作用域級別。因此,您可以直接訪問該範圍的字段、方法和局部變量。例如,lambda表達式直接訪問methodInFirstLevel方法的x參數。要訪問類中的變量,請使用關鍵字this。在此示例中,this.x引用成員變量FirstLevel.x。

與本地和匿名類一樣,lambda表達式只能訪問用final或effectively final的局部變量和參數。例如,假設您在methodInFirstLevel方法內部添加以下賦值語句:

<code>void methodInFirstLevel(int x){
     x = 99;
    // ...
}/<code>

由於改變了x的值,所以該變量不再是final或實際上final類型的變量。由於lambda表達式myConsumer會訪問FirstLevel.x變量,結果Java編譯器報一條錯誤信息,類似於lambda表達式引用的本地變量必須是final或實際上是final

<code>System.out.println(“ x =” + x);/<code>

目標類型

您如何確定Lambda表達式的類型?回憶一下選擇年齡在18至25歲之間的男性用戶的lambda表達式:

<code>p -> p.getGender() == Person.Sex.MALE
    && p.getAge() >= 18
    && p.getAge() <= 25/<code>

此lambda表達式用在以下兩個方法:

<code>public static void printPersons(List roster, CheckPerson tester)

public void printPersonsWithPredicate(List roster, Predicate tester)/<code>

當調用printPersons方法時,它期望的數據類型為CheckPerson,因此lambda表達式為該類型。但是,當調用printPersonsWithPredicate方法時,它期望的數據類型為Predicate,因此lambda表達式就是這種類型。

這些方法期望的數據類型稱為目標類型。

關注:Java提升營

→「技術乾貨」每日推送

→「2T免費資料」隨時領取


分享到:


相關文章: