前幾天發表了本文的圖片版,本文原來是在 word 編寫的,後來想要發佈到開源中國的博客,爲了圖方便,就直接截圖上傳到博客了。不少朋友看了以後留言,想要文字版的。本文篇幅很是長,把文字整理到博客真的很是費時。還好終於整理好了,獻給大家,但願對大家有幫助。java
本文源代碼上傳到了碼雲,請點擊 LambdaExpression 獲取。git
本文微信公衆號同步發佈,微信掃一掃文章末尾二維碼或搜索 鄭寶填 便可關注。程序員
目 錄
1. LAMBDA 表達式是什麼
2. LAMBDA 表達式用在何處
2.1. 方法一:建立方法,尋找符合條件的會員,但方法所指定的條件是硬編碼
2.2. 方法二:建立一個適應性更好的方法,去尋找符合條件的會員
2.3. 方法三:在獨立類中定義篩選會員的條件
2.4. 方法四:在匿名類中定義篩選會員的條件
2.5. 方法五:使用 LAMBDA 表達式規定篩選會員的條件
2.6. 方法六:在標準函數式接口環境中,使用 LAMBDA 表達式
2.7. 方法七:讓程序的全部功能都使用 LAMBDA 表達式
2.8. 方法八:使用泛型,進一步提升方法的適應性
2.9. 方法九:使用聚合操做,並用 LAMBDA 表達式做爲聚合操做的參數
3. 在 GUI 應用程序中使用 LAMBDA 表達式
4. LAMBDA 表達式語法
5. 變量訪問權限
6. 目標類型
6.1. 目標類型和方法參數
7. 序列化
8. 方法引用
8.1. 訪問靜態方法
8.2. 訪問特定對象的實例方法
8.3. 訪問特定類型的隨機對象的實例方法
8.4. 訪問構造函數正則表達式
Lambda 表達式是 Java 8 的新特性,是 Oracle 公司爲了加強 Java 基礎功能而引入的一種編程語法。請注意, Lambda 表達式是一種新的 Java 編程語法,你將會看到你之前所沒有看到的 Java 編程語法,相信可以讓你耳目一新。算法
首先,須要指出的是,對於第一次接觸 Lamdba 表達式的程序員來講,儘管 Lambda 表達式看起來很新鮮,但須要注意的是, Lamdba 表達式本質是一個函數式接口( functional interface )的實現類的實例。express
函數式接口:一個加上註解 @FunctionalInterface 的接口,例如接口 Comparator<T> 。這樣的接口只有一個抽象方法( 方法被 public abstract 修飾,或是默認沒有任何修飾)。註解 @FunctionalInterface 是一個信息型( informative annotation )的註解,標示接口是一個函數式的接口,區別於普通的接口。須要注意的是,函數式接口中的默認方法,由於他們已經有了默認實現,因此他們並不計入抽象方法。關於默認方法,之後會講,此處暫時不展開。另外,函數式接口所定義的抽象方法如果和頂級類 Object 定義的抽象方法同樣,該方法不計入函數式接口的抽象方法。好比,函數式接口定義了這樣一個方法,「 int hashCode(); 」,由於 Object 頂級類也定義這樣一個抽象方法,「 public native int hashCode(); 」,因此抽象方法 hashCode 不計入函數式接口的抽象方法。這時你在思考,爲什麼這樣的抽象方法不計入呢?緣由很簡單。 Java 語法規定,任何類都有一個默認的上級類,那就是頂級類 Object,並對頂級 Object 的抽象方法作了默認實現。在本例中,函數式接口的實現類也是同樣,它也默認繼承了頂級類 Object ,並頂級類 Object 的抽象方法 hashCode 作了默認實現,這至關於,對函數式接口中定義的 hashCode 也作了默認實現,函數式接口的實現類如果不想覆蓋抽象方法 hashCode ,保持默認實現,在 Java 語法上講,徹底沒有問題。正是由於這些抽象方法(函數式接口和頂級類 Object 都定義的抽象方法),在函數式接口的實現類中能夠不覆蓋,全部它們不計入函數式接口的抽象方法。只有全新定義的抽象方法能被計入函數式接口的抽象方法,並且只有惟一的一個。編程
須要指出的是,如果一個接口的定義符合函數式接口的定義,即便沒有加上註解 @FunctionalInterface ,編譯器一樣會認爲它是一個函數式接口。@FunctionalInterface 是一個信息型的註解,起到標示的做用,讓人一目瞭然。同時,如果一個接口加上此註解,可是,定義倒是不符合函數式接口的定義,編譯器便會報出錯誤。因此,咱們建議,如果你想定義一個函數式接口,最好仍是寫上註解 @FunctionalInterface ,雖然這不是必須的。數組
咱們說過, Lambda 表達式是函數式接口實現類的實例,那麼,編寫Lambda 表達式,實際上,就是在編寫函數式接口惟一的抽象方法的實現。瀏覽器
Lambda 表達式有以下特色:微信
1.調用方法時,它能夠做爲方法的參數。調用方法時,須要給方法參數傳值,這個值能夠是基礎類型的值,也能夠是一個類的實例。在 Java 8 中,你能夠把一個 Lambda 表達式傳遞給方法,做爲方法的參數。 Lambda 表達式是函數式接口實現類的實例,因此, lambda 表達式做爲方法的參數,實際就是把類實例做爲方法的參數。編寫 Lambda 表達式,實際是編寫函數式接口惟一的抽象方法的實現。所以,它是具有某種行爲,或者說是具有某種功能的代碼單元,這樣的功能代碼,能夠傳遞給方法的參數。
2.方法引用( Method References )更加簡潔和可讀性更好,它由lambda 表達式演變而來。關於方法引用,咱們接下來會詳細的講解,你會見識它這一振奮人心的特性。
3.默認方法的功能容許你把新的默認方法添加到老舊的接口中,但依舊可以保持兼容性。通常來講,咱們定義了接口,接着就會給這個接口添加一個或一個以上的實現類。在後期的程序版本升級中,咱們須要修改早期定義的接口,爲之添加新的方法。如果這些方法是默認的,也便是 public abstract,那麼,該接口的全部實現類都必須作相應的修改,爲這些新添加的抽象方法添加實現。如果不去修改這些實現類,那麼,編譯報錯,出現了代碼兼容性的問題。因而,咱們就開始思考,可否作到,爲早期的接口添加方法的同時,不用去修改它的實現類,代碼依舊能夠不報錯,保持兼容性呢?如果把這些在早期定義的接口中新添加的方法定義爲默認方法,即有關鍵字 default,這樣能夠保證兼容性。默認方法,便是在接口定義它時,已經爲之作了默認實現的方法。既然如此,接口的實現類能夠重寫它,也能夠不重寫它,保持它默認的實現。再次提醒一下,默認方法的定義,須要加上關鍵字 default。關於默認方法,之後會詳細講解,此處暫時不展開。
4.靜態方法的功能容許你把新的靜態方法添加到老舊的接口中,但依舊可以保持兼容性。靜態方法便是靜態的默認方法。在接口定義方法中,加上關鍵字 static。既然靜態方法也是默認方法,爲早期的接口添加靜態方法的同時,不用去修改它的實現類,代碼依舊能夠不報錯,保持兼容性。關於靜態方法,之後會詳細講解,此處暫時不展開。
5. Java 8 新添加了一些類和加強了一些類(修改原有的類,使之功能更增強大),很好的利用了 Lambda 表達式和 Stream 。關於 Stream,接下類咱們會詳細的講解。
許多的方法,它的參數是接口類型的,當咱們的程序調用這個方法時,須要爲之傳遞一個實現了這個接口的實現類的實例。此處,咱們假設有一個方法 F,它的參數是接口類型 I,爲了調用這個方法 F,一種比較笨拙的方式是,定義一個類,假設爲 B,類 B 實現接口 I,建立類 B 的實例,拿着類 B 的實例做爲方法 F 的參數。顯然,人們意識到了這種調用方法的笨拙,因而,就出現了匿名的實現類。咱們知道,匿名的實現類,不須要獨立建立接口的實現類,在給方法傳遞參數時便可直接實例化一個匿名類的實例,同時,不須要指定接口實現類的類名(即稱之爲匿名)。這樣的方式,顯得更加的簡潔和方便。
可是,有了匿名實現類,咱們依舊面臨一個問題。如果接口只有一個抽象方法,爲了實現這個抽象方法,咱們還要爲之建立匿名實現類,這樣仍是顯得很笨拙和不清晰。在這樣的情景中,使用 Lambda 表達式,你將會看到更加簡潔和可讀性更好的代碼。如同前面所講,調用方法時, Lambda 能夠做爲方法的參數。Lambda 表達式是函數式接口實現類的實例,因此,Lambda 表達式做爲方法的參數,實際就是把一個類實例做爲方法的參數。Lambda 表達式表達或是設計了一組功能,把它傳遞給方法做爲參數,實際上,能夠理解爲把一組功能傳遞給了方法。所以,爲了讓代碼更加簡潔,編程更加高效,調用方法(該方法的參數類型是接口類型)時,若接口有多個抽象方法,咱們能夠建立這個接口的匿名實現類的實例,做爲方法的參數。如果接口只有惟一一個抽象方法,好比函數式接口,咱們能夠建立這個接口的 Lambda 表達式,做爲調用方法的參數,把一組功能傳遞給方法。好比說,在一個 GUI ( Graphical User Interface )程序中,咱們點擊按鈕,就會調用響應函數,咱們能夠把 Lambda 表達式做爲響應函數的參數,傳遞給響應函數。這個 Lambda 表達式規定了響應邏輯,好比彈出一個提示窗口給 GUI 使用用戶。
咱們假設有這樣一種情景,咱們要建立一個社交網絡應用( social networking application ),管理員應該可以管理全部的應用會員( members of the social networking application ),當會員符合必定的條件,管理員就會對他們執行一些操做,好比給他們發送消息。下面,咱們詳細地描述這個場景。
業務:對符合條件,被選中的會員執行一些操做;
執行者:管理員;
前置條件:管理員已經登陸社交網絡應用;
後置條件:只對符合條件,被選中的會員執行一些操做,而不是針對全部會員。
詳細業務:1. 管理員指定條件;2. 管理員指定操做;3.管理員點擊提交按鈕;4. 系統後臺找出符合條件的會員;5.系統後臺對符合條件的會員執行操做。
擴展:管理員在點擊提交按鈕以前,或者是在管理員指定的操做發生以前,管理員可以預覽符合指定條件的會員。
業務發生的頻率:一天能發生屢次。
假設社交網絡應用的會員實體類以下所示:
package cn.lambda.test; import java.time.LocalDate; public class Person { // 性別枚舉 public enum Sex { MALE, FEMALE } private String name; // 姓名 private LocalDate birthday; // 生日 private Sex gender; // 性別 private String emailAddress; // 郵件地址 private int age; // 年齡 public void printPerson() { // 打印會員的我的信息 } // getter setter methods }
假設全部應用會員存儲在集合 List<Person> roster 對象中。
下面咱們使用 9 種方法實現上述的場景,設計的難度由淺到深,適應性由窄到廣。一開始,咱們使用單純( naive )的方法,接着呢,咱們使用獨立類和匿名類改善這個方法,最終,咱們使用 Lambda 表達式,讓方法變得高效而且簡潔。
由於這個方法只能匹配一種條件,即年齡大於指定的數字,如果須要匹配其餘的條件,好比性別是男的,一種最簡單的方式就是,再次建立一個方法,讓他匹配另外一種條件,即性別。
public static void printPersonsOlderThan(List<Person> roster, int age) { for (Person p : roster) { if (p.getAge() >= age) { p.printPerson(); } } }
這個方法的適應性很窄,如果你的程序進行升級,這個方法頗有可能就不能用了。假設你修改了會員實體類的數據結構,把年齡 age 的數據類型修改成字符 String 類型;假設你修改了計算年齡的算法,年齡小於某個指定的數字。這樣的一些修改,這個方法不但不能實現業務,並且有可能編譯錯誤。另外,就算後期不去升級程序,爲了適應匹配其餘的條件,好比指定性別,指定郵件地址等,咱們須要建立許多相似的方法去知足業務的須要。
以下這個方法比起上一個例子的方法 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(); } } }
如果把會員的年齡做爲篩選條件,這個方法 printPersonsWithinAgeRange 適應性比起上一個例子要好。但問題是,如果要把會員的性別做爲篩選條件呢?或是要把會員的年齡範圍和指定性別做爲聯合篩選條件呢?如果你決定改變會員實體類的數據結構,好比增長會員的關係狀態,地理位置,接着要把他們做爲篩選條件呢?很顯然,咱們須要建立不少方法知足各類條件的篩選,這些方法是分離的( separate method ),獨立的,所以代碼是很易碎的( brittle code )。一種替換這些易碎代碼的方案是,把篩選的條件定義在一個獨立的類中。
public static void printPersons(List<Person> roster, CheckPerson tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
在這個方法中,全部的應用會員放在 List 集合 roster 中,遍歷每個會員,判斷他是否符合條件。方法的第二個 CheckPerson 接口類型的參數 tester,它就是篩選會員的條件。判斷的方式是調用 tester.test 方法,如果該方法返回 true, 代表當前會員符合條件,調用當前會員的 printPerson 方法,打印會員的信息。
方法的第二個參數是 CheckPerson 接口類型的,咱們須要定義接口 CheckPerson。
package cn.lambda.test; public interface CheckPerson { boolean test(Person p); }
下面咱們須要爲這個接口 CheckPerson 定義實現類,實現抽象方法 test,實現的邏輯,即篩選條件,是符合美國義務兵役制度的會員,假設符合這一制度的具體條件是男性且年齡在 18 至 25 歲之間。
package cn.lambda.test; class CheckPersonEligibleForSelectiveService implements CheckPerson { public boolean test(Person p) { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } }
爲了可以完成這個場景,最後咱們須要調用方法 printPersons。
List<Person> roster = ... printPersons(roster, new CheckPersonEligibleForSelectiveService());
這個方法 printPersons 看起來不是那麼易碎了,就算咱們後期升級程序,須要修改了會員實體類 Person 的數據結構,或是修改篩選條件,咱們依舊不須要改變這個方法printPersons ,由於這個方法再也不出現判斷條件,更沒有出現會員實體類 Person 的任何字段。具體的判斷條件被定義在一個獨立的接口實現類中。可是,咱們依舊面臨一個問題,爲了可以篩選出符合條件的應用會員,咱們須要去維護一個接口 CheckPerson 和一個實現類 CheckPersonEligibleForSelectiveService ,這依舊是件麻煩事。因而咱們想到了改進的方案,使用匿名類( anonymous class )代替獨立定義的接口實現類。好比這個例子中,實現類 CheckPersonEligibleForSelectiveService 就是獨立定義的,下一個例子咱們要使用匿名類替換它。
printPersons 的第二個參數是 CheckPerson 接口類型的,在本例子中,調用方法 printPersons 時,此參數是一個匿名類的實例。在這個匿名類中,規定了篩選應用會員的條件,即符合美國義務兵役制,制度的具體內容就是應用會員是男性且年齡介於 18 歲至 25 歲之間。
List<Person> roster = ... printPersons(roster, new CheckPerson() { public boolean test(Person p) { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } });
咱們發現,這個方法的調用,有效地減小了代碼量,由於咱們再也不須要在獨立的實現類中定義篩選會員的條件。可是,這並不完美,接口 CheckPerson 的定義很是簡單,只有惟一一個抽象方法 test,咱們的定義了該接口的匿名實現類,實現了抽象方法 test,並建立了匿名類的實例,有關匿名類的代碼看起來太多了,由於它僅僅只需實現一個抽象方法 test。下一個例子,咱們正式使用 Lambda 表達式代替匿名類,你會看到更加簡潔和可讀性更好的代碼。
接口 CheckPerson 是一個函數式接口,由於該接口只有惟一一個抽象方法 test。前面強調過,一個接口的定義一旦符合函數式接口的定義,那麼編譯器就會認爲它是一個函數式接口,無論該接口是否加上註解 @FunctionalInterface 。顯然,定義接口 CheckPerson 時,咱們並無加上註解 @FunctionalInterface 。 Lambda 表達式的本質是函數式接口的實現類實例,所以編寫 Lambda 表達式本質就是在編寫函數式接口惟一一個抽象方法的實現邏輯。既然抽象方法只有惟一一個,Lambda 表達式能夠省略抽象方法的名字。下面咱們調用方法 printPersons ,使用 Lambda 表達式代替匿名類,請注意 printPersons 的第二個參數。
List<Person> roster = ... printPersons(roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25);
因爲 Lambda 表達式從匿名類演化而來,兩者從本質上也是相同。那咱們一塊兒分析,相比較於匿名類,Lambda 表達式有哪些新的表現形式。在本例中,Lambda 表達式省略了「 new 接口 一對小括號 一對大括號」和「抽象方法聲明的前部分」的代碼,即省略了「new CheckPerson(){...} 」和「public boolean test 」,只剩下抽象方法聲明的後部分,小括號以及小括號裏面的方法參數類型和參數名字,即「(Person p) 」。緊接着小括號,有一個指向右邊的箭頭,箭頭左邊是抽象方法聲明的後部分,小括號以及小括號裏面的方法參數類型和參數名字,箭頭右邊是抽象方法的實現邏輯。實現邏輯部分,省略了一對大括號和 return 關鍵字,只剩下一條表達式語句。相信你能夠感覺到, Lambda 表達式是多麼的簡潔,而且看起來很清晰,由於它省略了許多沒必要要的代碼。關於 Lambda 表達式的語法,接下來咱們還會全面的講解,此處咱不展開,須要指出的是,本例中的 Lambda 表達式還能夠進一步簡化。箭頭左邊,小括號能夠省略,參數的類型能夠省略,只剩下參數的名字,即 p 。
List<Person> roster = ... printPersons(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25); }
若你第一次看到 Lambda 表達式,相信會被它的簡潔性和可讀性所震撼。即使如此,Java 設計者們依舊以爲還可讓程序代碼更少,讓設計更加的細膩和極致。更進一步的設計就涉及標準的函數式接口,咱們自定義了函數式接口 CheckPerson ,與之對應就是 JDK 提供的標準函數式接口,下一個例子咱們就來聊聊標準函數式接口。
咱們再次仔細觀察以前自定義的函數式接口 CheckPerson 。
package cn.lambda.test; public interface CheckPerson { boolean test(Person p); }
這個接口極其簡單,有惟一一個抽象方法(全部函數式接口都有這個特徵),抽象方法 test 有一個參數和一個 boolean 類型的返回值。這個抽象方法是如此的簡單,咱們徹底沒有必要本身寫代碼把它定義在咱們的程序裏,這種事情能夠交給 JDK 去作。所以,JDK 定義了幾種標準的函數式接口,其中一個標準的函數式接口的抽象方法就是接收一個參數和一個 boolean 類型的返回值,咱們的程序能夠直接利用這個標準的函數式接口。 JDK 定義的幾種函數式接口,放在包 java.util.function 中。
咱們可使用標準函數式接口 Predicate<T> 替代自定義函數式接口 CheckPerson 。咱們看以看到, Predicate<T> 有惟一一個抽象方法 boolean test(T t) ,幾個默認方法( 關鍵字 default ),一個靜態方法( 關鍵字 static ),因爲默認方法和靜態方法都有默認的實現邏輯,所以它們都不算是抽象方法。
標準函數式接口 Predicate<T> ,它表明着一個斷言, Predicate 的中文意思就是斷言。有些人可能對斷言這個詞有些陌生,通俗地講,斷言就是對一個對象或是一個基本數據做出判斷,要麼判斷爲 true ,要麼判斷爲 false ,可見,斷言的結果是 boolean 類型的。既然涉及到斷言(判斷),就須要斷言標準和等待斷言的對象或是等待斷言的基本數據。泛型接口 Predicate<T> 的尖括號有一個類型參數 T,它是該接口抽象方法 test 的參數類型。抽象方法的類型爲 T 的參數就是等待斷言的對象,那斷言的標準是什麼呢?咱們先來看看 Predicate<T> 惟一的抽象方法 test 的定義:
boolean test(T t);
該抽象方法的功能是根據斷言標準,對等待斷言的對象,也就是參數 T t ,評估出結果。如果等待斷言的對象 T t 符合斷言標準,該抽象方法返回 true ,不然返回 false 。標準函數式接口的實現方式可使用匿名類,也可使用 Lambda 表達式,但不管使用哪種,都必須對惟一的抽象方法 test 做出實現,實現的邏輯就是斷言的標準。好比,實現的邏輯是篩選出符合美國義務兵役制度,具體條件是男性且年齡在 18 至 25 歲之間的應用會員。其中,「符合美國義務兵役制度,具體條件是男性且年齡在18至25歲之間」是斷言標準,「應用會員」就是等待斷言的對象。
在此處,咱們有必要簡單回憶一下泛型的知識。 Predicate<T> 是一個泛型接口,和泛型類同樣,泛型接口能夠在尖括號「 <> 」中,指定一個或多個類型參數。對於接口 Predicate<T> 而言,它只有一個類型參數,那就是 T 。 T 稱之爲泛型參數,泛型類,泛型接口稱之爲泛型類型( generic type )。當咱們建立或聲明泛型類實例,或是建立泛型接口的實現類的實例,或是聲明泛型接口類型的實例時,須要給泛型參數指定一個實際類型的參數,如此一來,泛型類,或是泛型接口就具備了實際類型的參數,此時,他們稱之爲參數化類型( parameterized type )。請注意泛型類型和參數化類型的區別,前者的參數類型是泛型,後者的參數類型是實際類型。下面代碼中的 Predicate<Person> 就是一個參數化類型,由於它的參數是實際類型 Person 。
interface Predicate<Person> { boolean test(Person t); }
這個參數化類型 Predicate<Person> 的抽象方法有一個 boolean 類型的返回值和一個參數,參數類型是 Person ,這和自定義的函數式接口 CheckPerson 的抽象方法 test 如出一轍。所以,咱們沒必要再去自定義 CheckPerson ,而是使用標準函數式接口 Predicate<T> 代替它。
public static void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
接下來,我須要調用這個方法 printPersonsWithPredicate ,篩選出符合條件的應用會員,條件是符合美國義務兵役制度的會員,假設符合這一制度的具體條件是男性且年齡在 18 至 25 歲之間。
List<Person> roster = ... printPersonsWithPredicate(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25);
在本例中,方法 printPersonsWithPredicate 使得咱們的代碼更加簡潔了,省略了自定義函數式接口 CheckPerson 。可是,這個方法的適應性依舊較窄。現有代碼的邏輯是,遍歷應用的每個會員,如果會員符合指定的條件,就把會員的信息打印出來。請注意,對符合條件的會員所執行的操做就是打印出他的信息,這就把操做寫死了。若要想要改變操做了,不想打印會員信息了,這個方法就不適用了。在本例中,方法 printPersonsWithPredicate 的參數使用了一個標準函數式接口,用來接收 Lambda 表達式,該 Lambda 表達式是一個判斷標準。標準的函數式接口有好幾個,下一個例子,咱們使用其餘的標準函數式接口,再也不對符合條件的會員所執行的操做寫死,改造後,方法的適應性變得更好。
除了對符合條件的會員執行打印信息的操做「 printPerson() 」外,咱們還可使用一個 Lambda 表達式,傳遞給方法,在這個方法中,對這些符合條件的會員執行更多的操做。也就是說,咱們可使用 Lambda 表達式代替 printPerson() ,使得操做適用更多的狀況,而不僅僅是打印信息。既然操做使用 Lambda 表達式,那麼方法的定義中須要一個函數式接口類型的參數,用來接收表達操做的 Lambda 表達式( Lambda 表達式的本質是函數式接口的實現類的實例)。咱們思考操做這個動做,它須要一個參數,在本例中,該參數表明一個會員,一個 Person 類型的參數。有了這個 Person 類型的參數,操做才能對會員開展。另外,操做不須要返回值( void )。方法的定義中須要一個函數式接口類型的參數,首先,一般咱們須要去定義函數式接口。可是,經過咱們剛剛的分析,該函數式接口的抽象方法須要一個參數而且沒有返回值,正好標準函數式接口有這樣一個接口,它就是 Consumer<T> 。所以,咱們沒有必要去自定義函數式接口。
標準函數式接口 Consumer<T> 表明執行一個操做,它的抽象方法接收單個參數且沒有結果(返回值爲 void )。尖括號「 <> 」有一個類型參數,表明該接口的抽象方法的參數類型。它有一個抽象方法和一個默認方法,抽象方法定義以下:
void accept(T t);
該抽象方法對指定的參數 T t 執行一個操做。
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,和上一個例子的方法 printPersonsWithPredicate 相比,你會發現它多了一個標準函數式接口類型的參數 Consumer<Person> block ,這個參數是用來接收表達操做的 Lambda 表達式的,操做便是對符合條件的應用會員執行操做。在 printPersonsWithPredicate 方法中,只能對符合條件的會員執行打印信息的操做,代碼是「 p.printPerson(); 」,而在本例的方法 processPersons 中,代碼被替換成了「 block.accept(p); 」,表示方法 processPersons 再也不把符合條件的會員所執行操做限制死,具體的操做由調用方法 processPersons 的調用者決定時。調用者使用 Lambda 表達式決定操做的具體內容,把 Lambda 表達式做爲方法 processPersons 的第三個參數,傳遞給方法 processPersons 。
最終,爲了是實現咱們的業務場景,咱們須要調用方法 processPersons :
List<Person> roster = ... processPersons(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.printPerson());
關注 processPersons 的第三個參數,它表明對符合條件的應用會員執行操做的 Lambda 表達式。該 Lambda 表達式只有一個參數,即 p ,它對標準函數式接口 Consumer<T> 的抽象方法 accept 的實現邏輯是「 p -> p.printPerson()); 」,即打印出會員的信息,固然,它也能夠是其餘的實現邏輯,其餘的操做。也許,如今你對 processPersons 方法的第三個參數的內涵依舊不是很能理解。下面,咱們嘗試使用匿名實現類的方式來調用 processPerson 方法,相信你會有更深的認識。
List<Person> roster = ... processPersons(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, new Consumer<Person>() { @Override public void accept(Person t) { t.printPerson(); } });
咱們再來看看 processPersons 方法的定義:
public static void processPersons(List<Person> roster, Predicate<Person> tester, Consumer<Person> block) { for (Person p : roster) { if (tester.test(p)) { block.accept(p); } } }
它的第三個參數是 Consumer<Person> block ,當調用 processPersons 時,咱們須要爲之傳遞一個實現了標準函數式接口 Consumer<Person> 的實現類的實例。這個實例能夠是匿名類的實例,也能夠是 Lambda 表達式。此處,這個實例是匿名實現類的實例。由於接口 Consumer<Person> 有惟一一個抽象方法 accept ,因此,匿名實現類須要對這個抽象方法進行實現,實現的邏輯是「 t.printPerson(); 」,即打印應用會員信息。在對方法 processPersons 定義時,關於第三個參數,即表明對符合條件的應用會員執行操做,有這樣的代碼,「 block.accept(p); 」,最終調用這個方法 processPersons 時,真正執行的就是 Consumer<Person> 實現類的 accept 方法。在此例中,真正執行的就是匿名類的 accept 方法或 Lambda 表達式的 accept 方法(固然, Lambad 表達式已經看不到 accept 方法名了),即打印應用會員信息,「 t.printPerson(); 」。
在這個例子的方法 processPersons 中,對符合條件的應用會員執行的操做能夠適應多種狀況,顯然它的適應性變得更好了。到此爲止,相信你心中還是有個疑問,對符合條件的應用會員所執行的操做,目前是直接針對會員這個實體類的對象操做的,即直接對 Person 類型的對象進行操做,好比 t.printPerson() ,直接使用 Person 對象調用 printPerson() 方法。如果想要對會員的某些屬性直接進行操做,那該怎麼辦呢?好比說,我想要直接操做會員的郵件地址,把會員的郵件地址打印出來。請你注意個人用詞「直接」。換言之,標準函數式接口 Consumer<T> 的抽象方法的類型不是 Person ,而是 String 類型(會員郵件地址是 String 類型)。顯然,在這樣的場景下,首先,咱們須要對符合條件的應用會員執行一個提取(轉化)操做,把會員的郵件地址提取出來,接着把郵件地址做爲標準函數式接口 Consumer<T> 抽象方法的參數,這樣,方能直接針對會員的郵件地址進行操做。這兒,涉及到一個提取(轉化)操做,這個提取方法須要一個參數,即 Person 類型的參數,它是應用會員,它是提取的原材料。還須要一個返回值,即 String 類型的的返回值,它是郵件地址,他是提取的目標。這個提取操做一樣可使用 Lambda 表達式來表達,既然涉及到新的 Lambda 表達式, processPersons 須要一個新的函數式接口類型的參數,用來接收這個表達提取的 Lambda 表達式。根據剛剛的分析,這個函數式接口的抽象方法須要一個參數,一個返回值,剛好有一個標準的函數式接口能夠知足要求,這樣一來咱們沒有必須自定義函數式接口,直接使用這個標準函數式接口,它就是 Function<T, R> 。
Function<T, R> 是一個標準的函數式接口,它接收一個參數,產出一個結果(一個返回值)。它是一個泛型接口,尖括號有兩個類型的參數,第一個類型參數 T 表示抽象方法的參數類型,第二個類型參數 R 表示抽象方法的返回值類型。它有兩個默認方法,一個靜態方法,一個抽象方法,抽象方法以下所示:
R apply(T t);
該抽象方法表明着對參數 T t 執行一個功能( function ),好比執行一個提取或是轉化功能,獲得類型爲 R 的結果。 T t 是執行功能的原材料,R 是執行功能的目標結果的類型。
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); } } }
咱們關注如下這兩行代碼:
String data = mapper.apply(p); block.accept(data);
方法 apply 的參數 p ,是符合條件的應用會員。方法 apply 對會員 p 執行一個功能(轉化或提取),獲得 String 類型的 data( data 表明什麼意思?具體的意思由調用者決定。在本例中, data 表明會員的郵件地址)。接着,accept 方法對會員的郵件地址執行一個操做。如今你能夠看到,程序再也不直接針對應用會員執行操做,而是直接針對會員的郵件地址進行操做。相信你也注意到,方法 processPersonsWithFunction 的第四個參數 Consumer<String> block ,標準函數式接口 Consumer 的類型參數再也不是 Person 類型的,已被修改成 String 類型了。
最終,爲了是實現咱們的業務場景,咱們須要調用方法 processPersonsWithFunction :
List<Person> roster = ... processPersonsWithFunction(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email));
傳遞給 processPersonsWithFunction 的第三個參數的是 Lambda 表達式,該 Lambda表達式表明提取符合條件的應用會員的郵件地址(把符合條件的應用會員轉化爲會員的郵件地址)。第四個參數仍然是一個 Lambda 表達式,該表達式表明對郵件地址執行一個操做,即打印郵件地址。做爲初學者,也許如今你對processPersonsWithFunction 方法的第3、第四個參數的內涵依舊不是很能理解。下面,咱們嘗試使用匿名實現類的方式來調用processPersonsWithFunction 方法,相信你會有更深的認識。
List<Person> roster = ... processPersonsWithFunction(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, new Function<Person, String>() { @Override public String apply(Person t) { return t.getEmailAddress(); } }, new Consumer<String>() { @Override public void accept(String t) { System.out.println(t); } });
咱們仔細觀察方法 processPersonsWithFunction 的定義,該方法的全部參數聲明都是實際類型。
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); } } }
然而,在這個方法體的邏輯中,和參數的實際類型聯繫並都不密切。好比,第一個參數是元素類型是 Person 類型的應用會員集合 roster 。方法體的邏輯中對這個集合進行遍歷時,單個 Person p 並無訪問 Person 類的任何屬性和方法。另外,對於 List<X> 集合 roster ,方法體邏輯一樣沒有訪問 List<X> 類的任何屬性和方法。第二個參數是等待斷言的對象類型是 Person 類型的斷言 tester ,方法邏輯中關於 tester 有這樣的代碼,「 tester.test(p) 」,依舊沒有訪問 Person 類的任何屬性和方法。第三個參數和第四個參數也是一樣的狀況。既然如此,在方法的定義中,參數聲明類型應該使用泛型,讓它的適應性更進一步。另外,咱們知道集合 List<X> 是集合你們庭的一員,集合都是直接或間接實現了接口 Iterable<X> ,因此,咱們可使用 Iterable<X> 類替代 List<X> 。
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 :
List<Person> roster = ... processElements(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email) );
對方法 processElements 的調用和對方法 processPersonsWithFunction 的調用沒有不一樣。只是,方法 processElements 的適應性獲得更進一步的提高。
值得關注的是, processElements 這個方法的執行流程,經過分析執行流程,咱們能夠爲此進入另外一個話題。
1.從集合數據源中獲取一個資源對象( source object )。在本例中,遍歷集合 List<Person> roster,獲取一個一個的應用會員 Person 。須要注意的是,roster 的類型是 List<X> ,一樣也是類型 Iterable<X> 。
2.使用斷言,篩選資源對象。經篩選後,資源對象成爲了已篩選對象( filtered object )。在本例中,篩選符合美國義務兵役制度的應用會員,制度的具體條件是男性且年齡在 18 至 25 歲之間。斷言參數是標準函數式接口類型, Predicate<X> tester ,咱們使用 Lambda 表達式爲該參數傳值。
3.對已篩選對象執行轉化操做。經轉化後,已篩選對象成爲了已映射對象( mapped object )。在本例中,把篩選出來的應用會員轉化爲會員郵件地址(從篩選出來的應用會員中提取他們的郵件地址)。轉化操做參數是標準函數式接口類型, Function<X, Y> mapper ,咱們使用 Lambda 表達式爲該參數傳值。
4.對已映射對象執行操做。在本例中,該操做就是打印會員的郵件地址。操做參數是標準函數式接口類型, Consumer<Y> block ,咱們使用 Lambda 表達式爲該參數傳值。
咱們能夠清楚的看到,以上執行流程是一環接一環。集合數據源->資源對象->已篩選對象->已映射對象->操做。爲了執行這個流程,咱們的方法 processElements 一共聲明瞭 4 個參數,其中一個是集合類型,三個是標準函數式接口類型。
1.針對第一個集合類型的參數 Iterable<X> source ,咱們對它進行遍歷,獲取資源對象。因而咱們思考,可否讓 JDK 自動遍歷?
2.針對第二個標準函數式接口類型的參數 Predicate<X> tester ,咱們調用它的 test 方法對資源對象進行斷言,獲取已篩選對象。因而咱們思考,可否讓 JDK自動進行斷言?
3. 針對第三個標準函數式接口類型的參數 Function<X, Y> mapper ,咱們調用它的 apply 方法對已篩選對象進行轉化,得到已映射對象。因而咱們思考,可否讓 JDK 自動進行轉化?
4. 針對第四個標準函數式接口類型的參數 Consumer<Y> block ,咱們調用它的 accept 方法對已映射對象執行一個操做,因而咱們思考,可否讓 JDK自動執行一個操做?
答案是確定的,這須要用到聚合操做( aggregate operation )。關於聚合操做,之後會詳細的講解。下面的例子,咱們先給出一個聚合操做完成咱們的場景,並對聚合操做做簡單解釋,目的是讓你們感覺一個聚合操做的簡潔、高效。
下面我將用一句話來表達咱們的業務場景,由於聚合操做從頭至尾就是一條語句,這是聚合操做的典型特性,就像流水線通常,從頭流到尾,不須要中斷。這句話是,在應用會員列表中篩選出符合美國義務兵役制度的會員並打印出會員的郵件地址。
public static <X, Y> void processWithAggregate( Collection<X> source, Predicate<X> tester, Function<X, Y> mapper, Consumer<Y> block) { source.stream().filter(tester).map(mapper).forEach(block); }
下面簡單介紹上述聚合操做中涉及到的四個 API。
default Stream<X> stream() : 使用方法 processWithAggregate的第一個參數 Collection<X> source 做爲數據源,返回一個有序流( sequential Stream ),這個有序流的類型是 Stream<X> ,核心做用是可以自動遍歷 Collection<X> source 中元素,並對元素(資源對象)執行各類操做。既然如此,擁有了有序流,咱們再也不須要手工遍歷 Collection<X> source了。須要注意的是,該方法 stream 的返回值是一個泛型接口類型 Stream<X> ,尖括號「 <> 」的類型參數 X 和 Collection<X> 中的 X 保持一致。也就是說,集合中的元素類型和有序流中的元素類型是一致的。這樣描述以後,你可能會誤會元素存放在 Stream<X> 類型的有序流中,請你務必注意, Stream<X> 類型的有序流不會存聽任何元素,它的功能是自動遍歷和操做元素,元素是存放在集合中的。
Stream<X> filter(Predicate<? super X> predicate) :對有序流的元素(資源對象)使用斷言進行篩選,返回一個新的有序流,新有序流中的元素是通過篩選的元素(已篩選對象)。 Filter 方法有一個參數 predicate 表明斷言,關於斷言前面已作詳述。方法 filter 的參數是標準函數式泛型接口類型 Predicate<? super X> predicate ,尖括號「 <> 」的類型參數是 X 或 X 的父類型。該方法的返回值是一個泛型接口 Stream<X> ,尖括號「 <> 」的類型參數是 X 。之全部會有這樣含義的類型參數,由於調用方法 filter (調用者)的是一個 Stream<X> 類型的實例,該實例由方法 stream 返回。
<Y> Stream<Y> map(Function<? super X, ? extends Y> mapper) :對已篩選元素(已篩選對象)執行轉化操做,返回一個新的有序流,新有序流中的元素是通過轉化的元素(已映射對象)。方法 map 有個參數 mapper 表明轉化,關於轉化前面已作詳述。方法 map 的參數是標準函數式泛型接口類型 Function<? super X, ? extends Y> , 尖括號「 <> 」有兩個類型參數,第一個類型參數是 X 或是 X 的父類型,它是等待轉化的對象的類型,即源對象類型,第二個類型參數 Y 或 Y 的子類型,它是轉化後的對象類型,即目標對象類型。 Y 能夠是任意類型,在 map 方法定義的開始部分已作了聲明。方法 map 的返回值是個泛型接口 Stream<Y> ,尖括號「 <> 」的類型參數是 Y ,這和目標對象的類型保持一致,該方法的返回值就是包含目標對象的有序流。之因此會有這樣含義的類型參數,由於調用方法 map (調用者)的是一個 Stream<X> 類型的實例,該實例由方法 filter 返回。
void forEach(Consumer<? super Y> action) :對已轉化的元素(已映射對象)執行一個操做,該方法沒有返回值,再也不有新的有序流產生,流結束了,方法 foreach 是個終止操做。方法有個參數 action 表明操做,關於操做前面已作詳述。方法 forEach 的參數是標準函數式泛型接口類型 Consumer<? super Y> action ,尖括號「 <> 」的類型參數是 Y 或是 Y 的父類型,之全部會有這樣含義的類型參數,由於調用方法 forEach (調用者)的是一個 Stream<Y> 類型的實例,該實例由方法 map 返回。
相信你已經發現,和方法 processElements 相比,方法 processWithAggregate 的第一個參數類型由 Iterable<X> 修改爲了Collection<X> 。 Collection<X> 也是集合你們庭中較爲頂級的接口,它繼承了頂級接口 Iterable<X> 。 Collection<X> 接口有默認方法 stream ,功能是把集合做爲數據源,返回一個有序流。可是,頂級接口 Iterable<X> 沒有這樣的方法,故須要把 Iterable<X> 替換成 Collection<X> 。
API filter、map、forEach 稱之爲聚合操做,聚合操做對集合的元素進行處理時,不是直接針對集合進行的,而是針對有序流,這就是爲何咱們須要調用集合的 stream 方法,去得到一個有序流。有序流能夠理解爲元素的序列( sequence of elements ),它的主要功能是用來自動遍歷和處理元素。和集合的重大區別是,有序流並不存儲元素,元素是存儲在集合中的。既然有序流不存儲數據,那麼,它的數據來自哪裏?經過管道在源頭獲取數據。何爲源頭?集合就是源頭之一。到目前爲止,咱們所接觸到的有序流的數據都是來自集合。實際上,它的數據來源不止是集合,在之後的例子中,你將會看到不一樣的數據來源。何爲管道?管道就是流操做序列。好比 filter -> map -> forEach ,你會發現他們一環扣一環,就像是一條流水線,故稱爲管道,即流操做序列。再一次強調,有序流並不存儲數據,它是經過管道在源頭獲取數據。一般來講,聚合操做接收 Lambda 表達式做爲參數的值,這些 Lambda 表達式具體規定聚合操做的內容。
關於聚合操做,還有不少關於功能和性能的重要知識,這些特性很是有吸引力,這些之後再講,此處暫不展開。
最終,爲了實現咱們的業務場景,咱們須要調用方法 processWithAggregate :
List<Person> roster = ... processWithAggregate(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email) );
GUI 的全稱是 Graphical User Interface ,翻譯成中文是圖形用戶界面。 GUI 應用程序能夠理解成桌面應用程序,咱們平常使用的瀏覽器、 QQ、 Word、 Eclipse 等都屬於 GUI應用程序。使用 Java 語言開發 GUI 應用程序不是很流行,這類應用程序一般使用 C++ 語言開發而成。固然,如果使用 Java 語言開發 GUI 應用程序, Lambda 也有不少的應用場景,在此咱們作一些簡單的介紹。若是你沒打算從事開發 Java GUI 應用程序的工做,本節的內容你稍作了解便可。
GUI 應用程序開發最多見的場景就是須要去響應各類事件,好比鍵盤事件,鼠標事件,滾動事件等。爲了響應事件,程序須要建立事件處理器,建立事件處理器一般須要實現事件處理器接口,事件處理器接口一般是函數式接口,只有惟一一個抽象方法。咱們先來看一個使用 JavaFX 實現的 Java GUI 應用程序的代碼片斷。
Button btn = new Button(); btn.setText("Say 'Hello World'"); btn.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { System.out.println("Hello World!"); } });
方法 setOnAction 的定義以下:
public final void setOnAction(EventHandler<ActionEvent> value);
爲了響應按鈕 Button btn 的鼠標單擊事件,咱們須要爲它建立一個事件處理器。「 btn.setOnAction 」便是爲按鈕建立事件處理器。在本例中,匿名類的實例就是事件處理器,並把這個實例做爲方法 setOnAction 的參數。該方法的參數 EventHandler<ActionEvent> value ,其類型是一個函數式接口類型,只有惟一一個抽象方法 「 void handle(T event) 」,因此,咱們可使用 Lambda 表達式來代替匿名類的實例,讓它做爲方法 setOnAction 的參數。
Button btn = new Button(); btn.setText("Say 'Hello World'"); btn.setOnAction( event -> System.out.println("Hello World!") );
一個 Lambda 表達式由如下幾個部分組成:
1.參數部分。參數有類型,有名字,多個參數使用逗號分隔,並用小括號括起來。好比前面屢次出現的 Lambda 表達式:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
你會以爲這個表達式的參數怪怪的,咱們先來看這個表達式的參數的完整的代碼:
(Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
Lambda 表達式的參數類型能夠省略。如果只有惟一一個參數,小括號能夠省略。到目前爲止,咱們所接觸到的函數式接口的抽象方法都只接受一個參數,下面,咱們來看一個它的抽象方法接受兩個參數的標準函數式接口 BiFunction<T, U, R> 。
BiFunction<T, U, R> 的功能是可以接受兩個參數,併產出一個結果。該泛型接口的尖括號「 <> 」有三個類型參數,第一個類型參數 T 表明抽象方法第一個參數的類型,第二個類型參數 U 表明抽象方法第二個參數的類型,第三個類型參數 R 表明抽象方法的返回值的類型。如下是該標準函數式接口抽象方法的定義:
R apply(T t, U u);
該抽象方法對指定的參數 T t , U u 執行一個功能(轉化、提取等操做),返回一個 R 類型的結果。
BiFunction<String, Integer, Float> biFunction = (firstAddend, secondAddend) -> Float.parseFloat(firstAddend) + secondAddend;
這個 Lambda 表達式表明對兩個參數 String firstAddend, Integer secondAddend 執行一個功能,獲得一個 Float 類型的結果。具體來講,就是對兩個參數進行加法運算,獲得和。咱們假設 String firstAddend 是數字符串,因此使用 Float.parseFloat(firstAddend) 轉爲 Float 類型,接着和 Integer secondAddend 進行加法運算,獲得 Float 類型的和。
2.箭頭 -> 。
3.方法體。Lambda 表達式的方法體和普通函數的方法同樣,使用花括號「{} 」括起來。花括號裏面寫 Java 語句。好比前面屢次出現的 Lambda 表達式:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
你會以爲這個表達式的方法體怪怪的,咱們先來看這個表達式的完整的代碼:
(Person p) -> { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; }
若 Lambda 表達式的方法體只有一條語句,形式爲「 {return 表達式;} 」,則這三部分「花括號 return ; 」能夠省略,只留下「表達式」。
咱們再來看一個前面寫過的 lambda 表達式:
email -> System.out.println(email)
咱們先來看這個 Lambda 表達式的方法體的完整代碼:
email -> { System.out.println(email); }
若 Lambda 表達式的方法體只有一條語句,形式爲「 {執行沒有返回值的代碼;} 」,則這兩部分「{} ;」能夠省略,只留下「執行沒有返回值的代碼」。
在講解 Lambda 表達式參數部分時,咱們寫過這樣的 Lambda 表達式:
BiFunction<String, Integer, Float> biFunction = (firstAddend, secondAddend) -> Float.parseFloat(firstAddend) + secondAddend;
如今咱們對這個 Lambda 表達式進行改造,讓它的方法體的邏輯豐富起來。
BiFunction<String, Integer, Float> biFunction2 = (firstAddend, secondAddend) -> { Float firstAddendFloat = null; try { firstAddendFloat = Float.parseFloat(firstAddend); } catch (NumberFormatException e) { System.out.println("第一個加數格式不正確,請輸入數字類型的字符"); return 0f; } return firstAddendFloat + secondAddend; };
該 Lambda 表達式方法體語句不止一條,花括號就不能省略了。
咱們屢次強調, Lambda 表達式本質是函數式接口的實現類實例,編寫 Lambda 表達式,就是在編寫函數式接口惟一抽象方法的是實現。既然如此, 和其餘類型的變量同樣,Lambda 表達式一般出如今塊( blocks )內。
塊指的是0 條或更多的 Java 語句,這些語句使用花括號「 {} 」圍起來。塊能出如今不少地方而且塊中還能有塊。
package cn.lambda.test; public class AccessVariable { public static void main(String[] args) { // 大塊開始 boolean condition = true; if (condition) { // 小塊 1 開始 System.out.println("條件爲 true 。"); } // 小塊 1 結束 else { // 小塊 2 開始 System.out.println("條件爲 false 。"); } // 小塊 2 結束 } // 大塊結束 }
Lambda 表達式如果出如今塊內,這就涉及到訪問塊內、塊外變量的權限問題。和匿名類( anonymous classes )和局部類( local classes )同樣, Lambda 表達式也能訪問塊內、塊外變量。在本節中,咱們以局部類和 Lambda 表達式作比較說明,講解 Lambda 表達式是如何訪問這些變量的。一開始,咱們須要回憶局部類是如何訪問塊內和塊外變量的。
局部類區別於獨立類,獨立類單獨定義在 Java 文件中( .java 文件 ),類名和 Java 文件名保持一致,編譯後會有獨立的字節碼文件( .class 文件 )。而局部類不能定義在獨立的 Java 文件中,它在獨立類的內部定義,最多見的是定義在獨立類的方法的方法體中,即方法體所在的塊。
package cn.lambda.test; public class LocalClassExample { private static String regularExpression = "[^0-9]"; // 正則表達式,非數字 // 格式化號碼 public static void validatePhoneNumber(final String phoneNumber1, final String phoneNumber2) { final int numberLength = 10; // 有效號碼位數 // int numberLength = 10; class PhoneNumber { // 經格式化的號碼(去掉號碼中非數字的部分) private String formattedPhoneNumber = null; // 格式化號碼 PhoneNumber(String phoneNumber) { // 修改塊內局部變量,錯誤: numberLength = 7; // 修改塊外類的成員變量,正確: regularExpression = "[^a-z]"; String currentNumber = phoneNumber.replaceAll(regularExpression, ""); if (currentNumber.length() == numberLength) formattedPhoneNumber = currentNumber; else formattedPhoneNumber = null; } } } }
咱們定義了一個獨立類 LocalClassExample ,它有一個 static String 類型的成員變量 regularExpression ,是個正則表達式,表明非數字。有個靜態方法 validatePhoneNumber ,兩個參數 String phoneNumber1 , String phoneNumber2 ,該方法用來格式化號碼。在方法體中定義了一個局部類 PhoneNumber 。方法體的多條語句組成了塊,可見,該局部類 PhoneNumber 定義在方法體所在的塊中。在這個方法體的塊中,有兩部份內容,一部分是 final int 類型的變量 numberLength ,表明有效號碼的位數,另外一部份內容就是局部類 PhoneNumber 。由於變量 numberLength 和局部類 PhoneNumber 處於同一個塊,相對於局部類 PhoneNumber 而言,變量 numberLength 稱之爲局部變量( local variables ),類 LocalClassExample 的成員變量 regularExpression 稱之爲塊外類成員變量,方法 validatePhoneNumber 的兩個參數 String phoneNumber1 , String phoneNumber2 稱之爲塊外方法參數變量。請你分清楚這三種變量,局部類 PhoneNumber 變量訪問的權限就是針對這三種變量的,它們分別是局部變量、塊外類成員變量、塊外方法參數變量。
局部類有一個 String formattedPhoneNumber 的成員變量,表明通過格式化的號碼,把原號碼的非數字部分去掉就成爲了格式化號碼。有一個構造函數,構造函數的功能就是對原號碼進行格式化。在局部類的構造函數中,咱們關注這一條語句,「 phoneNumber.replaceAll(regularExpression, ""); 」,這條語句把原號碼中的非數字字符替換掉,用到的變量 regularExpression 是塊外類成員變量,這說明局部類能夠訪問塊外類成員變量。而且這種訪問權限很是大,不但能夠讀取,還能夠修改,局部類修改塊外類的成員變量是被容許的。
在局部類的構造函數中,咱們關注這一條表達式,「 currentNumber.length() == numberLength 」,原號碼替換掉非數字的字符後,判斷號碼位數是否和有效的號碼位數相等。用到的變量 numberLength 是局部變量,這說明局部類能夠訪問局部變量。可是,訪問的權限不是很大。局部類訪問的局部變量必須是 final ,好比「 final int numberLength = 10; 」。但這不表明局部變量一旦聲明爲非 final ,編譯器就會報錯。好比,「 int numberLength = 10; 」,在局部變量 numberLength 的生命週期內(從它在方法 validatePhoneNumber 聲明的地方開始,一直到該方法結束),不去對 numberLength 進行賦值,編譯器是不會報錯的。聲明一個非 final 的局部變量並初始化後,在其生命週期內,再也不對其進行賦值,編譯器就認爲它是 final 的,聲明時有無加上關鍵字 final ,其效果同樣。可是,如果在局部變量 numberLength 的生命週期內,對它進行賦值,局部類訪問局部變量 numberLength ,編譯器就會報錯。好比,局部變量 numberLength 被聲明爲非 final ,在局部類的構造函數對其賦值,好比,「 numberLength = 7; 」,編譯器會報出錯誤「 Local variable numberLength defined in an enclosing scope must be final or effectively final 」,翻譯成中文便是「局部變量 numberLength ,定義在把它圍住的範圍內(塊內),必須是 final 或 有效 final 」。雖然能夠不把局部變量聲明爲 final ,但咱們仍是建議聲明爲 final,一是防止局部變量初始化後,咱們對它進行賦值(一旦賦值,就會編譯錯誤),二是一目瞭然,起到重要的提示做用。
在編譯器的報錯信息中,咱們注意到了有效 final 這個詞, final 和 有效 final 有何區別?對於一個基本類型的變量,一旦被聲明爲 final 且進行初始化,接下來有語句對它進行賦值,編譯器就會報錯「 The final local variable a cannot be assigned. It must be blank and not using a compound assignment 」,譯成中文大意是「 final 局部變量不能被賦值 」,這樣的 final 稱之爲有效 final 。可是,一個對象的引用被聲明爲 final 且指向一個對象,僅僅表明該對象引用不能指向新的對象,可是,該對象引用指向的對象的屬性值倒是能夠被修改的,這稱之爲非有效 final 。這句話至關繞口,咱們再來看看局部變量「 final int numberLength = 10; 」,它屬於基礎類型,因此該 final 是有效 final 。可是,咱們把 int 類型修改成 NumberLengthClass 類型,「 final NumberLengthClass numberLength = new NumberLengthClass(10); 」,該 final 就屬於非有效 final 了, 類 NumberLengthClass 定義以下:
package cn.lambda.test; public class NumberLengthClass { private int numberLength; // 有效號碼位數 public NumberLengthClass(int numberLength) { super(); this.numberLength = numberLength; } public int getNumberLength() { return numberLength; } public void setNumberLength(int numberLength) { this.numberLength = numberLength; } }
咱們在局部變量 NumberLengthClass numberLength 的生命週期內,對它進行賦值(對象引用指向新的對象),好比,在局部類對它進行賦值,「 numberLength = new NumberLengthClass(9); 」,編譯報錯「 The final local variable numberLength cannot be assigned, since it is defined in an enclosing type 」,譯成中文「 final 局部變量 numberLength 不能被賦值,由於它被定義在把它圍住的範圍內(塊內)」。可是,在局部變量 NumberLengthClass numberLength 的生命週期內,修改它指向的對象的屬性值,編譯倒是經過的。好比,在局部類修改它指向的對象的屬性值,「 numberLength.setNumberLength(5); 」,編譯不會報錯。正是由於如此,「 final NumberLengthClass numberLength = new NumberLengthClass(10); 」,稱之爲非有效 final 。
Java 語法規定,局部類能訪問局部變量,但局部變量必須是 final ,因此建議程序員把局部變量聲明爲 final 。可是,因爲非有效 final 的存在,存在修改局部變量而編譯器卻不報錯的狀況。但請你務必注意,即使編譯器不報錯,即使有非有效 final 的存在,你依舊不要嘗試去修改局部變量的值,不然,你將可能嚐到難以發現緣由的苦頭。至於其中的原理,咱們就再也不此處展開,由於這已經偏離本節的主題。
事實上,局部類還能夠訪問塊外方法的參數變量。可是,和訪問局部變量同樣,塊外方法的參數變量必須是 final 的。咱們能夠在局部類 PhoneNumber 定義一個方法 printOriginalNumbers ,用來訪問塊外方法的參數變量,表明獲取原始號碼。
package cn.lambda.test; public class LocalClassExampleTest { // 格式化號碼 public static void validatePhoneNumber(final String phoneNumber1, final String phoneNumber2) { class PhoneNumber { public void printOriginalNumbers() { // 修改塊外方法參數變量,錯誤: phoneNumber1 = "123-456-7890"; System.out.println("原始號碼,第一個是 " + phoneNumber1 + " ,第二個是 " + phoneNumber2); } } } }
局部類訪問局部變量和塊外方法參數變量,有句專業的表達,叫作捕獲變量和參數( it captures that variable or parameter )。此情景中,局部變量稱之爲被捕獲變量( captured variable ),塊外方法參數變量稱之爲被捕獲參數( captured parameter )。
最後,把這個例子的完整代碼附上。
package cn.lambda.test; public class LocalClassExample { private static String regularExpression = "[^0-9]"; // 正則表達式,非數字 // 格式化號碼 public static void validatePhoneNumber(final String phoneNumber1, final String phoneNumber2) { final int numberLength = 10; // 有效號碼位數 // int numberLength = 10; class PhoneNumber { // 經格式化的號碼(去掉號碼中非數字的部分) private String formattedPhoneNumber = null; // 格式化號碼 PhoneNumber(String phoneNumber) { // 修改塊內局部變量,錯誤: numberLength = 7; // 修改塊外類的成員變量,正確: regularExpression = "[^a-z]"; String currentNumber = phoneNumber.replaceAll(regularExpression, ""); if (currentNumber.length() == numberLength) formattedPhoneNumber = currentNumber; else formattedPhoneNumber = null; } // 獲取格式化的號碼 public String getNumber() { return formattedPhoneNumber; } // 獲取原始號碼 public void printOriginalNumbers() { // 修改塊外方法參數,錯誤: phoneNumber1 = "123-456-7890"; System.out.println("原始號碼,第一個是 " + phoneNumber1 + " ,第二個是 " + phoneNumber2); } } PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1); PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2); myNumber1.printOriginalNumbers(); if (myNumber1.getNumber() == null) System.out.println("第一個是無效號碼"); else System.out.println("第一個是格式化號碼是 " + myNumber1.getNumber()); if (myNumber2.getNumber() == null) System.out.println("第二個是無效號碼"); else System.out.println("第二個是格式化號碼是 " + myNumber2.getNumber()); } public static void main(String... args) { validatePhoneNumber("123-456-7890", "456-7890"); } }
在局部類定義了一些變量,這些變量能夠是局部類成員變量,也能夠是局部類方法的參數變量。如果這些變量和塊外的變量重名,這些局部類變量便會遮蔽( shadow )塊外的變量。
package cn.lambda.test; public class OuterClass { private int x = 96; // 1.塊外類成員變量 x,值等於 96 // 2.塊外方法參數變量 x,值等於 97 public void outerClassMethod(int x) { // 局部變量不能和塊外方法參數變量重名 int x = 100; class LocalClass { // 3.局部類成員變量 x,值等於 98 private int x = 98; // 4.局部類方法參數變量 x ,值等於 99 public void localClassMethod(int x) { System.out.println(x); // 得到第 4 個 x 的值 System.out.println(this.x); // 得到第 3 個 x 的值 System.out.println(OuterClass.this.x); // 得到第 1 個 x 的值 // 第 2 個 x 的值沒法直接獲取。 } } LocalClass localClass = new LocalClass(); localClass.localClassMethod(99); } public static void main(String... args) { OuterClass outerClassInstance = new OuterClass(); outerClassInstance.outerClassMethod(97); } }
咱們定義了一個獨立的類 OuterClass ,獨立類內如果定義了局部類,獨立類也稱之爲外部類,因此咱們給這個獨立類取名爲 OuterClass 。它有一個成員變量 x ,相對於局部類 LocalClass而言,此 x 稱之爲塊外類成員變量 x ,咱們把它標記爲第 1 個 x ,值是 96 。獨立類 OuterClass 有個方法 outerClassMethod ,方法參數變量名字也叫作 x ,相對於局部類 LocalClass 而言,此 x 稱之爲塊外方法參數變量 x ,咱們把它標記爲第 2 個 x 。主函數調用此方法時,傳入的值是 97 ,因此第 2 個 x 的值是 97 。方法 outerClassMethod 的方法體所在塊定義了局部類 LocalClass ,局部類有個成員變量,名字也叫作 x ,此 x 稱之爲局部類成員變量 x ,咱們把它標記爲第 3 個 x ,值等於 98 。局部類 LocalClass 有個方法 localClassMethod ,方法參數變量名字也叫作 x ,此 x 稱之爲局部類方法參數變量 x ,咱們把它標記爲第 4 個 x 。調用該方法時,傳入的值是 99 ,因此該 x 的值等於 99 。
如今咱們來關注局部類 LocalClass 的方法 localClassMethod ,這個方法有三行語句。第一行語句,「 System.out.println(x); 」,此 x 是指的是第 4 個 x ,即局部類的方法參數變量 x 。第二行語句,「 System.out.println(this.x); 」,此 x 指的是第 3 個 x ,即局部類的成員變量 x 。主函數建立了外部類 OuterClass 的實例 outerClassInstance ,接着使用該實例調用它的方法 outerClassMethod ,在該方法中,定義了局部類 LocalClass ,並建立了局部類的實例 localClass ,並使用該實例調動它的方法 localClassMethod 。從調用的順序來看,程序先建立了外部類 OuterClass 的實例 outerClassInstance ,接着才建立局部類 LocalClass 的實例 localClass 。所以,局部類 LocalClass 的方法 localClassMethod 第二行語句,「 System.out.println(this.x); 」, this.x 便是當前對象的 x ,定義爲局部類的成員變量的 x 。該方法的第三行語句, 「 System.out.println(OuterClass.this.x); 」 ,此 x 指的是第 1 個 x ,OuterClass.this.x 便是外部類(塊外類)當前對象的 x ,定義爲塊外類成員變量 x 。
須要特別注意的是,第 2 個 x ,即塊外方法參數變量 x ,如果堅持不更名字或藉助臨時變量等手段,局部類 LocalClass 的方法 localClassMethod 再也沒法直接訪問。另外,局部變量不能取名爲 x ,由於 Java 的一條基本語法是,「方法體所定義的變量不能和方法的參數變量重名。」固然,在實際的開發工做中,不管是塊內變量仍是塊外變量,咱們應當儘可能避免重名,不然,特別容易引發混淆。如果有重名狀況發生時,變量的取值就會遵循遮蔽的語法。
關於局部類訪問塊內和塊外變量的,咱們總結以下:
1.能夠訪問局部變量,可是局部變量必須是 final 。
2.能夠訪問塊外方法參數變量,可是塊外方法參數變量必須是 final 。
3.能夠訪問塊外類成員變量,權限很大,能讀能寫。
4.局部類的成員變量或是方法參數變量,會遮蔽塊外類(外部類)的同名變量。
關於 lambda 表達式,上述第 一、二、3 點特徵,它也具有。可是,它不具有第 4 點特徵。即lambda 表達式的方法參數變量,不會遮蔽塊外類(把它圍住的那個類)的同名變量。雖然 lambda 表達式是從匿名類演化而來, lambda 表達式的定義是匿名類的定義的簡化版。可是,lambda 表達式和匿名類仍是有區別的。 請注意這句話, Lambda 表達式被詞法定界了( Lambda expressions are lexically scoped. )。詞法定界不是很好理解,表達式如果被詞法定界,意味着它不會繼承父接口的任何成員變量,沒有遮蔽功能。咱們知道, lambda 表達式是函數式接口的實現類實例,因此,定義 lambda 表達式,實際上要經歷兩件事情。第一件事情是定義函數式接口實現類,第二件事情是建立該實現類實例。 this 稱之爲當前對象,可是, 定義 lambda 表達式時,也就是定義函數式接口實現類時, lambda 表達式表明的實現類自己沒有 this 對象,此時如果使用 this 對象,指的是把 lambda 表達式圍住的類的當前對象,而不是 lambda 表達式表明的實現類的當前對象。 lambda 表達式被詞法定界的表現總結以下:
1. 不會繼承父接口的任何成員變量。
2. 沒有遮蔽功能。
3. lambda 表達式表明的實現類沒有 this 對象,this 對象指的是把 lambda 表達式圍住的類的當前對象。
下面咱們結合匿名類,舉例說明 lambda 表達式被詞法定界的表現。
package cn.lambda.test; @FunctionalInterface public interface Animal { // 動物共同特徵,能呼吸 boolean breath = true; // 獲取動物特徵 void getFeature(); }
定義一個函數式接口 Animal ,該接口表明動物,有一個 boolean 類型的屬性是 breath ,表明動物都能呼吸。接口中的屬性的默認修飾符是 public static final 。該接口有一個方法 getFeature ,表明獲取動物的特徵。一開始,咱們使用匿名類實現該函數式接口。該匿名類表明鳥類,鳥類從動物繼承了能呼吸的特徵。另外,它有本身的獨特特徵,即能飛行,有飛行高度的屬性。
package cn.lambda.test; public class TestAnimal { public static void main(String[] args) { Animal bird = new Animal() { private int flighAltitude = 500; // 飛行高度,鳥類獨特特徵 @Override public void getFeature() { System.out.println("動物共同特徵,能呼吸: " + breath); System.out.println("鳥類飛行高度: " + this.flighAltitude); } }; bird.getFeature(); } }
在匿名類的定義中,你會發現匿名實現類可以繼承函數式接口的成員變量 breath ,所以,語句 [ System.out.println("動物共同特徵,能呼吸: " + breath); ] 能夠正常輸出 true 。但如果使用 lambda 表達式,你會驚訝的發現,該語句會報錯。關注這行語句, [ System.out.println("鳥類飛行高度: " + this.flighAltitude); ] ,輸出結果是 500 。請你注意, flighAltitude 是匿名實現類的成員變量, this 表明匿名實現類的當前對象,而不是把匿名類圍住的類的當前對象,即不是 TestAnimal 類的當前對象。如果使用 lambda 表達式, 此處的 this 成了 TestAnimal 類的當前對象。下面,咱們使用 lambda 表達式實現該函數式接口。
package cn.lambda.test; public class TestAnimalWithLambda { private boolean testClass = true; public void bird() { Animal bird = () -> { // 錯誤: lambda 表達式不會繼承父接口成員變量 // System.out.println("動物共同特徵,能呼吸: " + breath); System.out.println("鳥類飛行高度: " + this.testClass); }; bird.getFeature(); } public static void main(String[] args) { new TestAnimalWithLambda().bird(); } }
在 lambda 表達式的定義中,語句 [ System.out.println("動物共同特徵,能呼吸: " + breath); ] ,編譯報錯 「 breath cannot be resolved to a variable 」,譯成中文「 breath 沒法解析成變量」,這是由於 lambda 表達式不會繼承父接口的任何成員變量。關注這一行代碼 [ System.out.println("鳥類飛行高度: " + this.testClass); ] ,this 不是 lambda 表達式表明的實現類的當前對象,而把 lambda 表達式圍住的類的當前對象,便是 TestAnimalWithLambda 類的當前對象, testClass 是 TestAnimalWithLambda 類的成員變量。
前面咱們講解局部類變量的遮蔽功能時,定義過一個類 OuterClass ,如今對它進行改造,在局部類的方法 localClassMethod 使用 lambda 表達式。
package cn.lambda.test; import java.util.function.Consumer; public class LambdaScope { private int x = 96; // 1.塊外獨立類成員變量 x,值等於 96 // 2.塊外獨立類方法參數變量 x,值等於 97 public void outerClassMethod(int x) { class LocalClass { // 3.塊外局部類成員變量 x,值等於 98 private int x = 98; // 4.塊外局部類方法參數變量 x ,值等於 99 public void localClassMethod(int x) { // int y = 0; // 此處如有上述語句,Lambda 表達式參數變量名字不能是 y Consumer<Integer> consumer = p -> { System.out.println(x); // 得到第 4 個 x 的值 System.out.println(this.x); // 得到第 3 個 x 的值 System.out.println(LambdaScope.this.x); //得到第 1 個 x 的值 // 第二個 x 的值沒法直接獲取。 }; consumer.accept(99); } } LocalClass localClass = new LocalClass(); localClass.localClassMethod(99); } public static void main(String... args) { LambdaScope outerClassInstance = new LambdaScope(); outerClassInstance.outerClassMethod(97); } }
以上代碼註釋是站在 lambda 表達式的角度上進行的,它所在塊是局部類的 LocalClass 的方法 localClassMethod 的方法體。 Lambda 表達式的參數變量名字是 p ,如果修改成 x ,編譯報錯,「 Lambda expression's parameter x cannot redeclare another local variable defined in an enclosing scope. 」,譯成中文「把 lambda 表達式圍住的範圍,已經定了局部變量 x , lambda 表達式的參數變量不能再次聲明爲 x.」。把 lambda 表達式圍住的範圍,指的是局部類 LocalClass ,局部類的方法 localClassMethod 參數變量已經取名爲 x ,所以 lambda 表達式的參數變量名字就不能取名爲 x 了。之因此 lambda 表達式不能和局部變量或塊外方法參數變量重名,是由於 lambda 表達式沒有遮蔽功能。
上面咱們提到而來匿名類,局部類,獨立類等類,你可能有點混淆。局部 Java 的語法規則,類分爲這麼幾種。粗粒度地分爲獨立類( independent classes ),內嵌類( nested classes )。內嵌類分爲靜態內嵌類( static nested classes )和非靜態內嵌類( non-static nested classes ),非靜態內嵌類也稱之爲內部類( inner classes )。內部類有普通內部類和特別的內部類,特別內部類有兩個,分別是局部類( local classes )和匿名類( anonymous classes )。
咱們來看一看前面寫了屢次的 lambda 表達式。
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
該 lambda 表達式的含義是挑選符合美國義務兵役制度的會員,符合這一制度的具體條件是男性且年齡在 18 至 25 歲之間。咱們暫且把這個 lambda 表達式稱之爲挑選會員 lambda 表達式。如果僅僅只看這個表達式,咱們能知道的信息很是有限。咱們僅能知道有個函數式接口,它的抽象方法接受一個參數,返回一個 boolean 類型結果。至因而哪個函數式接口,須要觀察 lambda 表達式所處的環境( context or situation )。在前面的例子中,咱們定義這樣兩個方法:
public static void printPersons(List<Person> roster, CheckPerson tester); public static void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester);
接着,咱們使用挑選會員 lambda 表達式做爲這兩個方法的參數,對它們進行調用:
List<Person> roster = ... printPersons(roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25); List<Person> roster = ... printPersonsWithPredicate(roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25);
方法 printPersons 的第二個參數是函數式接口 CheckPerson 類型,它的抽象方法接受一個參數,返回一個 boolean 類型的結果。挑選會員 lambda 表達式恰好符合這一特性,因此它能夠做爲方法 printPersons 的第二個參數,該 lambda 表達式的類型就是CheckPerson 類型。
方法 printPersonsWithPredicate 的第二參數是函數式接口 Predicate 類型,它的抽象方法一樣接受一個參數,返回一個 boolean 類型的結果。挑選會員 lambda 表達式恰好符合這一特性,因此它能夠做爲方法 printPersonsWithPredicate 的第二個參數, 該 lambda 表達式的類型就是 Predicate 類型。
可見,在不一樣的環境(本例是做爲方法的實參), lambda 表達式能夠屬於不一樣函數接口的實現類的實例,會有不一樣的類型。只要 lambda 表達式符合函數式接口的抽象方法的定義,它就能夠成爲該函數式接口的實現類的實例。
每一個方法都有本身指望的參數類型,好比方法 printPersons 的第二個參數指望的類型是函數式接口 CheckPerson 類型,方法 printPersonsWithPredicate 的第二參數指望的類型是函數式接口 Predicate 類型,方法指望的參數類型稱之爲目標類型( target type )。編譯器在 lambda 表達式所處的環境裏(好比, lambda 做爲方法的實參),根據目標類型,確認 lambda 表達式的類型。 Lambda 表達式所處的環境,不僅僅是做爲方法的實參,還能夠是如下這些環境:
1.變量聲明;
2.賦值;
3.返回值;
4.數組初始化;
5.方法實參;
6.Lambda 表達式方法體;
7.條件表達式( condition ? result1 : result2 );
8.強制轉換表達式。
當 lambda 所處的環境是做爲方法的實參,如果不一樣的方法取不一樣的名字,目標類型很好確認,就像是方法printPersons 的第二參數和方法 printPersonsWithPredicate 的第二個參數,前者目標類型是CheckPerson ,後者是 Predicate。可是,如果在一個類中,存在重載的兩個方法,目標類型該如何肯定呢?JDK 有這樣兩個函數式接口:
@FunctionalInterface public interface Runnable { public abstract void run(); } @FunctionalInterface public interface Callable<V> { V call() throws Exception; }
咱們自定義一個類 TargetType ,該類有兩個重載的方法 invoke 。
package cn.lambda.test; import java.util.concurrent.Callable; public class TargetType { public static void invoke(Runnable r) { r.run(); } public static <T> T invoke(Callable<T> c) throws Exception { return c.call(); } public static void main(String[] args) throws Exception { String s = invoke(() -> "done"); } }
主函數調用方法 invoke ,那麼,哪個invoke 方法會被調用?如果沒有返回值的 invoke方法被調用,則目標類型和 lambda 表達式類型是函數式接口 Runnable 類型,如果有返回值的 invoke 方法被調用,則目標類型和 lambda 表達式類型是函數式接口 Callable 類型。答案是有返回值的那個 invoke 方法會被調用。定義在主函數的 lambda 表達式沒有方法參數,但有返回值。有返回值的 invoke 方法的參數類型是 Callable 類型,該接口的抽象方法 call 正好是沒有參數,但有返回值,和定義在主函數的 lambda 表達式相匹配。沒有返回值的 invoke 方法的參數類型是 Runnable 類型,該接口的抽象方法 run 既無參數也無返回值,和定義在主函數的 lambdad 表達式不匹配,因此主函數調用的 invoke 是有返回值的 invoke 方法, 目標類型和 lambda 表達式的類型是函數式接口 Callable 類型。
Lambda 表達式只要符合如下的條件,它是能夠序列化的:
1.它的目標類型是可序列化的;
2.它所捕獲的變量是可序列化的。
可是,和內部類(包括普通內部類、局部類和匿名類)同樣,序列化 lambda 表達式是很是不推薦的。至於其中的緣由,你們能夠自行去了解內部類的序列化問題,此處再也不展開,由於它偏離了本節的主題。
如今咱們知道,若接口是函數式接口,它的實現類實例可使用 lambda 表達式,比起匿名類實例, lambda 表達式更加的簡潔和可讀性更好。然而, Java 設計者們並不知足於此,他們以爲,在某些狀況, lambda 表達式能夠被替代,有新的表達方式可讓代碼變得更加簡潔和可讀性更好。有些時候, lambda 表達式的方法體僅僅是使用了一條語句,調用了一個方法就結束了。在這樣的場景下,咱們可使用方法引用( method references )代替 lambda 表達式,讓你代碼變得更少。
在前面,咱們定義了會員實體類,全部應用會員存儲在集合 List<Person> roster 對象中。如今,咱們假設會員存儲在數組中,而且根據他們的生日進行正向排序,即生日從早到晚,從小到大。在 Java 領域,說一個時間早,指的是該時間距離 1970 年 1 月 1 日 0 點 0 分 0 秒比較近。相反地,說一個時間晚,指的是該時間距離 1970 年 1 月 1 日 0 點 0 分 0 秒比較遠。好比,2016 年和 2017 年相比, 2016 比較早,比較小, 2017 比較晚,比較大。可見,如果按照時間進行正向排序,時間早,時間小,排在前面,時間晚,時間大,排在後面。如果按照會員生日進行正向排序,等同按照會員的年齡從小到大進行排序。首先,我麼須要把 List<T> 類型的 roster 轉化爲數組類型的 rosterAsArray 。
Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);
<T> T[] toArray(T[] a) :該方法返回一個數組,數組元素包含了 List<T> 中的全部元素,數組中的元素順序和 List<T> 中的元素順序保持一致。該方法聲明瞭類型參數(泛型參數) T ,表明數組元素的類型。請注意,返回值類型 T[] 和參數類型 T[] 是同一種類型,這就說明,參數指定的類型等同了返回值的類型。在本例,咱們的參數指定爲 new Person[roster.size()] ,它的類型是一個元素類型爲 Person 的數組,這就表明返回值類型也是元素類型爲 Person 的數組。該方法的參數是 T[] a ,如果 a 的大小可以裝得下 List<T> 中的全部元素,該方法的返回值就是 a 。不然,該方法會建立一個新的數組,其類型和 a 保持一致,大小和 List<T> 元素個數保持一致,返回值就是新建立的數組。在本例,該方法的參數 new Person[roster.size()] ,它的大小恰好是 List<T> 元素的個數,可以裝得下 List<T> 中的全部元素,因此,該方法返回值就是數組 new Person[roster.size()] ,無需建立新的數組。該方法的參數是 T[] a ,如果 a 的大小超過 List<T> 中元素的元素個數,剩餘的空間會被設置成 null 。
該方法是基於數組的對象和基於 Collection 的對象( List 是 Collection 的子類)的橋樑,提供了把基於 Collection 的對象轉化爲基於數組的對象的通道。而且,該方法能夠經過指定參數的類型,精確地控制返回值的類型,而不是簡單地返回一個元素類型是 Object 類型的數組。參數類型是數組 T[] ,建立數組時,經過指定數組的大小( List<T> 的元素個數有多少,就指定數組的大小有多大),能夠有效的避免空間的浪費。
想要對 Person[] rosterAsArray 中的元素根據他們的生日進行正向排序,咱們須要定義一個生日比較器( Comparator )。
package cn.lambda.test; import java.util.Comparator; class PersonAgeComparator implements Comparator<Person> { public int compare(Person a, Person b) { return a.getBirthday().compareTo(b.getBirthday()); } }
會員的生日字段的類型是 LocalDate ,該類有個方法 public int compareTo(ChronoLocalDate other) , LocalDate 是 ChronoLocalDate 的子類。該方法比較兩個日期,調用該方法的當前對象表明一個日期(當前日期),該方法參數表明另外一個日期。既然是計較,就涉及到排序,該方法的排序是正向排序,即時間從小到大,或者說從早到晚。該方法返回值是 int ,若當前日期小於另外一個日期,返回負數,大於另外一個日期,返回正數,相等,返回 0 。
Comparator<T> 是一個函數式接口,類型參數 T 表明待比較對象的類型。咱們自定義的比較器 PersonAgeComparator 須要實現該接口,該接口的惟一一個抽象方法, 「 int compare(T o1, T o2); 」,該方法比較參數指定的兩個對象。若第一個對象 o1 大於或等於或小於第二個對象 o2 ,該方法分別返回正整數、 0 、負整數。這樣的返回值規則和類 LocalDate 的方法 compareTo 徹底一致,因此,在編寫類 PersonAgeComparator 的方法 compare 的具體邏輯中,咱們調用了類 LocalDate 的方法 compareTo ,實現按照會員的生日從小到大的排序的邏輯。
有了生日比較器,咱們就能夠對數組Person[] rosterAsArray 進行排序了。
Arrays.sort(rosterAsArray, new PersonAgeComparator());
public static <T> void sort(T[] a, Comparator<? super T> c) :該方法根據指定的比較器 c,對數組 a 進行排序。對於本例而言,就是根據生日比較器(年齡從小到大),對會員進行排序。固然,咱們須要保證數組中的元素都是都是能夠彼此進行比較的。好比,本例中的數組元素是 Person 類型的會員,具體的邏輯是對會員的生日進行比較,因此數組不能出現生日 birthday 是 null 的會員。其次,該方法能保證每次的排序結果都是同樣的。這句話是針對排序是相等的元素而言的。好比數組有兩個會員 p1 、 p2 ,他們的生日都是同一天,那麼排序時,如果 p1 被排在第二位, p2 被排在第三位,之後進行排序,都是這樣的順序,不會有時候成了p1 被排在第三位, p2 被排在第二位。該方法的這一特徵有句專業的表達,「 This sort is guaranteed to be stable.」 ,譯成中文「排序可以保證是穩定的」。
爲了可以對數組 Person[] rosterAsArray進行排序,根據數組元素會員的生日從早到晚,年齡從小到大進行排序,咱們定義了獨立類(比較器) PersonAgeComparator 去實現接口 Comparator<T> ,接着建立獨立類的實例,做爲方法 sort 的第二個參數。請注意 sort 方法的第二個參數類型 Comparator<T> ,它是一個函數式接口,因此咱們徹底可使用 lambda 表達式來代替上述的步驟。
Arrays.sort(rosterAsArray, (a, b) -> a.getBirthday().compareTo(b.getBirthday()));
Lambda 表達式的方法體有一條語句,咱們把這條語句寫在會員實體類 Person 的靜態方法 compareByAge 中,該方法定義以下:
public static int compareByAge(Person a, Person b) { return a.birthday.compareTo(b.birthday); }
實體類 Person 經改造以後,多了一個方法 compareAge ,如今是這樣子的:
package cn.lambda.test; import java.time.LocalDate; import java.util.List; public class Person { // 性別枚舉 public enum Sex { MALE, FEMALE } private String name; // 姓名 private LocalDate birthday; // 生日 private Sex gender; // 性別 private String emailAddress; // 郵件地址 private int age; // 年齡 public void printPerson() { // 打印會員的我的信息 } public static int compareByAge(Person a, Person b) { return a.birthday.compareTo(b.birthday); } // getter setter methods }
這樣一來, lambda 表達式能夠寫成這樣:
Arrays.sort(rosterAsArray, (a, b) -> Person.compareByAge(a, b));
你仔細觀察這個 lambda 表達式,發現它的方法體就是調用了一個方法,所以,咱們可使用方法引用代替 lambda 表達式。
Arrays.sort(rosterAsArray, Person::compareByAge);
相信你立馬發現,方法引用讓代碼更少了,可讀性更好了。方法引用的語法簡單極了,在本例中,它的語法形式是「類名::靜態方法名」,如果把它轉爲 lambda 表達式,它表達兩層含義:
1.lambda 表達式的方法參數就是 compareAge 的方法參數,即「 (Person a, Person b) 」 。
2.lambda 表達式的方法體就是調用了方法 compareAge ,即 「 Person.compareByAge(a, b) 」。
方法引用有四種類型,這是其中一種,稱之爲訪問靜態方法( compareAge 是靜態方法),語法形式是「 ContainingClass::staticMethodName 」,即「類名::靜態方法」。
實體類 Person 的方法 compareAge 如果非靜態的方法,而是特定對象的實例方法,咱們該如何對數組 Person[] rosterAsArray進行排序呢?
Person personInLambda = new Person(); Arrays.sort(rosterAsArray, (a, b) -> { return personInLambda.compareByAge(a, b); });
由於如今,方法 compareByAge 是實例方法,必須使用 Person 類型的實例來調用它,因此咱們須要建立 Person 類型的實例 personInLambda 。如今的 lambda 表達式,它的方法體依舊只是調用了一個方法,所以,咱們可使用方法引用代替 lambda 表達式。
Person personInLambda = new Person(); Arrays.sort(rosterAsArray, personInLambda::compareByAge);
這種方法引用稱之爲訪問特定對象的實例方法,語法形式是「 containingObject::instanceMethodName 」,即「對象::實例方法」。如果把它轉爲 lambda 表達式,它表達兩層含義:
1.lambda 表達式的方法參數就是 compareAge 的方法參數,即「 (Person a, Person b) 」 。
2.lambda 表達式的方法體就是調用了方法 compareAge ,即 「 personInLambda.compareByAge(a, b) 」
String 類有這樣一個很是實用的實例方法 「 public int compareToIgnoreCase(String str) 」,以調用該方法的當前對象做爲做爲一個字符串(下文稱之爲 this 字符串),方法參數做爲另外一個字符串(下文稱之爲 another 字符串),忽略這兩個字符串的大小寫,按照字典序,對它們進行比較。何爲字典序?每一個字符串( strings )都是由 0 至多個字符( character )組成的,每一個字符都有 Unicode 值,多個字符組成了字符序列( character sequence )。 this 字符串是一個字符序列, another 字符串也是字符序列,這兩個字符序列按照字典序進行比較。
若是兩個字符串是不一樣的,從某個索引開始,字符是不同的,這個索引稱之爲有效索引( valid index )。好比說,字符串「 lambdaexpression 」和「 lambdatest 」,這兩個字符串是不一樣的,索引 0 到索引5 ,字符相同,第 6 個索引,字符串「 lambda 」是字符 「 e 」,字符串「 lambdatest 」是字符「 t 」,字符不同,因此索引 6 稱之爲有效索引。固然還有其餘的索引,這兩個字符串的字符也是不一樣的,此處咱們取這些索引中的最小值,使用 K 標記該索引。此時,方法 compareToIgnoreCase 的返回值是:
this.charAt(k) - anotherString.charAt(k)
this 表明 this 字符串,anotherString 表明 another 字符串,方法「 public char charAt(int index) 」返回指定索引 index 的字符。兩個字符進行求差,實際是兩個字符的 Unicode 值進行求差,字符會自動轉化爲對應的 Unicode 值,再進行運算。可見,若是this 字符串位於索引 K 的字符的 Unicode 值比較小,方法 compareToIgnoreCase 的返回值是負整數,表示 this 字符串比較小,排在 another 字符串的前面。若是this 字符串位於索引 K 的字符的 Unicode 值比較大,方法 compareToIgnoreCase 的返回值是正整數,表示 this 字符串比較大,排在 another 字符串的後面。若是this 字符串位於索引 K 的字符的 Unicode 值和 another 字符串位於索引 K 的字符的 Unicode 值相等,方法 compareToIgnoreCase 的返回值是 0 ,表示 this 字符串和 another 字符串相等。
有時候,兩個字符串是不一樣的,可是不存在索引,它們字符是不一樣的。好比字符串「 lambda 」和「 lambdaexpression 」,顯然,這兩個字符串是不一樣的,長度不一樣,可是直到第一個字符串的索引結束,也不存在和第二個字符串不一樣的字符。此時,方法 compareToIgnoreCase 的返回值是:
this.length()-anotherString.length()
可見,在這場場景下,方法 compareToIgnoreCase 的返回值是由兩個字符串的長度計算的,再也不是由某個索引的字符的 Unicode 值計算的。固然,計算結果依然是負整數、正整數、 0 ,依舊錶明 this 字符串「比較小,排在 another 字符串的前面」、「比較大,排在 another 字符串的後面」、「和 another 字符串相等」。
須要注意的是,方法 compareToIgnoreCase 計算兩個字符串的某個索引的字符的 Unicode 值時,是忽略大小的,兩個字符串會在一開始就被格式化成所有小寫。好比 this 字符串位於索引 K 的字符是「 a 」,another 字符串位於索引 K 的字符是「 A 」,方法 compareToIgnoreCase 計算時,這兩個字符的 Unicode 值相等。若是你想讓兩個字符串的字符大小寫敏感,使用 String 類的另外一個方法「 public int compareTo(String anotherString) 」。
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" }; Arrays.sort(stringArray, (a, b) -> a.compareToIgnoreCase(b));
咱們定義了元素類型是 String 的數組 stringArray ,對數組中的字符串進行字典序排序,使用的是 String 類的實例方法 compareToIgnoreCase 。前面已經提過,類 Arrays 方法 sort 的第二個參數類型是函數式接口 Comparator<? super T> ,該接口的抽象方法 compare 的返回值規則和 String 類的方法 compareToIgnoreCase 徹底一致。抽象方法 compare 的返回值規則是,若第一個對象 o1 大於或等於或小於第二個對象 o2 ,該方法分別返回正整數、 0 、負整數。方法 compareToIgnoreCase 的返回規則是,若 this 字符串大於或等於或小於 another 字符串,該方法分別返回正整數、 0 、負整數。因此,在給抽象方法 compare 作實現時,能夠調用 String 類的方法 compareToIgnoreCase ,實現對數組中的字符串進行字典序排序,且爲正向排序。
lambda 表達式,有兩個參數,在本例中,咱們把這兩個參數取名爲 a 、 b ,表明數組中的兩個字符串。從語法上講,這兩個名字是隨機取的,稱之爲隨機名字( arbitrary names ),咱們徹底能夠爲它們取其餘的名字。當 java 運行時, lambda 表達式兩個參數變量,也就是 String 類型的對象引用,會指向兩個字符串對象。這兩個對象稱之爲特定類型的隨機對象( Arbitrary Object of a Particular Type )。方法 compareToIgnoreCase 是實例方法,使用的 lambda 表達式的第一個參數調用該方法,這稱之爲訪問特定類型的隨機對象的實例方法。
咱們注意到,該 lambda 表達式的邏輯一樣是調用一個方法就結束了。所以,咱們可使用方法引用代替 lambda 表達式。
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" }; Arrays.sort(stringArray, String::compareToIgnoreCase);
方法 compareToIgnoreCase 是實例方法,必須是 String 類型的實例才能夠調用它,但觀察這個方法引用「 String::compareToIgnoreCase 」,咱們看不出有任何 String 類型的實例調用該方法。請注意,如果方法引用的方法是實例方法,但看不出有實例調用它,說明如果使用 lambda 表達式,是 lambda 表達式的第一個方法參數調用了它。因此,這種形式的方法引用,「 String::compareToIgnoreCase 」,稱之爲訪問特定類型的隨機對象的實例方法。語法形式是,「 ContainingType::methodName 」,即「特定類型的隨機對象::實例方法」。如果把它轉爲 lambda 表達式,它表達兩層含義:
1.lambda 表達式的方法參數,第一個參數是做爲當前對象,調用了方法 compareToIgnoreCase ,其餘參數便是方法 compareToIgnoreCase 的參數,即「 (String a, String b) 」。
2.lambda表達式的方法體就是調用了方法 compareToIgnoreCase ,即「 a.compareToIgnoreCase(b) 」。
如今咱們有這樣的需求,把一個集合(源集合)的元素複製到另外一個集合(目標集合)。
public static <T, S extends Collection<T>, D extends Collection<T>> D transferElements( S sourceCollection, Supplier<D> collectionFactory) { D result = collectionFactory.get(); for (T t : sourceCollection) { result.add(t); } return result; }
該方法 transferElements 聲明瞭三個類型參數,第一個類型參數是 T ,表明任意類型。第二個類型參數是 S ,它的類型是 Collection<T> 或是 Collection<T> 的子類型。第三個類型參數是 D ,它的類型和第二個類型參數 S 同樣。該方法返回值是類型 D ,表明新的集合,即目標集合,裏面存放的元素是從老集合,即源集合複製而來。該方法有兩個參數,第一個參數 S sourceCollection 表明源集合,第二個參數 Supplier<D> collectionFactory 表明集合工廠,能夠從集合工廠獲取一個目標集合。
Supplier<T> 是一個標準函數式接口,表明供應結果的供應商。類型參數 T 表明結果的類型。該接口抽象方法「 T get(); 」,不接受參數,返回一個類型爲 T 的結果。
方法 transferElements 首先從集合工廠獲取一個結果,在本例中,該結果就是目標集合。接着遍歷源集合,把源集合的每個元素複製到目標集合,最後,把目標集合返回。
接下面調用方法 transferElements ,實現需求。
List<Person> roster = ... Set<Person> rosterSetLambda = transferElements(roster, () -> new HashSet<Person>());
源集合是元素類型是 Person 的 List 集合,目標集合是元素類型是 Person 的 HashSet 集合,實現了把 List 集合中的元素複製到 HashSet 集合。仔細觀察這個 lambda 表達式,它的方法體僅僅是調用了構造方法,建立了一個實例,因此,咱們可使用方法引用代替 lambda 表達式。
Set<Person> rosterSetLambda = transferElements(roster, HashSet<Person>::new);
這種形式的方法引用稱之爲訪問構造函數,語法形式是「 ClassName::new 」,即「類名::new 」。此時,相信你心中會有一個疑問,類 HashSet<E> 有好幾個構造函數,方法引用「 HashSet<Person>::new 」調用的哪個構造函數?這和目標類型有關。方法 transferElements 的第二個參數的目標類型是函數式接口 Supplier<D> collectionFactory ,該函數式接口的抽象方法不接受參數,因此,方法引用「 HashSet<Person>::new 」調用的就是無參的構造函數。
public static <T, R> R constructor(T t, Function<T, R> mapper) { return mapper.apply(t); }
該方法的功能是根據輸入,得到一個輸出。聲明瞭兩個類型參數 T 、 R ,均表明任意類型,返回值的類型是 R 。該方法有兩個參數,第一個參數是 T t 表明一個輸入,第二參數 Function<T, R> mapper 是標準函數式接口類型,前面已經提過,該接口表明根據輸入,得到一個輸出。下面咱們調用該方法。
constructor(2, t -> new Person());
輸入的是一個整數數值 2 ,輸出的一個 Person 實例。仔細觀察這個 lambda 表達式,它的方法體僅僅是調用了構造方法,並且是無參構造函數,建立了一個實例,因此,咱們可使用方法引用代替 lambda 表達式。
constructor(2, Person::new);
此時,方法引用「 Person::new 」調用的是類 Person 的哪一個構造函數?答案是帶有一個 int 或 Integer 類型的參數的構造函數會被調用。好比,類 Person 有以下的構造函數會被調用。
public Person(int age) { this.age = age; }
爲何呢?這和目標類型有關。方法 constructor 的第二個參數類型是 Function<T, R> mapper ,它的抽象方法「 R apply(T t); 」接受一個類型爲 T 的參數,在本例中,接收一個類型爲 Integer 的參數。因此,方法引用「 Person::new 」調用的是帶有一個 int 或 Integer 類型的參數的構造函數。如果把訪問構造函數的方法引用轉爲 lambda 表達式,它表達兩層含義:
1.lambda 表達式的方法參數就是構造函數 Person(int age) 的方法參數,即「 (int age) 」 。
2.lambda 表達式的方法體就是調用了構造函數 ,即 「 new Person(age) 」。構造函數的參數是由目標類型決定的,目標類型必定是函數式接口類型,接口的抽象方法的參數和構造函數的參數保持一致。
至此,有關 lambda 的知識就講完了。 Java 設計者們以匿名類爲出發點,設計了 lambda 表達式,進而設計出方法引用。目的是讓程序的代碼變得更少,可讀性更好。
更多技術交流,敬請關注微信公衆號,掃一掃下方二維碼便可關注: