語法說明
一個lambda表達式由如下幾個部分組成:
1. 在圓括號中以逗號分隔的形參列表。在CheckPerson.test方法中包含一個參數p,代表了一個Person類的實例。注意:lambda表達式中的參數的類型是可以省略的;此外,如果只有一個參數的話連括號也是可以省略的。比如上一節曾提到的代碼:
1
2
3
|
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 |
2. 箭頭符號:->。用來分隔參數和函數體。
3. 函數體。由一個表達式或代碼塊組成。在上一節的例子中使用了這樣的表達式:
1
2
3
|
p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 |
如果使用的是表達式,java運行時會計算并返回表達式的值。另外,還可以選擇在代碼塊中使用return語句:
1
2
3
4
5
|
p -> { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 ; } |
不過return語句并不是表達式。在lambda表達式中需要將語句用花括號括起來,然而卻沒有必要在只是調用一個返回值為空的方法時也用花括號括起來,所以如下的寫法也是正確的:
1
|
email -> System.out.println(email) |
lambda表達式和方法的聲明看起來有很多類似的地方。所以也可以把lambda表達式視為匿名方法,也就是沒有定義名字的方法。
以上提到的lambda表達式都是只使用了一個參數作為形參的表達式。下面的實例類,Caulator,演示了如何使用多個參數作為形參:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.zhyea.zytools; 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表達式定義了兩個算數操作:addition和subtraction。執行程序會打印如下內容:
1
2
|
40 + 2 = 42 20 - 10 = 10 |
訪問外部類的局部變量
類似于局部類或匿名類,lambda表達式也可以訪問外部類的局部變量。不同的是,使用lambda表達式時無需考慮覆蓋之類的問題。lambda表達式只是一個詞法上的概念,這意味著它不需要從超類中繼承任何名稱,也不會引入新的作用域。也就是說,在lambda表達式中的聲明和在它的外部環境中的聲明意義是一樣的。在下面的例子中對此作了演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.zhyea.zytools; import java.util.function.Consumer; public class LambdaScopeTest { public int x = 0 ; class FirstLevel { public int x = 1 ; void methodInFirstLevel( int x) { //如下的語句會導致編譯器在statement A處報錯“local variables referenced from a lambda expression must be final or effectively final” // 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 ); } } |
這段代碼會輸出如下內容:
1
2
3
4
|
x = 23 y = 23 this.x = 1 LambdaScopeTest.this.x = 0 |
如果使用示例中lambda表達式myConsumer中的參數y替換為x,編譯器就會報錯:
1
2
3
|
Consumer<integer> myConsumer = (x) ->{ // .... }; |
編譯器報錯信息是:“variable x is already defined in method methodInFirstLevel(int)”,就是說在方法methodInFirstLevel中已經定義了變量x。報錯是因為lambda表達式不會引入新的作用域。也因此呢,可以在lambda表達式中直接訪問外部類的域字段、方法以及形參。在這個例子中,lambda表達式myConsumer直接訪問了方法methodInFirstLevel的形參x。而訪問外部類的成員時也是直接使用this關鍵字。在這個例子中this.x指的就是FirstLevel.x。
然而,和局部類或匿名類一樣,lambda表達式也只能訪問局部變量或外部被聲明為final(或等同于final)的成員。比如,我們將示例代碼methodInFirstLevel方法中“x=99”前面的注釋去掉:
1
2
3
4
5
6
7
8
|
//如下的語句會導致編譯器在statement A處報錯“local variables referenced from a lambda expression must be final or effectively final” 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); }; |
因為在這段語句中修改了參數x的值,使得methodInFirstLevel的參數x不可以再被視為final式的。因此java編譯器就會在lambda表達式訪問局部變量x的地方報出類似“local variables referenced from a lambda expression must be final or effectively final”這樣的錯誤。
目標類型
該如何判斷lambda表達式的類型呢。再來看一下篩選適齡服兵役人員的代碼:
1
2
3
|
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 |
這段代碼在兩處用到過:
public static void printPersons(List<Person> roster, CheckPerson tester) —— 方案三
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) —— 方案六
在調用printPersons方法時,這個方法期望一個CheckPerson 類型的參數,此時上面的那個表達式就是一個CheckPerson 類型的表達式。在調用printPersonsWithPredicate方法時,期望一個Predicate<Person>類型的參數,此時同樣的表達式就是Predicate<Person>類型的。像這樣子的,由方法期望的類型來決定的類型就叫做目標類型(其實我覺得scala中的類型推斷用在這里更合適)。java編譯器就是通過目標類型的上下文語境或者發現lambda表達式時的位置來判斷lambda表達式的類型的。這也就意味著只能在Java編譯器可以推斷出類型的位置使用Lambda表達式:
-
變量聲明;
-
賦值;
-
返回語句;
-
數組初始化;
-
方法或者構造器參數;
-
lambda表達式方法體;
-
條件表達式(?:);
-
拋出異常時。
目標類型和方法參數
對于方法參數,Java編譯器還需要依賴兩個語言特性來決定目標類型:重載解析和類型參數推斷。
看一下下面的這兩個函數式接口( java.lang.Runnable and java.util.concurrent.Callable<V>):
1
2
3
4
5
6
7
|
public interface Runnable { void run(); } public interface Callable<v> { V call(); } |
Runnable.run()方法沒有返回值,而Callable.call()方法有。
假設我們像下面這樣重載了invoke方法:
1
2
3
4
5
6
7
|
void invoke(Runnable r) { r.run(); } <t> T invoke(Callable<t> c) { return c.call(); } |
那么在下面的語句中將會調用哪個方法呢:
String s = invoke(() -> "done");
調用的是invoke(Callable<T>),因為這個方法有返回值,而invoke(Runnable<T>)沒有返回值。在這種情況下lambda表達式(() -> “done”)的類型是Callable<T>。
序列化
如果一個lambda表達式的目標類型還有它調用的參數的類型都是可序列化的,那么lambda表達式也是可序列化的。然而就像內部類一樣,強烈不建議對lambda表達式進行序列化。