1. 程式人生 > >Java 8 Lambda表達式一看就會

Java 8 Lambda表達式一看就會

郵件 world! call() 靜態方法 ready function 性別 operation rom

匿名內部類的一個問題是:當一個匿名內部類的實現非常簡單,比如說接口只有一個抽象函數,那麽匿名內部類的語法有點笨拙且不清晰。我們經常會有傳遞一個函數作為參數給另一個函數的實際需求,比如當點擊一個按鈕時,我們需要給按鈕對象設置按鈕響應函數。lambda表達式就可以把函數當做函數的參數,代碼(函數)當做數據(形參),這種特性滿足上述需求。當要實現只有一個抽象函數的接口時,使用lambda表達式能夠更靈活。

使用Lambda表達式的一個用例

假設你正在創建一個社交網絡應用。你現在要開發一個可以讓管理員對用戶做各種操作的功能,比如搜索、打印、獲取郵件等操作。假設社交網絡應用的用戶都通過Person類表示:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    private String name;

    private LocalDate birthday;

    private Sex gender;

    private String emailAddress;

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

    public void printPerson() {
        // ...
    }
}

假設社交網絡應用的所有用戶都保存在一個 List<Person>

的實例中。

我們先使用一個簡單的方法來實現這個用例,再通過使用本地類、匿名內部類實現,最終通過lambda表達式做一個高效且簡潔的實現。

方法1:創建一個根據某一特性查詢匹配用戶的方法

最簡單的方式是創建幾個函數,每個函數搜索指定的用戶特征,比如searchByAge()這種方法,下面的方法打印了年齡大於某特定值的所有用戶:

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

這個方法是有潛在的問題的,如果引入一些變動(比如新的數據類型)這個程序會出錯。假設更新了應用且變化了Person類,比如使用出生年月代替了年齡;也有可能搜索年齡的算法不同。這樣你將不到不再寫許多API來適應這些變化。

方法2:創建一個更加通用的搜索方法

這個方法比起printPersonsOlderThan更加通用;它提供了可以打印某個年齡區間的用戶:

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

如果想打印特定的性別或者打印同時滿足特定性別和某年齡區間的用戶呢?如果要改動Person類,添加其他屬性,比如戀愛狀態、地理位置呢?盡管這個方法比printPersonsOlderThan方法更加通用,但是每個查詢都創建特定的函數都是有可以導致程序不夠健壯。你可以使用接口將特定的搜索轉交給需要搜索的特定類中(面向接口編程的思想——簡單工廠模式)。

方法3:在本地類中設定特定的搜索條件

下面的方法可以打印出符合搜索條件的所有用戶信息

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

這個方法通過調用tester.test方法檢測每個roster列表中的元素是否滿足搜索條件。如果tester.test返回true,則打印符合條件的Person實例。

通過實現CheckPerson接口實現搜索。

interface CheckPerson {
    boolean test(Person p);
}

下面的類實現了CheckPerson接口的test方法。如果Person的屬性是男性並且年齡在18到25歲之間將會返回true

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

當要使用這個類的時候,只需要實例化一個實例,並將實例以參數的形式傳遞給printPersons方法。

printPersons(roster, new CheckPersonEligibleForSelectiveService());

盡管這個方式不那麽脆弱——當Person發生變化時你不需要重新更多方法,但是你仍然需要在添加一些代碼:要為每個搜索標準創建一個本地類來實現接口。CheckPersonEligibleForSelectiveService類實現了一個接口,你可以使用一個匿內部類替代本地類,通過聲明一個新的內部類來滿足不同的搜索。

方法4:在匿名內部類中指定搜索條件

下面的printPersons函數調用的第二個參數是一個匿名內部類,這個匿名內部類過濾滿足性別為男性並且年齡在18到25歲之間的用戶:

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

這個方法減少了很多代碼量,因為你不必為每個搜索標準創建一個新類。但是,考慮到CheckPerson接口只有一個函數,匿名內部類的語法有顯得有點笨重。在這種情況下,可以考慮使用lambda表達式替換匿名內部類,像下面介紹的這種。

方法5:通過Lambda表達式實搜索接口

CheckPerson接口是一個函數式接口。接口中只有一個抽象方法的接口屬於函數式接口(一個函數式接口也可能包換一個活多個默認方法或者靜態方法)。由於函數式接口只包含一個抽象方法,你可以在實現該方法的時候省略方法的名字。因此你可以使用lambda表達式取代匿名內部類表達式,像下面這樣調用:

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

lambda表達式的語法後面會做詳細介紹。你還可以使用標準的函數式接口取代CheckPerson接口,這樣會進一步減少代碼量。

方法6:使用標準的函數式接口和Lambda表達式

CheckPerson接口是一個非常簡單的接口:

interface CheckPerson {
    boolean test(Person p);
}

它只有一個抽象方法,因此它是一個函數式接口。這個函數有個一個參數和一個返回值。它太過簡單以至於沒有必要在你應用中定義它。因此JDK中定義了一些標準的函數式接口,可以在java.util.function包中找到。比如,你可以使用Predicate&lt;T&gt;取代CheckPerson。這個接口中只包含boolean test(T t)方法。

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

