Java™ 教程(Lambda表達式)

Lambda表達式

匿名類的一個問題是,若是匿名類的實現很是簡單,例如只包含一個方法的接口,那麼匿名類的語法可能看起來不實用且不清楚,在這些狀況下,你一般會嘗試將功能做爲參數傳遞給另外一個方法,例如當有人單擊按鈕時應採起的操做,Lambda表達式使你能夠執行此操做,將功能視爲方法參數,或將代碼視爲數據。html

上一節匿名類向你展現瞭如何在不給它命名的狀況下實現基類,雖然這一般比命名類更簡潔,但對於只有一個方法的類,即便是匿名類也彷佛有點過多和繁瑣,Lambda表達式容許你更緊湊地表達單方法類的實例。java

Lambda表達式的理想用例

假設你正在建立社交網絡應用程序,你但願建立一項功能,使管理員可以對知足特定條件的社交網絡應用程序成員執行任何類型的操做,例如發送消息,下表詳細描述了此用例:git

字段 描述
名稱 對選定的成員執行操做
主要角色 管理員
前提條件 管理員已登陸系統
後置條件 僅對符合指定條件的成員執行操做
主要成功案例 1. 管理員指定要執行特定操做的成員的條件
2. 管理員指定要對這些選定成員執行的操做
3. 管理員選擇Submit按鈕
4. 系統查找符合指定條件的全部成員
5. 系統對全部匹配成員執行指定的操做
擴展 管理員能夠選擇在指定要執行的操做以前或選擇Submit按鈕以前預覽符合指定條件的成員
發生頻率 一天中不少次

假設此社交網絡應用程序的成員由如下Person類表示:github

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

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

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

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

假設你的社交網絡應用程序的成員存儲在List<Person>實例中。算法

本節首先介紹這種用例的簡單方法,它使用局部和匿名類改進了這種方法,而後使用lambda表達式以高效和簡潔的方法完成,在示例RosterTest中找到本節中描述的代碼摘錄。express

方法1:建立搜索匹配一個特徵的成員的方法

一種簡單的方法是建立幾種方法,每種方法都會搜索與一個特徵匹配的成員,例如性別或年齡,如下方法打印超過指定年齡的成員:segmentfault

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

注意:List是有序集合,集合是將多個元素組合到一個單元中的對象,集合用於存儲、檢索、操做和傳遞聚合數據,有關集合的更多信息,請參閱集合路徑。api

這種方法可能會使你的應用程序變得脆弱,這是因爲引入了更新(例如更新的數據類型)致使應用程序沒法工做的可能性。假設你升級應用程序並更改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來檢查List參數名單中包含的每一個Person實例是否知足CheckPerson參數tester中指定的搜索條件,若是方法tester.test返回true值,則在Person實例上調用方法printPersons

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

interface CheckPerson {
    boolean test(Person p);
}

如下類經過指定方法test的實現來實現CheckPerson接口,此方法可過濾符合美國選擇性服務條件的成員:若是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表達式的信息,請參見Lambda表達式的語法。

你可使用標準功能性接口代替CheckPerson接口,從而進一步減小所需的代碼量。

方法6:將標準功能性接口與Lambda表達式一塊兒使用

從新考慮CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

這是一個很是簡單的接口,它是一個功能性接口,由於它只包含一個抽象方法,此方法接受一個參數並返回一個布爾值,該方法很是簡單,在你的應用程序中定義一個方法可能不值得,所以,JDK定義了幾個標準的功能性接口,你能夠在java.util.function包中找到它們。

例如,你可使用Predicate<T>接口代替CheckPerson,該接口包含方法boolean test(T t)

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

接口Predicate<T>是泛型接口的示例(有關泛型的更多信息,請參閱泛型(更新)課程),泛型類型(例如泛型接口)在尖括號(<>)中指定一個或多個類型參數,該接口僅包含一個類型參數T。當你使用實際類型參數聲明或實例化泛型類型時,你具備參數化類型,例如,參數化類型Predicate<Person>以下:

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

此參數化類型包含一個方法,該方法具備與CheckPerson.boolean test(Person p)相同的返回類型和參數,所以,你可使用Predicate<T>代替CheckPerson,以下面的方法所示:

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

