java8官方文檔—Lambda表達式

聲明:本文翻譯自The Java? Tutorials(官方文檔)

簡述

匿名類有一個問題,如果匿名類的實現(xiàn)非常簡單,比如一個只包含一個方法的接口,那么這樣的匿名類的語法是笨拙和不清晰的。在這些情況下,你通常會嘗試將功能作為參數(shù)傳遞給另一個方法,比如某人點擊按鈕時,它將發(fā)生的具體行為。lambda表達式允許你這樣做,將功能作為方法的參數(shù),或者把代碼當作數(shù)據(jù)。

上一節(jié),Anonymous Classes ,為你展示了如何實現(xiàn)一個匿名的基礎類。雖然它通常比有名稱的類更加簡潔,對于只有一個方法的類,甚至是匿名類,依然顯得有點過于笨拙。lambda表達式可以讓你更加簡潔的表達這種只有單一方法類的實例。

Lambda表達式使用示例

假如你正在開發(fā)一個社交應用。你想開發(fā)一個功能,使管理員可以執(zhí)行任何類型的動作,如發(fā)送消息給某些滿足一定條件的社交應用成員。下面的表格描述了這個應用的細節(jié):

字段 描述
名稱 對選定的成員執(zhí)行操作
主要角色 管理員
前置條件 管理員已經(jīng)登錄系統(tǒng)
后置條件 只對符合條件的成員進行操作
主要場景 1.管理員指定執(zhí)行某個操作的成員的標準
2.管理指定對選定成員執(zhí)行的操作
3.管理員選擇提交按鈕
4.系統(tǒng)查找所有符合條件的成員
5.系統(tǒng)對符合條件的成員執(zhí)行指定的操作
擴展 管理員可以選擇在指定要執(zhí)行的操作之前或在選擇提交按鈕之前,對符合指定條件的成員進行預覽
發(fā)生頻率 一天多次

假如社交應用的成員通過下面的Person類來描述:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

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

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

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

假如社交應用的成員實例存儲在List<Person>中。
本節(jié)以一個簡單的方法開始這個例子。它通過局部類、匿名類逐步改進方法,然后用lambda表達式以一種簡明的方式來完成。本節(jié)中的代碼摘錄自例子RosterTest,Person

方式1:創(chuàng)建搜索匹配一個特征的成員的方法

以一種簡單的方式創(chuàng)建幾個方法,每個方法查找匹配某一特征的成員,比如性別或者年齡。下面的方法打印出大于指定年齡的成員:

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

注意List是一個有序的集合。集合是將多個元素組合成一個單元的對象。集合用于存儲、檢索、操作和傳遞聚集數(shù)據(jù)。更多關于集合的信息,查看Collections路徑。

這種方式可能使得你的應用很脆弱,當你對類進行更新時(比如一個新的數(shù)據(jù)類型)可能導致應用無法工作。假設你更新應用,改變Person類的結構,可能是增加一個不同的成員變量,也可能是用一個新的數(shù)據(jù)類型或算法來記錄年齡信息。為了適應這些變化,你必須要重寫大量的API。另外,如果你想打印一個小于指定年齡的成員,這個類也將給你不必要的限制。這該如何解決呢?

方式2:創(chuàng)建更通用的搜索方法

下面的方法比pringPersonsOlderThan更通用。它打印年齡在一定范圍內(nèi)的成員:

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類,添加其他的屬性比如關系狀態(tài)或者地理定位,該怎么辦?雖然這個方法比pringPersonsOlderThan更通用,但嘗試為每個可能的搜索查詢創(chuàng)建一個單獨的方法仍然會導致代碼的脆弱。相反,你可以將按指定條件搜索的代碼分開在不同的類中。

方式3:在局部類中指定搜索條件代碼

下面的方法打印符合搜索條件的成員:

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

這個方法通過調(diào)用CheckPerson參數(shù)tester中的tester.test方法,來檢查每一個包含在List參數(shù)roster中的Person實例。如果方法tester.test返回一個true值,那么Person實例中的printPersons方法將會被調(diào)用。

你通過實現(xiàn)CheckPerson接口,來指定查詢條件:

interface CheckPerson {
    boolean test(Person p);
}