Predicate&lt;T&gt;是一個泛型接口,泛型需要在尖括號(<>)指定一個或者多個參數。這個接口中只包換一個參數T。當你聲明或者通過一個真實的類型參數實例化泛型後,你將得到一個參數化的類型。比如,參數化後的類型Predicate&lt;Person&gt;像下面代碼所示:

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

參數化後的的接口包含一個接口,這和 CheckPerson.boolean test(Person p)完全一樣。因此,你可以像下面的代碼一樣使用Predicate&lt;T&gt; 取代CheckPerson

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

那麽,可以這樣調用這個函數:

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

這個不是使用lamdba表達式的唯一的方式。建議使用下面的其他方式使用lambda表達。

方法7:在應用中全都使用Lambda表達式

再來看看方法printPersonsWithPredicate哪裏還可以使用lambda表達式:

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

這個方法檢測roster中的每個Person實例是否滿足tester的標準。如果Person實例滿足tester中設定的標準,那麽Person實例的信息將會被打印出來。

你可以指定一個不同的動作來執行打印滿足tester中定義的搜索條件的Person實例。你可以指定這個動作是一個lambda表達式。假設你想要一個功能和printPerson一樣的lambda表示式(一個參數、返回void),你需要實現一個函數式接口。在這種情況下,你需要一個包含一個只有一個Person類型參數和返回void的函數式接口。Consumer&lt;T&gt;接口包換一個void accept(T t)函數,它符合上述需求。下面的函數使用 Consumer&lt;Person&gt; 調用accept()從而取代了p.printPerson()的調用。

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

那麽可以這樣調用processPersons函數:

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

如果你想對用戶的信息進行更多處理而不止打印出來,那該怎麽辦呢?假設你想驗證成員的個人信息或者獲取他們的聯系人的信息呢?在這種情況下,你需要一個有返回值的抽象函數的函數式接口。Function&lt;T,R&gt;接口包含了R apply(T t)方法,有一個參數和一個返回值。下面的方法獲取參數匹配到的數據,然後根據lambda表達式代碼塊做相應的處理:

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

下面的函數從roster中獲取符合搜索條件的用戶的郵箱地址,並將地址打印出來。

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

方法8:使用泛型使之更加通用

再處理processPersonsWithFunction函數,下面的函數可以接受包含任何數據類型的集合:

public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

可以這樣調用上述函數來實現打印符合搜索條件的用戶的郵箱:

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

該方法的調用只要執行了下面動作:

  1. 從集合中獲取對象,在這個例子中它是包換Person實例的roster集合。roster是一個List類型,同時也是一個Iterable類型。
  2. 過濾符合Predicate數據類型的tester的對象。在這個例子中,Predicate對象是一個指定了符合搜索條件的lambda表達式。
  3. 使用Function類型的mapper映射每個符合過濾條件的對象。在這個例子中,Function對象時要給返回用戶的郵箱地址。
  4. 對每個映射到的對象執行一個在Consumer對象塊中定義的的動作。在這個例子中,Consumer對象時一個打印Function對象返回的電子郵箱的lamdba表達式。

你可以通過一個聚合操作取代上述操作。

方法9:使用lambda表達式作為參數的合並操作

下面的例子使用了聚合操作,打印出了符合搜索條件的用戶的電子郵箱:

roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

下面的表映射了processElements函數執行操作和與之對應的聚合操作

processElements動作 聚合操作
獲取對象源 Stream<E> stream()
過濾符合Predicate對象(lambda表達式)的實例 Stream<T> filter(Predicate<? super T> predicate)
使用Function對象映射符合過濾標準的對象到一個值 <R> Stream<R> map(Function<? super T,? extends R> mapper)
執行Consumer對象(lambda表達式)設定的動作 void forEach(Consumer<? super T> action)

filter,mapforEach是聚合操作。聚合操作是從stream中處理各個元素的,而不是直接從集合中(這就是為什麽第一個調用的函數是stream())。steam是對各個元素進行序列化操作。和集合不同,它不是一個儲存數據的數據結構。相反地,stream加載了源中的值,比如集合通過pipeline將數據加載到stream中。pipeline是stream的一種序列化操作,這個例子中的就是filter- map-forEach。還有,聚合操作通常可以接收一個lambda表達式作為參數,這樣你可自定義需要的動作。

在GUI程序中使用lambda表達式

為了處理一個圖形用戶界面(GUI)應用中的事件,比如鍵盤輸入事件,鼠標移動事件,滾動事件,你通常是實現一個特定的接口來創建一個事件處理。通常,時間處理接口就是一個函數式接口,它們通常只有一個函數。

之前使用匿名內部類實現的時間相應:

 btn.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

可以使用如下代碼替代:

 btn.setOnAction(
          event -> System.out.println("Hello World!")
        );

Lambda表達式語法