所以,如下方法調用與在方法3:在局部類中指定搜索條件代碼以獲取有資格得到選擇性服務的成員中調用printperson時相同:

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

這不是此方法中使用lambda表達式的惟一可能位置,如下方法提出了使用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();
        }
    }
}

此方法檢查List參數roster中包含的每一個Person實例是否知足Predicate參數tester中指定的條件,若是Person實例知足tester指定的條件,則在Person實例上調用printPersron方法。

你能夠指定一個不一樣的操做來執行那些知足tester指定的條件的Person實例,而不是調用printPerson方法,你可使用lambda表達式指定此操做。假設你想要一個相似於printPerson的lambda表達式,它接受一個參數(Person類型的對象)並返回void,請記住,要使用lambda表達式,你須要實現一個功能性接口。在這種狀況下,你須要一個包含抽象方法的功能性接口,該方法能夠接受一個Person類型的參數並返回voidConsumer<T>接口包含void accept(T t)方法,它具備這些特性,如下方法將調用p.printPerson()替換爲調用方法acceptConsumer<Person>實例:

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

所以,如下方法調用與在方法3:在局部類中指定搜索條件代碼以獲取有資格得到選擇性服務的成員中調用printPersons時相同,用於打印成員的lambda表達式以下:

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

若是你想對成員的我的資料進行更多操做而不是打印出來,該怎麼辦?假設你要驗證成員的我的資料或檢索他們的聯繫信息?在這種狀況下,你須要一個包含返回值的抽象方法的功能性接口,Function<T,R>接口包含方法R apply(T t),如下方法檢索參數mapper指定的數據,而後對參數block指定的操做執行操做:

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方法:

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

此方法調用執行如下操做:

  1. 從集合source獲取對象源,在此示例中,它從集合roster中獲取Person對象的源,請注意,集合rosterList類型的集合,也是Iterable類型的對象。
  2. 過濾與Predicate對象tester匹配的對象,在此示例中,Predicate對象是一個lambda表達式,指定哪些成員有資格得到選擇性服務。
  3. 將每一個篩選對象映射到Function對象mapper指定的值,在此示例中,Function對象是一個lambda表達式,它返回成員的電子郵件地址。
  4. Consumer對象block指定的每一個映射對象執行操做,在此示例中,Consumer對象是一個lambda表達式,用於打印字符串,該字符串是Function對象返回的電子郵件地址。

你可使用聚合操做替換每一個操做。

方法9:使用接受Lambda表達式做爲參數的聚合操做

如下示例使用聚合操做來打印有資格得到選擇性服務的集合roster中包含的成員的電子郵件地址:

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對象匹配的對象 Stream<T> filter(Predicate<? super T> predicate)
將對象映射到Function對象指定的另外一個值 <R> Stream<R> map(Function<? super T,? extends R> mapper)
執行Consumer對象指定的操做 void forEach(Consumer<? super T> action)

操做filtermapforEach是聚合操做,聚合操做處理流中的元素,而不是直接來自集合(這是本例中調用的第一個方法是stream的緣由)。流是一系列元素,與集合不一樣,它不是存儲元素的數據結構,相反,流經過管道攜帶來自源(例如集合)的值,管道是一系列流操做,在此示例中爲filter-map-forEach,此外,聚合操做一般接受lambda表達式做爲參數,使你能夠自定義它們的行爲方式。

有關聚合操做的更全面討論,請參閱聚合操做課程。

GUI應用程序中的Lambda表達式

要處理圖形用戶界面(GUI)應用程序中的事件,例如鍵盤操做、鼠標操做和滾動操做,一般會建立事件處理程序,這一般涉及實現特定的接口,一般,事件處理程序接口是功能性接口;他們每每只有一種方法。

在JavaFX示例HelloWorld.java中(在上一節匿名類中討論過),你能夠在此語句中用lambda表達式替換匿名類:

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

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

