一、為什么要引入lambda表達式
????????lambda 表達式是一個可傳遞的代碼塊 , 可以在以后執行一次或多次 。
? ? ? ? 在介紹lambda表達式之前,我們看一下,以前,我們對于一個問題的通常寫法。
????????
? ? ? ? 假設你已經了解了如何按指定時間間隔完成工作,當然不了解也沒關系,只是作為例子說明。 將這個工作放在一個 ActionListener 的 actionPerformed 方法中 :
class Worker implements ActionListener
{public void actionPerformed(ActionEvent event){// do some work}
}
????????想要反復執行這個代碼時, 可以構造 Worker 類的一個實例 。 然后把這個實例提交到一個 Timer 對象 。 這里的重點是 actionPerformed 方法包含希望以后執行的代碼 。
????????或者可以考慮如何用一個定制比較器完成排序。 如果想按長度而不是默認的字典順序對
字符串排序 , 可以向 sort 方法傳人一個 Comparator 對象 :
class LengthComparator implements Comparator<String>
{public int compare(String first, String second){return first.length() - second.length();}
}
????????這兩個例子有一些共同點, 都是將一個代碼塊傳遞到某個對象 ( 一個定時器 , 或者一個 sort 方法)。這個代碼塊會在將來某個時間調用。
????????到目前為止, 在 Java 中傳遞一個代碼段并不容易 , 不能直接傳遞代碼段 。 ? Java 是一種面
向對象語言 , 所以必須構造一個對象 , 這個對象的類需要有一個方法能包含所需的代碼。
????????在其他語言中, 可以直接處理代碼塊 。 Java 設計者很長時間以來一直拒絕增加這個特性 。 畢竟, Java 的強大之處就在于其簡單性和一致性 。 如果只要一個特性能夠讓代碼稍簡潔一些 , 就把這個特性增加到語言中, 這個語言很快就會變得一團糟 , 無法管理 。 不過 , 在另外那些 語言中, 并不只是創建線程或注冊按鈕點擊事件處理器更容易 ; 它們的大部分 API 都更簡單 、 更一致而且更強大。 在 Java 中 , 也可以編寫類似的 API 利用類對象實現特定的功能 , 不過這種 API 使用可能很不方便 。
二、lambda表達式的語法
????????再來考慮上面討論的排序例子。 我們傳入代碼來檢查一個字符串是否比另一個字符串短。 這里要計算 :
????????first. length()? - second . length()
????????first 和 second 是什么 ? 它們都是字符串 。 Java 是一種強類型語言 , 所以我們還要指定它 們的類型:
????????(String first, String second)
????????????????-> first.length()?- second.length()
????????這就是一個lambda表達式。lambda 表達式就是一個代碼塊 , 以及必須傳入代碼的變量規范。
????????你已經見過 Java 中的一種 lambda 表達式形式 : 參數 , 箭頭 ( - > ) 以及一個表達式 。 如果代碼要完成的計算無法放在一個表達式中, 就可以像寫方法一樣 , 把這些代碼放在 {} 中 , 并包含顯式的 return 語句 。 例如 :
????????即使 lambda 表達式沒有參數 , 仍然要提供空括號 , 就像無參數方法一樣 :
????????如果可以推導出一個 lambda 表達式的參數類型 , 則可以忽略其類型 。 例如 :
????????在這里, 編譯器可以推導出 first 和 second 必然是字符串 , 因為這個 lambda 表達式將賦給一個字符串比較器。
????????如果方法只有一個參數 , 而且這個參數的類型可以推導得出 , 那么甚至還可以省略小括號 :
????????無需指定 lambda 表達式的返回類型 。 lambda 表達式的返回類型總是會由上下文推導得出。 例如 , 下面的表達式:
????????可以在需要 int? 類型結果的上下文中使用 。
????????如果一個 lambda 表達式只在某些分支返回一個值 , 而在另外一些分支不返回值 , 這是不合法的。 例如 , ( int x ) - > { if ( x > = 0 ) return 1 ; } 就不合法 。
? ? ? ? 代碼示例:
package FunctionProm;import javax.swing.*;
import java.util.Arrays;
import java.util.Date;public class LambdaTest {public static void main(String[] args) {String[] planets = new String[] { "Mercury" , "Venus" , "Earth" , "Mars" , "Jupiter" , "Saturn" , "Uranus" , "Neptune" };System.out.println(Arrays.toString(planets));System.out. println("Sorted in dictionary order:") ;Arrays.sort(planets);System.out.println (Arrays.toString(planets));System.out . println ("Sorted by length:");Arrays.sort(planets, (first, second) -> first.length() - second.length()) ;System.out. println(Arrays.toString(planets));Timer t = new Timer(1000, event ->System.out.println ("The time is " + new Date()));t.start();// keep program running until user selects "0k"JOptionPane.showMessageDialog (null , "Quit program?");System.exit(0);}
}
三、函數式接口
????????前 面 已 經 討 論 過, Java 中 已 經 有 很 多 封 裝 代 碼 塊 的 接 口 , 如 ActionListener 或 Comparator。 lambda 表達式與這些接口是兼容的。
????????對于只有一個抽象方法的接口, 需要這種接口的對象時 , 就可以提供一個 lambda 表達式。 這種接口稱為函數式接口 ( functional interface ) 。
????????為了展示如何轉換為函數式接口, 下面考慮 Arrays . sort 方法 。 它的第二個參數需要一個 Comparator 實例 , Comparator 就是只有一個方法的接口 , 所以可以提供一個 lambda 表達式 :
????????在底層, Arrays . sort 方法會接收實現了 Comparator < String > 的某個類的對象 。 在這個對象上調用 compare 方法會執行這個 lambda 表達式的體。這些對象和類的管理完全取決于具體實現, 與使用傳統的內聯類相比,這樣可能要高效得多。最好把 lambda 表達式看作是一個函數,而不是一個對象, 另外要接受 lambda 表達式可以傳遞到函數式接口。
????????lambda 表達式可以轉換為接口 ,這一點讓 lambda 表達式很有吸引力 。 具體的語法很簡短。 下面再來看一個例子 :
????????與使用實現了 ActionListener 接口的類相比 , 這個代碼可讀性要好得多 。
????????實際上, 在 Java 中, 對 lambda 表達式所能做的也只是能轉換為函數式接口 。 在其他支 持函數字面量的程序設計語言中, 可以聲明函數類型 ( 如 ( String , String ) - > int ) 、 聲明這些類 型的變量, 還可以使用變量保存函數表達式 。 不過 , Java 設計者還是決定保持我們熟悉的接口概念, 沒有為Java 語言增加函數類型 。
????????Java API 在 java . util . fimction 包中定義了很多非常通用的函數式接口 。 其中一個接口BiFunction< T , U , R > 描述了參數類型為 T 和 U 而且返回類型為 R 的函數 。 可以把我們的字符串比較 lambda 表達式保存在這個類型的變量中 :
????????不過, 這對于排序并沒有幫助 。 沒有哪個 Arrays . sort 方法想要接收一個 BiFunction 。 如果你之前用過某種函數式程序設計語言, 可能會發現這很奇怪 。 不過 , 對于 Java 程序員而言, 這非常自然 。 類似 Comparator 的接口往往有一個特定的用途 , 而不只是提供一個有指定參數和返回類型的方法。 Java SE 8 沿襲了這種思路 。 想要用 lambda 表達式做某些處理 , 還是要謹記表達式的用途, 為它建立一個特定的函數式接口 。
????????java . util . function 包中有一個尤其有用的接口 Predicate :
????????ArrayList 類有一個 removelf 方法 , 它的參數就是一個 Predicate 。 這個接口專門用來傳遞 lambda 表達式 。 例如 , 下面的語句將從一個數組列表刪除所有 null 值 :
????????list. removelf ( e - > e = = null ) ;
????????四、方法引用
????????有時, 可能已經有現成的方法可以完成你想要傳遞到其他代碼的某個動作 。 例如 , 假設你希望只要出現一個定時器事件就打印這個事件對象。 當然 , 為此也可以調用 :
????????但是, 如果直接把 println 方法傳遞到 Timer 構造器就更好了 。 具體做法如下 :
????????表達式 System . out :: println 是一個方法引用 ( method reference ) , 它等價于 lambda 表達式 x 一 > System . out . println ( x ) 。
????????再來看一個例子, 假設你想對字符串排序 , 而不考慮字母的大小寫 。 可以傳遞以下方法表達式:
????????從這些例子可以看出, 要用:: 操作符分隔方法名與對象或類名 。 主要有 3 種情況 :
????????在前 2 種情況中 , 方法引用等價于提供方法參數的 lambda 表達式 。 前面已經提到 , System. out :: println 等價于 x - > System . out . println ( x)。 類似地, Math : : pow 等價于 ( x , y ) - >
Math . pow ( x , y)。
????????對于第 3 種情況 , 第 1 個參數會成為方法的目標 。 例如 , String : : compareToIgnoreCase 等
同于 ( x , y ) - > x . compareToIgnoreCase ( y ) 。
????????如果有多個同名的重栽方法, 編譯器就會嘗試從上下文中找出你指的那一個方法 。 例如, Math . max 方法有兩個版本 , 一個用于整數 , 另一個用于 double 值 。 選擇哪一個版 本取決于 Math :: max 轉換為哪個函數式接口的方法參數 。 類似于 lambda 表達式 , 方法引用不能獨立存在, 總是會轉換為 函數式接口 的實例 。
????????可以在方法引用中使用 this 參數 。 例如 , this :: equals 等同于 x - > this . equals ( x ) 。 使用
super 也是合法的 。 下面的方法表達式:
????????super: : instanceMethod
????????使用 this 作為目標 , 會調用給定方法的超類版本
為了展示這一點 , 下面給出一個假想的例子 :
????????TimedGreeter. greet 方法開始執行時 , 會構造一個 Timer , 它會在每次定時器滴答時執行 super:: greet 方法 。 這個方法會調用超類的 greet 方法 。
????????五、構造器引用
????????構造器引用與方法引用很類似, 只不過方法名為 new 。 例如 , Person : : new 是 Person 構造 器的一個引用。 哪一個構造器呢 ? 這取決于上下文 。 假設你有一個字符串列表 。 可以把它轉換為一個 Person 對象數組 , 為此要在各個字符串上調用構造器 , 調用如下 :
????????map 方法會為各個列表元素調用 Person ( String ) 構造器 。 如果有多個 Person 構造器 , 編譯器會選擇有一個 String 參數的構造器 , 因為它從上下文推導出這是在對一個字符串調用構造器。
????????可以用數組類型建立構造器引用。 例如 , int [] :: new 是一個構造器引用 , 它有一個參數 :即數組的長度。 這等價于 lambda 表達式 x - > new int [ x ]。
????????Java 有一個限制 , 無法構造泛型類型 T 的數組 。 數組構造器引用對于克服這個限制很有用。 表達式 new T [ n ] 會產生錯誤 , 因為這會改為 new Object [ n]。
????????對于開發類庫的人來說, 這是一個問題。 例如 , 假設我們需要一個 Person 對象數組。
????????Stream 接口有一個 toArray 方法可以返回 Object 數組 :
????????不過, 這并不讓人滿意 。 用戶希望得到一個 Person 引用數組 , 而不是 Object 引用數組 。流庫利用構造器引用解決了這個問題。 可以把 Person [ ] : : new 傳入 toArray 方法 :
????????toArray方法調用這個構造器來得到一個正確類型的數組 。 然后填充這個數組并返回 。
????????六、變量作用域
????????通常, 你可能希望能夠在 lambda 表達式中訪問外圍方法或類中的變量 。 考慮下面這個例子:
public static void repeatMessage(String text, int delay)
{ActionListener listener = event ->{System.out.println(text);Toolkit.getDefaultToolkit().beep():};new Timer(delay, listener).start();
}
????????來看這樣一個調用:
????????現在來看 lambda 表達式中的變量 text 。 注意這個變量并不是在這個 lambda 表達式中定義的。 實際上 , 這是 repeatMessage 方法的一個參數變量 。
????????如果再想想看, 這里好像會有問題 , 盡管不那么明顯 。 lambda 表達式的代碼可能會在 repeatMessage 調用返回很久以后才運行 , 而那時這個參數變量已經不存在了 。 如何保留 text
變量呢 ?
????????要了解到底會發生什么, 下面來鞏固我們對 lambda 表達式的理解 lambda 表達式有 3 個部分:
????????1 ) 一個代碼塊 ;
????????2 ) 參數 ;
????????3 ) 自由變量的值 , 這是指非參數而且不在代碼中定義的變量 。
????????在我們的例子中, 這個 lambda 表達式有 1 個自由變量 text 。 表示 lambda 表達式的數據結構必須存儲自由變量的值, 在這里就是字符串 " Hello " 。 我們說它被 lambda 表達式捕獲(下面來看具體的實現細節 。 例如 , 可以把一個 lambda 表達式轉換為包含一個方法的對象, 這樣自由變量的值就會復制到這個對象的實例變量中 。 )