一個lambda表達式由一下結構組成:

  • ()括起來參數,如果有多個參數就使用逗號分開。CheckPerson.test函數有一個參數p,代表Person的一個實例。

    註意: 你可以省略lambda表達式中的參數類型。另外,如果只有一個參數也可以省略括號。比如下面的lambda表達式也是合法的:

p -> p.getGender() == Person.Sex.MALE 
    && p.getAge() >= 18
    && p.getAge() <= 25
  • 箭頭符號:->
  • 主體:有一個表達式或者一個聲明塊組成。例子中使用這樣的表達式:
p.getGender() == Person.Sex.MALE 
    && p.getAge() >= 18
    && p.getAge() <= 25

如果設定的是一個表達式,java運行時將會計算表達式並最終返回結果。同時,你可以使用一個返回聲明:

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

在lambda表達式中返回的不是一個表達式,那麽就必須使用{}將代碼塊括起來。但是,當返回的是一個void類型時則不需要括號。比如,下面的也是一個合法的lambda表達式:

email -> System.out.println(email)

lambda表達式看起來有點像聲明函數,可以把lambda表達式看做是一個匿名函數(沒有名稱的函數)。

下面是一個有多個形參的lambda表達式的例子:

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));    
    }
}

方法operateBinary執行兩個數的數學操作。操作本身是對IntegerMath類的實例化。實例中通過lambda表達式定義了兩種操作,加法和減法。例子輸出結果如下:

40 + 2 = 42
20 - 10 = 10

獲取閉包中的本地變量

像本地類和匿名類一樣,lambda表達式也可以訪問本地變量;它們有訪問本地變量的權限。lambda表達式也是屬於當前作用域的,也就是說它不從父級作用域中繼承任何命名名稱,或者引入新一級的作用域。lambda表達式的作用域就是聲明它所在的作用域。下面的這個例子說明了這一點:

import java.util.function.Consumer;

public class LambdaScopeTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {

            Consumer<Integer> myConsumer = (y) -> 
            {
                System.out.println("x = " + x); 
                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);
    }
}

將會輸出如下信息:

x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0

如果像下面這樣在lambda表達式myConsumer中使用x取代參數y,那麽編譯將會出錯。

Consumer<Integer> myConsumer = (x) -> {
}

編譯會出現"variable x is already defined in method methodInFirstLevel(int)",因為lambda表達式不引入新的作用域(lambda表達式所在作用域已經有x被定義了)。因此,可以直接訪問lambda表達式所在的閉包的成員變量、函數和閉包中的本地變量。比如,lambda表達式可以直接訪問方法methodInFirstLevel的參數x。可以使用this關鍵字訪問類級別的作用域。在這個例子中this.x對成員變量FirstLevel.x的值。

然而,像本地和匿名類一樣,lambda表達式值可以訪問被修飾成final或者effectively final的本地變量和形參。比如,假設在methodInFirstLevel中添加定義聲明如下:

Effectively Final:一個變量或者參數的值在初始化後就不在發生變化,那麽這個變量或者參數就是effectively final類型的。

void methodInFirstLevel(int x) {
    x = 99;
}

由於x =99的聲明使methodInFirstLevel的形參x不再是effectively final類型。結果java編譯器就會報類似"local variables referenced from a lambda expression must be final or effectively final"的錯誤。

目標類型

在運行時java是怎麽判斷lambda表達式的數據類型的?再看一下那個要選擇性別是男性,年齡在18到25歲之間的lambda表達式:

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

這個lambda表達式已參數的形式傳遞到如下兩個函數:

  • public static void printPersons(List<Person> roster, CheckPerson tester)
  • public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)

當java運行時調用方法printPersons時,它期望一個CheckPerson類型的數據,因此lambda表達式就是這種類型。當java運行時調用方法printPersonsWithPredicate時,它期望一個Predicate&lt;Person&gt;類型的數據,因此lambda表達式就是這樣一個類型。這些方法期望的數據類型就叫目標類型。為了確定lambda表達式的類型,java編譯器會在lambda表達式的的上下文中判斷它的目標類型。只有java編譯器可推測出來了目標類型,lambda表達式才可以被執行。

目標類型和函數參數

對於函數參數,java編譯器可以確定目標類型通過兩種其他語言特性:重載解析和類型參數推斷。看下面兩個函數式接口( java.lang.Runnable and java.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

方法 Runnable.run 不返回任何值,但是 Callable&lt;V&gt;.call 有返回值。假設你像下面一樣重載了方法invoke

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

那麽執行下面程序哪個方法將會被調用呢?

String s = invoke(() -> "done");

方法invoke(Callable&lt;T&gt;)會被調用,因為這個方法有返回一個值;方法invoke(Runnable)沒有返回值。在這種情況下,lambda表達式() -&gt; "done"的類型是Callable&lt;T&gt;

最後

感謝閱讀,有興趣可以關註微信公眾賬號獲取最新推送文章。

技術分享圖片

Java 8 Lambda表達式一看就會