下面的類實現(xiàn)CheckPerson接口,來為test方法提供一個實現(xiàn)。這個方法篩選出了有資格為美國服役的成員:如果它的Person參數(shù)是男性并且年齡介于18到25之間,它將會返回true值:

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

要使用這個例子,你需要創(chuàng)建一個新的實例并且調(diào)用printPersons方法:

printPersons(
    roster, new CheckPersonEligibleForSelectiveService());

雖然這個方式比前面的方式要好一點——當改變Person的結構時,你不需要重寫方法——但你仍然有一些額外的多余的代碼:每一個你計劃要在應用中執(zhí)行的查詢,都需要一個新的接口和局部類。因為CheckPersonEligibleForSelectiveService實現(xiàn)了一個接口,您可以使用匿名類代替局部類,繞過需要為每個搜索聲明一個新類。

方式4:在匿名類中指定查詢條件代碼

在下面的printPersons方法調(diào)用中,其中一個參數(shù)是一個匿名類,這個匿名類篩選了有資格為美國服役的成員:他們必須時男性并且年齡介于18到25歲之間:

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

這種方法減少了代碼量,因為你不需要為你想要執(zhí)行的每個搜索創(chuàng)建一個新的類。但是,由于CheckPerson接口只包含一個方法,因此匿名類的語法非常笨重。在本例中,可以使用lambda表達式代替匿名類,如下一節(jié)所述。

方式5:用Lambda表達式指定查詢條件代碼

CheckPerson接口是一個函數(shù)式的接口。函數(shù)式接口是任何只包含一個抽象方法的接口。(一個函數(shù)式接口可能包含一個或者更多默認方法或者靜態(tài)方法.)因為一個函數(shù)式接口只包含一個抽象方法,實現(xiàn)它的時候你可以省略方法名。要做到這一點,您可以使用lambda表達式,而不是使用匿名類表達式:

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

您可以使用標準的函數(shù)式接口來代替接口CheckPerson,這將進一步減少所需的代碼量。

方式6:使用帶有標準函數(shù)式接口的Lambda表達式

重新考慮CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

這是一個非常簡單的接口。它是一個函數(shù)式接口,因為它只包含一個抽象方法。這個方法帶有一個參數(shù)并且返回一個boolean值。這個方法式如此的簡單,所以它可能沒有必要定義在你的應用中。因此JDK定義了幾個標準的函數(shù)式接口,你可以在java.util.function包中找到它們。

例如,你可以使用Predicate<T>接口代替CheckPerson。這個接口包含一個方法boolean test(T t):

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

接口Predicate<T>是一個泛型接口的例子。(更多關于泛型的知識,請看Generics (Updated)。)泛型類型(比如泛型接口)使用尖括號(<>)指定一個或者多個參數(shù)。這個接口只有一個類型參數(shù)T。當您使用實際類型參數(shù)聲明或實例化泛型類型時,你就有了一個參數(shù)化類型。例如,一個參數(shù)化類型Predicate<Person>如下所示:

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

這個參數(shù)化類型包含一個和CheckPerson.boolean test(Person p)有相同返回值和參數(shù)的方法。因此,你可以使用Predicate<T>代替CheckPerson,如下面的示例:

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

因此,下面的方法調(diào)用和你在方式3中調(diào)用printPersons一樣:

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

在這個方法中,這并不是使用lambda表達式的惟一可能的地方。下面介紹其他使用Lambda表達式的地方。

方式7:在整個應用中使用Lambda表達式

重新考慮printPersonWithPredicate方法中其他可以使用Lambda表達式的位置:

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

該方法檢查List參數(shù)roster中的每個Person實例,通過Predicate參數(shù)tester測試Person實例是否滿足條件。如果滿足,就調(diào)用Person實例的printPerson方法。

除了調(diào)用printPerson方法,你可以通過Lambda表達式指定在Person實例上進行的操作。假設你需要一個類似printPerson功能的Lambda表達式,接收一個參數(shù)(Person類型的對象)并返回void。請記住,要使用lambda表達式,你必須實現(xiàn)一個函數(shù)式接口。在這里,你需要一個包含一個抽象方法的函數(shù)式接口,該抽象方法接收一個Person類參數(shù)并返回void。Consumer<T>接口包含了方法void accept<T t>,恰好符合要求。下面的方法將使用Consumer<Person>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);
            }
        }
}

