Java8————Lambda表达式(二)
譯者注:文中內容均來自于官方教程《Lambda Expressions》,但是由于英漢語言的差異,部分語句官方描述過于冗余,因此譯者根據通常狀況的理解做了微調,但不會影響表達的含義。比如:
原文:You want to create a feature that enables an administrator to perform any kind of action, such as sending a message, on members of the social networking application that satisfy certain criteria.?
精準翻譯:你想要創建一個可以讓管理員表現某種行為的功能,比如發送一個消息給你的社交網絡應用中滿足具體條件的用戶。
譯者翻譯:?你希望開發一個可以讓管理員執行某種行為的功能,比如發送消息給那些滿足某種條件的用戶。
?Lambda表達式
使用匿名類的時候有一個問題是,如果你的匿名類(譯者注:匿名內部類就是為了實現某些接口而存在的)實現非常簡單,比如一個只包含一個方法的接口,那么匿名類的語法可能會有些笨拙和不清晰。這種情況下,通常要嘗試傳入一個函數作為另一個方法的參數,比如,當某人點擊一個按鈕時會執行什么動作?Lambda表達式允許你將一個函數作為方法的參數,或以代碼作為數據。
在前面的部分,Anonymous Classes,展示了如何以不命名的方式實現一個基礎類。盡管這比一個已命名的類更加簡潔,但對于僅有一個方法的類,即便是匿名類似乎也有點冗余和笨重。Lambda表達式可以讓你更簡潔的描述一個“單方法(single-method)”類的實例。
這部分涵蓋了以下主題:
一、理想的Lambda表達式使用情況
方案1:創建一個用于搜索滿足某一特征的成員的方法
方案2:創建一個更通用的搜索方法
方案3:在本地類(Local Class)中指定一段搜索條件代碼
方案4:在匿名類中指定一段搜索條件代碼
方案5:通過Lambda表達式指定一段搜索條件代碼
方案6:使用標準函數接口(Functionl Interfaces)的Lambda表達式
方案7:在你的應用中全面使用Lambda表達式
方案8:更廣泛的使用通用化
方案9:使用以Lambda表達式作為參數的聚合操作
二、GUI應用中的Lambda表達式
三、Lambda表達式語法
四、在封閉域中訪問局部變量
五、目標類型
? ? ? ? 目標類型和方法參數
六、序列化
一、理想的Lambda表達式使用情況
假設你正在開發一個社交網絡的應用。你希望開發一個可以讓管理員執行某種行為的功能,比如發送消息給那些滿足某種條件的用戶。下面的表格描述了這種使用情況的細節。
| 名稱? ? ? ? ? | 給選中的用戶執行動作 |
| 主要執行人 | Administrator |
| 先決條件? ? ? ? ? ? ? | Administrator已經登錄 |
| 后驗條件? ?? | 動作只對滿足特定條件的用戶生效 |
| 成功的關鍵 | |
| 擴展 | 管理員可以在指定的動作執行前或提交按鈕點擊前預覽到所有符合條件的用戶。 |
| 出現頻率 | 一天很多次 |
假設你社交應用中的成員可以用下面的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> 的實例中。
針對上述使用情形,這部分(譯者注:指本章內容)以一個樸實的解決方案作為開始。以本地類和匿名類作為改進方案,最后以一種有效的、簡潔的使用Lambda表達式的方案為結尾。更多信息請查看:RosterTest
方案1:創建一個用于搜索滿足某一特征的成員的方法
一個最傻瓜式的方案是創建多個方法,每個方法用于搜索符合一種特征的用戶,比如性別或者年齡。下面的方法會打印出大于某個指定年齡的所有成員。
public static void printPersonsOlderThan(List<Person> roster, int age) {for (Person p : roster) {if (p.getAge() >= age) {p.printPerson();}} }注意:List 是一個有序的Collection,Collection是一個可以把許多元素放入到單獨的單元中的對象。Collection用于存儲、檢索、操作、傳遞集合數據。Collection相關的更多信息,請參考Collection系列。
這種解決方案可能使你的應用非常脆弱,由于某些修改的加入(比如新的數據類型)就可能會使應用無法正常工作。假設你升級了你的應用,并改變了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:在本地類(Local Class)中指定一段搜索條件代碼
下面的方法打印出滿足指定搜索條件的所有成員。
public static void printPersons(List<Person> roster, CheckPerson tester) {for (Person p : roster) {if (tester.test(p)) {p.printPerson();}} }這個方法通過調用參數tester的test()方法校驗roster中的每一個Person對象不論它是否滿足已經被指定在CheckPerson中的搜索條件。如果tester.test()返回一個true,那么Person對象就會調用printPerson。
為了指定一個搜索條件,你需要實現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;} }為了使用這個類,你創建了它的新的實例,并且調用了printPerson方法。
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?接口是一個functional interface(函數接口),函數接口是僅有一個抽象方法的任意接口。(一個函數接口可能包含一個或多個default方法(譯者注:default方法是java8 加入的默認方法,它是一個在接口中存在的非抽象方法)或靜態方法)因為函數接口僅有一個抽象方法,你可以在實現這個接口的時候省略方法名。為了做到這一點,你是用了一個Lambda表達式,而不是一個匿名內部類表達式,正如下面高亮部分所示:
printPersons(roster,(Person p) -> p.getGender() == Person.Sex.MALE&& p.getAge() >= 18&& p.getAge() <= 25 );參考《Java8————Lambda表達式(一)》來獲得更多關于如何定義Lambda表達式的信息。
你可以使用標準的函數接口來取代CheckPerson的實例,這樣會大大將降低所需的代碼量。
方案6:使用標準函數接口(Functionl Interfaces)的Lambda表達式
重新思考一下CheckPerson?接口:
interface CheckPerson {boolean test(Person p); }這是一個非常簡單的接口。它是一個函數接口,因為僅僅含有一個抽象方法。這個方法接收一個參數并且返回一個Boolean值。這個接口太簡單了以至于有點不值得你在程序中去定義它。因此,JDK定義了許多標準的函數接口,你可以在java.util.function.包下找到他們。
例如,你可以使用Predicate<T>接口來取代CheckPerson。這個接口包含一個方法:boolean test(T t):
interface Predicate<T> {boolean test(T t); }Predicate<T>?接口是一個泛型接口的例子。(想獲得更多關于泛型的信息,參考?Generics (Updated)?課程)泛型類型(比如泛型接口)會在尖括號(<>)中定義一個或多個類型參數。上述接口包含了一個類型參數 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中調用printPersons?來獲取符合義務兵役的成員是一樣的:
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();}} }這個方法會校驗roster中的每個Person實例,不論它是否滿足在Predicate類型參數tester中指定的條件。如果Person實例滿足條件,那么printPersron?方法將會被調用。
除了調用printPerson方法,你可以為那些滿足條件的Person對象指定不同的行為。你可以使用Lambda表達式來指定這個行為。假設你想要一個和printPerson類似的Lambda表達式,一個只接收一個參數,且沒有返回值的Lambda表達式。記住,要想使用Lambda表達式,必須實現一個函數接口。為此,你需要一個僅包含一個抽象方法的函數接口,這個方法可以接受一個Person實例作為對象,并且沒有返回值。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);}} }結果,下面的方法調用和你在方案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:更廣泛的使用通用化方案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的數據源。注意roster集合,這個List類型的集合,其本身也是一個Iterable類型的對象。
2、篩選出與Predicate類型對象tester相匹配的對象。在這個例子中,Predicate對象是一個用來指定哪些成員符合義務兵役條件的Lambda表達式。
3、將每一個篩選出的對象映射成一個由Function對象mapper指定的值。在這個例子中,Function對象是一個返回成員電子郵件地址的Lambda表達式。
4、對每個已經映射的對象執行一個由Consumer對象block指定的動作。在這個例子中,Consumer對象是一個打印由Function對象返回的電子郵件地址字符串的Lambda表達式。
你可以用聚合操作來代替每一個動作。
方案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?操作與其對應的聚合操作。
| 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?Functionobject | <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都是聚合操作。聚合操作從一個stream中處理每一個元素,而不是直接在集合中(這也是為什么在這個例子中第一個調用的方法是stream())Stream是元素的一個序列。與集合不同,它不是一個數據結構,并不存儲元素。它通過一個管道從一個數據源中(如Collection)搬運每一個值。pipeline?是Stream操作的一個序列,就像上面例子中的filter-map-forEach。另外,聚合操作可以接收Lambda表達式作為參數,允許你自定義他們的表現行為。
二、GUI應用中的Lambda表達式(略)
三、Lambda表達式語法
參考《Java8————Lambda表達式(一)》
四、在封閉域中訪問局部變量
如同大多數本地和匿名類,Lambda表達式可以捕捉變量,它們有相同的訪問途徑訪問封閉域中的局部變量。然而,與本地和匿名類不同的是,Lambda表達式不會有任何覆蓋問題(shadowing issues)(參考Shadowing)。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 ASystem.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如果你在myConsumer中的聲明里用 y 來代替參數 x ,那么編譯器會產生一個錯誤。
Consumer<Integer> myConsumer = (x) -> {// ... }編譯器會產生一個錯誤“variable x is already defined in method methodInFirstLevel(int)”因為Lambda表達式不會引入一個新的級別的域。因此,你可以直接訪問封閉域中的屬性,方法,局部變量。例如,Lambda表達式可以直接訪問methodInFirstLevel方法的參數 x 。要訪問內部類中的變量,需使用this關鍵字。在這個例子中,this.x 引用的是成員變量:FirstLevel.x
然而,和本地及匿名類一樣,Lambda表達式只能訪問由final或Effectively final修飾的局部變量和參數。例如,假設你定義了methodInFirstLevel?方法后立即增加了如下賦值語句:
void methodInFirstLevel(int x) {x = 99;// ... }因為這個賦值語句,變量FirstLevel.x 就不再是一個effectively final了。結果,Java編譯器會在Lambda表達式myConsumer嘗試訪問FirstLevel.x變量時產生一個錯誤:"local variables referenced from a lambda expression must be final or effectively final" 。
System.out.println("x = " + x);五、目標類型
你如何決定Lambda表達式的類型呢?再次調用Lambda表達式來篩選年齡在15到25歲之間的男性成員:
p -> p.getGender() == Person.Sex.MALE&& p.getAge() >= 18&& p.getAge() <= 25這個Lambda表達式在下面兩個方法中使用到:
1、public static void printPersons(List<Person> roster, CheckPerson tester) 在方案3中。
2、public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) 在方案6中。
當Java運行時調用printPersons方法,期望的類型是CheckPerson,因此Lambda表達式就對應這個類型。然而,當Java運行時調用printPersonsWithPredicate方法時,它期望的類型是Predicate<Person>,那么Lambda表達式就對應這個類型。這些方法期望的數據類型就叫做目標類型。為了決定Lambda表達式的類型,Java編譯器會根據Lambda表達式使用處的上下文或環境來使用目標類型。由此你只需使用Lambda表達式即可,Java編譯器會幫你決定它的目標類型。
1、變量聲明
2、賦值
3、返回語句
4、數組初始化
5、方法或構造器參數
6、Lambda表達式體
7、條件表達式,?:(譯者注:即三目運算符)
8、計算表達式(Cast Expression)
目標類型和方法參數
對于方法參數,Java編譯器使用兩種語言特性來決定目標類型:重載解決和類型參數推斷
考慮如下兩個函數接口:
public interface Runnable {void run(); }public interface Callable<V> {V call(); }方法Runnable.run?沒有返回值,Callable<V>.call有返回值。
假設你有一個重載的方法如下:
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表達式強烈不推薦!
?
?
總結
以上是生活随笔為你收集整理的Java8————Lambda表达式(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用Aria2高速下载网盘文件
- 下一篇: Java 多线程 —— Reentran