方法調用btn.setOnAction指定在選擇由btn對象表示的按鈕時會發生什麼,此方法須要EventHandler<ActionEvent>類型的對象,EventHandler<ActionEvent>接口只包含一個方法,void handle(T event),此接口是一個功能性接口,所以你可使用如下顯示的lambda表達式來替換它:

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運行時將計算表達式,而後返回其值,或者,你可使用return語句:

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

    return語句不是表達式,在lambda表達式中,你必須將語句括在大括號({})中,可是,你沒必要在大括號中包含void方法調用,例如,如下是有效的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對兩個整數操做數執行數學運算,操做自己由IntegerMath實例指定,該示例使用lambda表達式,additionsubtraction定義了兩個操做,該示例打印如下內容:

40 + 2 = 42
20 - 10 = 10

訪問封閉範圍的局部變量

像局部和匿名類同樣,lambda表達式能夠捕獲變量,它們對封閉範圍的局部變量具備相同的訪問權限,可是,與局部和匿名類不一樣,lambda表達式沒有任何遮蔽問題(有關更多信息,請參閱遮蔽),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的聲明中用參數x代替y,則編譯器會生成錯誤:

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

編譯器生成錯誤「variable x is already defined in method methodInFirstLevel(int)」,由於lambda表達式不會引入新的做用域級別,所以,你能夠直接訪問封閉範圍的字段、方法和局部變量。例如,lambda表達式直接訪問methodInFirstLevel方法的參數x,要訪問封閉類中的變量,請使用關鍵字this,在此示例中,this.x引用成員變量FirstLevel.x

可是,與局部和匿名類同樣,lambda表達式只能訪問final或有效final的封閉塊的局部變量和參數,例如,假設你在methodInFirstLevel定義語句以後當即添加如下賦值語句:

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

因爲這個賦值語句,變量FirstLevel.x再也不是final,所以,Java編譯器生成相似於「local variables referenced from a lambda expression must be final or effectively final」的錯誤消息,其中lambda表達式myConsumer嘗試訪問FirstLevel.x變量:

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

目標類型

你如何肯定lambda表達式的類型?回想一下lambda表達式,它選擇了男性和年齡在18到25歲之間的成員:

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

這個lambda表達式用於如下兩種方法:

  • 方法3:在局部類中指定搜索條件代碼中的public static void printPersons(List<Person> roster, CheckPerson tester)
  • 方法6:將標準功能性接口與Lambda表達式一塊兒使用中的public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)

當Java運行時調用方法printPersons時,它指望CheckPerson的數據類型,所以lambda表達式屬於這種類型,可是,當Java運行時調用方法printPersonsWithPredicate時,它指望數據類型爲Predicate<Person>,所以lambda表達式屬於此類型。這些方法所指望的數據類型稱爲目標類型,要肯定lambda表達式的類型,Java編譯器使用發現lambda表達式的上下文或情境的目標類型,所以,你只能在Java編譯器能夠肯定目標類型的狀況下使用lambda表達式:

  • 變量聲明
  • 賦值
  • Return語句
  • 數組初始化
  • 方法或構造函數參數
  • Lambda表達體
  • 條件表達式,?:
  • 轉換表達式

目標類型和方法參數

對於方法參數,Java編譯器使用另外兩種語言功能肯定目標類型:重載決策和類型參數推斷。

考慮如下兩個功能性接口(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();
}

將在如下語句中調用哪一個方法?

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

將調用方法invoke(Callable<T>),由於該方法返回一個值,方法invoke(Runnable)沒有,在這種狀況下,lambda表達式() -> "done"的類型是Callable<T>

序列化

若是lambda表達式的目標類型及其捕獲的參數是可序列化的,則能夠序列化它,可是,與內部類同樣,強烈建議不要對lambda表達式進行序列化。

方法引用

你使用lambda表達式來建立匿名方法,可是,有時,lambda表達式只會調用現有方法,在這些狀況下,經過名稱引用現有方法一般更清楚,方法引用使你能夠這樣作;對於已經有名稱的方法,它們是緊湊的,易於閱讀的lambda表達式。

再次考慮Person類:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

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

    public int getAge() {
        // ...
    }
    
    public Calendar getBirthday() {
        return birthday;
    }    

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }}

假設你的社交網絡應用程序的成員包含在一個數組中,而且你但願按年齡對數組進行排序,你可使用如下代碼(在示例MethodReferencesTest中查找本節中描述的代碼摘錄):

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
        