因此,下面的方法調(diào)用和你在方式3中調(diào)用printPersons效果是一樣的:

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

如果你需要對成員信息做更多的操作而非只是打印。假設你需要驗證成員的個人信息或者獲取他們的聯(lián)系方式,該怎么做?在這種情況下,你需要一個函數(shù)式接口,該接口包含了一個能返回某值的抽象方法。Function<T, R>接口包含了R apply(T t)方法。下面的方法獲取了mapper參數(shù)指定的數(shù)據(jù),并對其執(zhí)行由block參數(shù)指定的操作:

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

下面的方法獲取了每個成員郵箱地址,并且打印出來:

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如下:

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

該方法調(diào)用執(zhí)行了如下操作:
1.獲取對象集合。在本例中,獲取了Person對象的集合roster。值得注意的是集合roster是屬于List類型的集合同時也是Iterable類型的對象。
2.篩選出滿足Predicate對象tester指定條件的對象。本例中,Predicate對象由一個Lambda表達式提供,表達式指定了篩選成員的條件。
3.通過Function對象將篩選出的對象映射為一個值。本例中,Function對象由一個返回成員郵箱的Lambda表達式提供。
4.在每一個映射后的值上執(zhí)行由Consumer對象block指定的操作。本例中,Consumer對象是一個Lambda表達式,表達式打印由Function對象返回的電子郵箱地址。

你還可以將這每個操作替換為聚集操作(aggregate operation)。

方式9:使用接受Lambda表達式作為參數(shù)的聚合操作

接下來的示例使用了聚集操作完成了方案8中相同的工作:

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方法執(zhí)行的每個操作與對應聚集操作的對照:

processElements Action Aggregate Operation
Obtain a source of objects Stream<E> stream()
Filter objects that match a Predicate object Stream<T> filter(Predicate<? super T> predicate)
Map objects to another value as specified by a Function object <R> Stream<R> map(Function<? super T,? extends R> mapper)
Perform an action as specified by a Consumer object void forEach(Consumer<? super T> action)

filter,map以及forEach操作都是聚集操作。聚集操作從一個流中獲取需要操作的元素,而不是直接從集合中獲?。ㄟ@就是開始需要調(diào)用stream的原因)。一個流是元素的序列,不同于集合,它不是一個存儲元素的數(shù)據(jù)結構。相反,流通過管道從源(例如集合)中運輸數(shù)據(jù)。管道是一系列的流操作,本例中就是filter-map-forEach。除此之外,聚集操作一般使用Lambda表達式作為其參數(shù),從而達到自定義的目的。

更深入的關于聚合操作的討論,請看Aggregate Operations課程。

在GUI程序中應用Lambda表達式

在圖形用戶界面的應用中響應諸如鍵盤事件、鼠標事件或者滾動事件的時候,你一般都需要創(chuàng)建事件處理器,而這通常涉及到實現(xiàn)某個接口。通常情況下,事件處理接口都是包含一個方法的函數(shù)式接口。

在匿名類一節(jié)中討論了javaFX示例HelloWorld.java,你可以使用Lambda表達式替代匿名類:

btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

btn.setOnAction方法指定了當btn代表的按鈕被觸發(fā)時需要執(zhí)行的操作。這個方法需要一個EventHandler<ActionEvent>類型的對象。EventHandler<ActionEvent>接口只包含了一個方法,void handle(T event)。這是一個函數(shù)接口,因此你可以如下使用Lambda表達式代替:

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

Lambda表達式的語法

一個lambda表達式由以下幾個部分組成:

  • 包圍在小括號內(nèi)的用逗號分隔的形參列表。CheckPerson.test方法包含了一個參數(shù)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運行時對表達式求值然后返回該值?;蛘?,你也可以使用return 語句:

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

return語句由于不是一個表達式,因而你必須將它放置在一對花括號({})中。然而你不必將返回void的方法調(diào)用放到花括號中。例如,下面是一個合法的Lambda表達式:

email -> System.out.println(email)

鑒于Lambda看起來和方法聲明很相似;你可以將Lambda表達式看成是匿名方法。

接下來的示例,Calculator展示了接收一個以上形參的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方法對兩個整數(shù)進行數(shù)學運算。運算的方法由IntegerMath實例提供。該示例使用Lambda定義了兩個運算,additionsubtraction。以下是示例的輸出:

