Java SE 8在6月13的版本中已經完全了全部的功能。在這些新的功能中,lambda表達式是推動該版本發布 的最重要新特性。因為Java第一次嘗試引入函數式編程的相關內容。社區對於lambda表達式也期待已久。 Lambda表達式的相關內容在JSR 335中定義,本文的內容基於最新的規范和JDK 8 Build b94。 開發環境使用 的是Eclipse。
Lambda表達式
要理解lambda表達式,首先要了解的是函數式接口(functional interface)。簡單來說,函數式接口是只包含一個抽象方法的接口。比如Java標准庫中的 java.lang.Runnable和java.util.Comparator都是典型的函數式接口。對於函數式接口,除了可以使用Java中 標准的方法來創建實現對象之外,還可以使用lambda表達式來創建實現對象。這可以在很大程度上簡化代碼的 實現。在使用lambda表達式時,只需要提供形式參數和方法體。由於函數式接口只有一個抽象方法,所以通過 lambda表達式聲明的方法體就肯定是這個唯一的抽象方法的實現,而且形式參數的類型可以根據方法的類型聲 明進行自動推斷。
以Runnable接口為例來進行說明,傳統的創建一個線程並運行的方式如下所示:
public void runThread() { new Thread(new Runnable() { public void run() { System.out.println("Run!"); } }).start(); }
在上面的代碼中,首先需要創建一個匿名內部類實現Runnable接口,還需要實現接口中的run方法。 如果使用lambda表達式來完成同樣的功能,得到的代碼非常簡潔,如下面所示:
public void runThreadUseLambda() { new Thread(() -> { System.out.println("Run!"); }).start(); }
相對於傳統的方式,lambda表達式在兩個方面進行了簡化:首先是Runnable接口的聲明,這可以通 過對上下文環境進行推斷來得出;其次是對run方法的實現,因為函數式接口中只包含一個需要實現的方法。
Lambda表達式的聲明方式比較簡單,由形式參數和方法體兩部分組成,中間通過“->”分隔。形式 參數不需要包含類型聲明,可以進行自動推斷。當然在某些情況下,形式參數的類型聲明是不可少的。方法體 則可以是簡單的表達式或代碼塊。
比如把一個整數列表按照降序排列可以用下面的代碼來簡潔實現:
Collections.sort(list, (x, y) -> y - x);
Lambda表達式“(x, y) -> y - x“ 實現了java.util.Comparator接口。
在Java SE 8之前的標准庫中包含的函數式接口並不多。Java SE 8增加了java.util.function包,裡面都是可以在開發中使用的函數式接口。開發人員也可以創建新的函數式 接口。最好在接口上使用注解@FunctionalInterface進行聲明,以免團隊的其他人員錯誤地往接口中添加新的 方法。
下面的代碼使用函數式接口java.util.function.Function實現的對列表進行map操作的方法。 從代碼中可以看到,如果盡可能的使用函數式接口,則代碼使用起來會非常簡潔。
public class CollectionUtils { public static List map(List input, Function processor) { ArrayList result = new ArrayList(); for (T obj : input) { result.add(processor.apply(obj)); } return result; } public static void main(String[] args) { List input = Arrays.asList(new String[] {"apple", "orange", "pear"}); List lengths = CollectionUtils.map(input, (String v) -> v.length()); List uppercases = CollectionUtils.map(input, (String v) -> v.toUpperCase()); } }
方法和構造方法引用
方法引用可以在不調用某個方法的情況下引用一個方法。構造方法引用可以在 不創建對象的情況下引用一個構造方法。方法引用是另外一種實現函數式接口的方法。在某些情況下,方法引 用可以進一步簡化代碼。比如下面的代碼中,第一個forEach方法調用使用的是lambda表達式,第二個使用的 是方法引用。兩者作用相同,不過使用方法引用的做法更加簡潔。
List input = Arrays.asList (new String[] {"apple", "orange", "pear"}); input.forEach((v) -> System.out.println(v)); input.forEach(System.out::println);
構造方法可以通過名稱“new”來進行引用,如下面的代碼所示:
List dateValues = Arrays.asList(new Long[] {0L, 1000L}); List dates = CollectionUtils.map(dateValues, Date::new);
接口的默認方法
Java開發中所推薦的實踐是面向接口而不是實現來編程。接口作為不同組件之間的 契約,使得接口的實現可以不斷地演化。不過接口本身的演化則比較困難。當接口發生變化時,該接口的所有 實現類都需要做出相應的修改。如果在新版本中對接口進行了修改,會導致早期版本的代碼無法運行。Java對 於接口更新的限制過於嚴格。在代碼演化的過程中,一般所遵循的原則是不刪除或修改已有的功能,而是添加 新的功能作為替代。已有代碼可以繼續使用原有的功能,而新的代碼則可以使用新的功能。但是這種更新方式 對於接口是不適用的,因為往一個接口中添加新的方法也會導致已有代碼無法運行。
接口的默認方法 的主要目標之一是解決接口的演化問題。當往一個接口中添加新的方法時,可以提供該方法的默認實現。對於 已有的接口使用者來說,代碼可以繼續運行。新的代碼則可以使用該方法,也可以覆寫默認的實現。
考慮下面的一個簡單的進行貨幣轉換的接口。該接口的實現方式可能是調用第三方提供的服務來完成實際的轉 換操作。
public interface CurrencyConverter { BigDecimal convert(Currency from, Currency to, BigDecimal amount); }
該接口在開發出來之後,在應用中得到了使用。在後續的版本更新中,第三方服務提供了新的批量 處理的功能,允許在一次請求中同時轉換多個數值。最直接的做法是在原有的接口中添加一個新的方法來支持 批量處理,不過這樣會造成已有的代碼無法運行。而默認方法則可以很好的解決這個問題。使用默認方法的新 接口如下所示。
public interface CurrencyConverter { BigDecimal convert(Currency from, Currency to, BigDecimal amount); default List convert(Currency from, Currency to, List amounts) { List result = new ArrayList(); for (BigDecimal amount : amounts) { result.add(convert(from, to, amount)); } return result; } }
新添加的方法使用default關鍵詞來修飾,並可以有自己的方法體。
默認方法的另外一個作用是實 現行為的多繼承。Java語言只允許類之間的單繼承關系,但是一個類可以實現多個接口。在默認方法引入之後 ,接口中不僅可以包含變量和方法聲明,還可以包含方法體,也就是行為。通過實現多個接口,一個Java類實 際上可以獲得來自不同接口的行為。這種功能類似於JavaScript等其他語言中可見的“混入類”(mixin)。 實際上,Java中一直存在“常量接口(Constant Interface)”的用法。常量接口中只包含常量的聲明。通過 實現這樣的接口,就可以直接引用這些常量。通過默認方法,可以創建出類似的幫助接口,即接口中包含的都 是通過默認方法實現的幫助方法。比如創建一個StringUtils接口包含各種與字符串操作相關的默認方法。通 過繼承該接口就可以直接使用這些方法。
Java SE 8標准庫已經使用默認方法來對集合類中的接口進行 更新。比如java.util.Collection接口中新增的默認方法removeIf可以刪除集合中滿足某些條件的元素。還有 java.lang.Iterable接口中新增的默認方法forEach可以遍歷集合中的元素,並執行一些操作。這些新增的默 認方法大多使用了java.util.function包中的函數式接口,因此可以使用lambda表達式來非常簡潔的進行操作 。
Lambda表達式是Java SE 8在提高開發人員生產效率上的一個重大改進。通過語法上的改進,可以減 少開發人員需要編寫和維護的代碼數量。