Arrays.sort(rosterAsArray, new PersonAgeComparator());

此調用排序的方法簽名以下:

static <T> void sort(T[] a, Comparator<? super T> c)

請注意,Comparator接口是一個功能性接口,所以,你可使用lambda表達式而不是定義而後建立實現Comparator的類的新實例:

Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

可是,這種比較兩個Person實例的出生日期的方法已經存在爲Person.compareByAge,你能夠在lambda表達式的主體中調用此方法:

Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
);

由於此lambda表達式調用現有方法,因此可使用方法引用而不是lambda表達式:

Arrays.sort(rosterAsArray, Person::compareByAge);

方法引用Person::compareByAge在語義上與lambda表達式(a, b) -> Person.compareByAge(a, b)相同,每一個都有如下特色:

  • 它的形式參數列表是從Comparator<Person>.compare複製的,它是(Person, Person)
  • 它的主體調用方法Person.compareByAge

各類方法引用

有四種方法引用:

種類 示例
引用靜態方法 ContainingClass::staticMethodName
引用特定對象的實例方法 containingObject::instanceMethodName
引用特定類型的任意對象的實例方法 ContainingType::methodName
引用構造函數 ClassName::new

引用靜態方法

方法引用Person::compareByAge是對靜態方法的引用。

引用特定對象的實例方法

如下是對特定對象的實例方法的引用示例:

class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }
        
    public int compareByAge(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

方法引用myComparisonProvider::compareByName調用方法compareByName,它是對象myComparisonProvider的一部分,JRE推斷出方法類型參數,在本例中是 (Person, Person)

對特定類型的任意對象的實例方法的引用

如下是對特定類型的任意對象的實例方法的引用示例:

String[] stringArray = { "Barbara", "James", "Mary", "John",
    "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

方法引用String::compareToIgnoreCase的等效lambda表達式將具備形式參數列表(String a, String b),其中ab是用於更好地描述此示例的任意名稱,方法引用將調用方法a.compareToIgnoreCase(b)

引用構造函數

你可使用名稱new以與靜態方法相同的方式引用構造函數,如下方法將元素從一個集合複製到另外一個集合:

public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {
        
        DEST result = collectionFactory.get();
        for (T t : sourceCollection) {
            result.add(t);
        }
        return result;
}

功能性接口Supplier包含一個不帶參數且返回對象的方法get,所以,你可使用lambda表達式調用方法transferElements,以下所示:

Set<Person> rosterSetLambda =
    transferElements(roster, () -> { return new HashSet<>(); });

你可使用構造函數引用代替lambda表達式,以下所示:

Set<Person> rosterSet = transferElements(roster, HashSet::new);

Java編譯器推斷你要建立包含Person類型元素的HashSet集合,或者,你能夠按以下方式指定:

Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

什麼時候使用嵌套類、局部類、匿名類和Lambda表達式

如嵌套類一節所述,嵌套類使你可以對僅在一個地方使用的類進行邏輯分組,增長封裝的使用,並建立更易讀和可維護的代碼。局部類、匿名類和lambda表達式也賦予這些優勢,可是,它們旨在用於更具體的狀況:

  • 局部類:若是你須要建立一個類的多個實例,訪問其構造函數或引入新的命名類型(例如,你須要稍後調用其餘方法),請使用它。
  • 匿名類:若是須要聲明字段或其餘方法,請使用它。
  • lambda表達式:

    • 若是要封裝但願傳遞給其餘代碼的單個行爲單元,請使用它,例如,若是要在集合的每一個元素上執行某個操做,或者在流程完成時,或者在流程遇到錯誤時,你將使用lambda表達式。
    • 若是你須要功能性接口的簡單實例而且不該用前述條件(例如,你不須要構造函數、命名類型、字段或其餘方法),請使用它。
  • 嵌套類:若是你的要求與局部類的要求相似,則須要使用它,你但願更普遍地使用該類型,而且不須要訪問局部變量或方法參數。

    • 若是須要訪問封閉實例的非公共字段和方法,請使用非靜態嵌套類(或內部類),若是不須要此訪問權限,請使用靜態嵌套類。

上一篇:匿名類

下一篇:枚舉類型

相關文章
相關標籤/搜索