40 + 2 = 42
20 - 10 = 10

訪問閉包環(huán)境的局部變量

和局部類及匿名類一樣,Lambda表達式也可以捕獲變量;他們對于局部變量有相同的訪問權限。然而和局部及匿名類不同的是,Lambda表達式不存在任何隱匿問題(shadowing issues)。Lambda表達式只是詞法意義上的作用域。這意味著它們沒有從超類繼承任何標識符或者是引入了新的作用域。Lambda中的聲明和在外部作用域中聲明是等價的。下面的示例,LambdaScopeTest,展示了這點:

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

示例的輸出如下:

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

如果將Lambda表達式myConsumer中的參數(shù)y替換為x,編譯器會提示錯誤:

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

編譯器提示錯誤“變量x已經(jīng)在方法methodInFirstLevel(int)定義了”,因為Lambda表達式并沒有引入新的作用域。因此,你可以直接訪字段、方法以及外部作用域中的局部變量。例如Lambda表達式可以直接訪問methodInFirstLevel的參數(shù)x。需要訪問外部類的變量,需使用 this關鍵字。在該示例中,this.x指向的是成員變量FirstLevel.x。

然而如同局部類及匿名類,Lambda表達式只可以訪問final或者effectively final類型的局部變量和外部作用域參數(shù)。假如在methodFirstLevel的開始處加入如下的賦值語句:

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

由于這個賦值語句,變量FirstLevel.x不再是effectively final了。此時編譯器就會在Lambda表達式試圖訪問FirstLevel.x變量的地方輸出錯誤信息,“Lambda表達式所訪問的局部變量必須是final或者effectively final”。

System.out.println("x = " + x);

** 譯者注:** 關于effectively final作一點補充,在 Java SE 7 中,編譯器對內(nèi)部類中引用的外部變量(即捕獲的變量)要求非常嚴格:如果捕獲的變量沒有被聲明為 final 就會產(chǎn)生一個編譯錯誤。Java SE 8現(xiàn)在放寬了這個限制——對于 Lambda 表達式和內(nèi)部類,允許在其中捕獲那些符合有效只讀Effectively final)的局部變量。簡單的說,如果一個局部變量在初始化后從未被修改過,那么它就符合有效只讀的要求,換句話說,加上final 后也不會導致編譯錯誤的局部變量就是有效只讀變量。

確定Lambda的類型

我們是怎么決定一個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) 在方式3中
  • public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) 在方式6中

當java運行調(diào)用printPersons時,它期望一個CheckPerson數(shù)據(jù)類型,因而Lambda表達式就是這個類型了。然而,當Java運行時調(diào)用printPersonsWithPredicate的時候,它期望一個Predicate<Person>類型,因而Lambda表達式就成為了這個類型的實例。這些方法期望的數(shù)據(jù)類型被稱為目標類型(target type)。為了決定一個Lambda的類型,當java編譯器發(fā)現(xiàn)一個Lambda表達式的時候,它會使用當前上下文或者環(huán)境的目標類型。這也就意味著你只能在編譯器能夠確定目標類型的情形下使用Lambda表達式:

  • 變量的聲明
  • 賦值
  • return 語句
  • 數(shù)組初始化
  • 方法或者構造函數(shù)參數(shù)
  • Lambda表達式body
  • 條件表達式 ?:
  • 類型轉換

目標類型與方法參數(shù)

對于方法參數(shù),java編譯器使用兩個特性來進行決定目標類型:重載決議(overload resolution)和類型推導(argument inference)。如下面兩個函數(shù)接口(java.lang.Runnablejava.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

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

方法Runnable.run沒有返回值,然而Callable<V>.call有。假設你重載了invoke方法:

void invoke(Runnable r) {
    r.run();
}
<T> T invoke(Callable<T> c) {
    return c.call();
}

那么如下語句將會調(diào)用那個函數(shù)?

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

invoke(Callable<T>)方法將會被調(diào)用,因為該方法有返回值而invoke(Runnable)沒有。在這種情況下,Lambda表達式()->"done"的類型為Callable<T>

序列化

如果Lambda表達式的目標參數(shù)和捕獲參數(shù)都是可以可序列化的,那么該Lambda表達式也是可以序列化的。然而,正如內(nèi)部類一樣,對其序列化是極不提倡的。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容