Lambda 允許我們定義匿名方法(即那個 Lambda 表達式,或叫閉包),作為一個功能性接口的實例。如果你不想把一個 Lambda 表達式寫得過大,那么你可以把表達式的內容分離出來寫在一個方法中,然后在放置 Lambda 表達式的位置上填上對那個方法的引用。
方法引用也應看作是一個 Lambda 表達式,所以它也需要一個明確的目標類型充當功能性接口的實例。簡單說就是被引用的方法要與功能接口的 SAM(Single Abstract Method) 參數、返回類型相匹配。方法引用的引入避免了 Lambda 寫復雜了可讀性的問題,也使得邏輯更清晰。
為了應對方法引用這一概念, JDK8 又重新借用了 C++ 的那個 “::” 域操作符,全稱為作用域解析操作符。
上面的表述也許不好明白,我看官方的那份 State of the Lambda 也覺得不怎么容易理解,特別是它舉了那個例子很難讓人望文生意。我用個自己寫的例子來說明一下吧。
目前的 Eclipse-JDK8 版還不能支持方法引用的特性,幸好就是在昨天正式版的 NetBeans IDE 7.4 對 JDK8 有了較好的支持,所以在 NetBeans 7.4 中寫測試代碼。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package testjdk8; /** * * @author Unmi */ public class TestJdk8 { public static void main(String[] args) { //使用 Lambda 表達式,輸出: 16: send email start((id, task) -> id + ": " + task); //或者 Machine m1 = (id, task) -> id + ": " + task; m1.doSomething( 16 , "send email" ); //使用方法引用,輸出: Hello 16: send email start(TestJdk8::hello); //或者 Machine m2 = TestJdk8::hello; m2.doSomething( 16 , "send email" ); } private static void start(Machine machine){ String result = machine.doSomething( 16 , "send email" ); System.out.println(result); } public static String hello( int id, String task){ return "Hello " + id + ": " + task; } } @FunctionalInterface interface Machine { public String doSomething( int id, String task); } |
說明:
1. Machine 是一個功能性接口,它只有一個抽象方法
2. start(Machine machine) 方法為 Lambda 表達式提供了一個上下文,表明它期盼接收一個 Machine 的功能性接口類型
3. start((id, task) -> id + ": " + task), 是傳遞了一個 Lambda 表達式給 start() 方法
4. start(TestJdk8::hello) 是把指向 TestJdk8::hello 方法的引用傳遞給了 start() 方法,這里可以理解 hello() 方法是 Lambda 表達式的另一種表現形式。
對應一下兩個 start() 方法調用的參數,Lambda 表達式的參數列表 (id, task) 與 hello 方法的參數 (int id, String task) 是一致的,返回值類型也是一致的。
想像一下如果一個 Lambda 表達式的代碼量很大,全部擠在一起作為 start() 方法的參數部分,混亂也不太方便于單步調試。所以可以把 Lambda 的實現挪出來放在一個單獨的方法中,在使用處只放置一個對該方法的引用即可。借助于方法引用,JDK8 把方法與 Lambda 表達式巧妙的結合了起來,直接的說 Lambda 表達就是一個方法,它用自己的方法列表和返回值。
那么符合什么條件的方法可以作為 Lambda 表達式來用呢?答:方法簽名與功能性接口的 SAM 一致即可。比如,可以進行下面的賦值:
1 2 | Consumer<Integer> b1 = System::exit //void exit(int status) 與 Consumer 的 SAM void accept(T t) 相匹配 Runnable r = MyProgram::main; //void main(String... args) 與 run() 方法能配上對 |
有些什么樣子的方法引用:
- 靜態方法 (ClassName::methName)
- 對象的實例方法 (instanceRef::methName)
- 對象的super 方法 (super::methName)
- 類型的實例方法 (ClassName::methName, 引用時和靜態方法是一樣的,但這里的 methName 是個實例方法)
- 類的構造方法 (ClassName::new)
- 數組的構造方法 (TypeName[]::new)
第 1 條,靜態方法以 ClassName 為作用域好理解,第 4 條中實例方法也可以用 ClassName::methName 的方式去引用,那么這里又有個約定了:如果實例方法用類型來引用的時候,那么調用時第一個參數將作為該引用方法的接收者,其余參數依次作為引用方法的參 數。舉個例子:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package testjdk8; import java.util.function.Function; /** * * @author Unmi */ public class TestJdk8 { public static void main(String[] args) { Function<String, String> upperfier = String::toUpperCase; System.out.println(upperfier.apply( "Hello" )); //HELLO Machine m = TestJdk8::hello; //hello 是實例方法 TestJdk8 test = new TestJdk8(); //test 作為 hello 方法的接收者,"Unmi" 作為 task 參數 System.out.println(m.doSomething(test, "Unmi" )); //Hello Unmi } public String hello(String task){ return "Hello " + task; } } @FunctionalInterface interface Machine { public String doSomething(TestJdk8 test, String task); } |
上面的代碼應該能有助于理解實例方法用類型來引用,如果引用的是泛型方法,類型寫在 :: 之前。
同樣當然對于第 2 條,引用實例方法時,SAM 的第一個參數也作為接收者,其作參數依次填充過去。
第 5 條,類的構造方法要用類型去引用,new 相當一個返回當前類型實例的實例方法,所以
1 2 | SocketImplFactory factory = MySocketImpl:: new ; SocketImpl socketImpl = factory.createSocketImpl(); |
數組是種類型,可以認為數組的構造方法是只接受一個整形參數,所以能這樣引用數組的構造方法:
1 2 | IntFunction< int []> arrayMaker = int []:: new ; int [] array = arrayMaker.apply( 10 ); // creates an int[10] |