Java 8 Lambda表達式一看就會

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

使用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>的實例中。express

咱們先使用一個簡單的方法來實現這個用例,再經過使用本地類、匿名內部類實現,最終經過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:在本地類中設定特定的搜索條件

下面的方法能夠打印出符合搜索條件的全部用戶信息app

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實例。ide

經過實現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<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();
        }
    }
}
複製代碼

那麼,能夠這樣調用這個函數:

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<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);
            }
        }
}
複製代碼

那麼能夠這樣調用processPersons函數:

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

複製代碼

若是你想對用戶的信息進行更多處理而不止打印出來,那該怎麼辦呢?假設你想驗證成員的我的信息或者獲取他們的聯繫人的信息呢?在這種狀況下,你須要一個有返回值的抽象函數的函數式接口。Function<T,R>接口包含了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 stream()
過濾符合Predicate對象(lambda表達式)的實例 Stream filter(Predicate<? super T> predicate)
使用Function對象映射符合過濾標準的對象到一個值 Stream 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 roster, CheckPerson tester)
  • public void printPersonsWithPredicate(List roster, Predicate tester)

當java運行時調用方法printPersons時,它指望一個CheckPerson類型的數據,所以lambda表達式就是這種類型。當java運行時調用方法printPersonsWithPredicate時,它指望一個Predicate<Person>類型的數據,所以lambda表達式就是這樣一個類型。這些方法指望的數據類型就叫目標類型。爲了肯定lambda表達式的類型,java編譯器會在lambda表達式的的上下文中判斷它的目標類型。只有java編譯器可推測出來了目標類型,lambda表達式才能夠被執行。

目標類型和函數參數

對於函數參數,java編譯器能夠肯定目標類型經過兩種其餘語言特性:重載解析和類型參數推斷。看下面兩個函數式接口( java.lang.Runnable and java.util.concurrent.Callable):

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>

最後

感謝閱讀,有興趣能夠關注微信公衆帳號獲取最新推送文章。

微信二維碼
相關文章
相關標籤/搜索