標籤:目標 開發 繼承 set 傳遞 接受 check 其他屬性 合并
匿名內部類的一個問題是:當一個匿名內部類的實現非常簡單,比如說介面
只有一個抽象函數,那麼匿名內部類的文法有點笨拙且不清晰。我們經常會有傳遞一個函數作為參數給另一個函數的實際需求,比如當點擊一個按鈕時,我們需要給按鈕對象設定按鈕響應函數。lambda運算式就可以把函數當做函數的參數,代碼(函數)當做資料(形參),這種特性滿足上述需求。當要實現只有一個抽象函數的介面時,使用lambda運算式能夠更靈活。
使用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>的執行個體中。
我們先使用一個簡單的方法來實現這個用例,再通過使用本地類、匿名內部類實現,最終通過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:在本地類中設定特定的搜尋條件
下面的方法可以列印出符合搜尋條件的所有使用者資訊
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執行個體。
通過實現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));
該方法的調用只要執行了下面動作:
- 從集合中擷取對象,在這個例子中它是包換
Person執行個體的roster集合。roster是一個List類型,同時也是一個Iterable類型。
- 過濾符合
Predicate資料類型的tester的對象。在這個例子中,Predicate對象是一個指定了符合搜尋條件的lambda運算式。
- 使用
Function類型的mapper映射每個符合過濾條件的對象。在這個例子中,Function對象時要給返回使用者的郵箱地址。
- 對每個映射到的對象執行一個在
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<E> stream() |
| 過濾符合Predicate對象(lambda運算式)的執行個體 |
Stream<T> filter(Predicate<? super T> predicate) |
| 使用Function對象映射符合過濾標準的對象到一個值 |
<R> Stream<R> map(Function<? super T,? extends R> mapper) |
| 執行Consumer對象(lambda運算式)設定的動作 |
void forEach(Consumer<? super T> action) |
filter,map和forEach是彙總操作。彙總操作是從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 = 4220 - 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 = 23y = 23this.x = 1LambdaScopeTest.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<Person> roster, CheckPerson tester)
- public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
當java運行時調用方法printPersons時,它期望一個CheckPerson類型的資料,因此lambda運算式就是這種類型。當java運行時調用方法printPersonsWithPredicate時,它期望一個Predicate<Person>類型的資料,因此lambda運算式就是這樣一個類型。這些方法期望的資料類型就叫目標類型。為了確定lambda運算式的類型,java編譯器會在lambda運算式的的上下文中判斷它的目標類型。只有java編譯器可推測出來了目標類型,lambda運算式才可以被執行。
目標類型和函數參數
對於函數參數,java編譯器可以確定目標類型通過兩種其他語言特性:重載解析和型別參數推斷。看下面兩個函數式介面( java.lang.Runnable and java.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>。
最後
感謝閱讀,有興趣可以關注公眾帳號擷取最新推送文章。
Java 8 Lambda運算式一看就會