前言:
Chapter 7. Lambdas and Streams
- 42. lambda表達式優於匿名類
42. lambda表達式優於匿名類
Java 8 引進的四個主要特性:
- 1、函數式接口:只有單個抽象方法的接口(對應Java8之前的函數類型);
- 2、lambda 表達式:一種 函數式接口的實例(對應Java8之前的函數對象)的方法;
- 3、方法引用:另外一種生成 函數式接口的實例(函數對象)的方法;
- 4、Stream:
備註:1&2&3:以便更容易地創建函數對象,4為處理數據元素序列提供類庫支持。
函數類型&函數對象(Java8之前的概念):
- 1、函數類型:以往,使用單一抽象方法的接口(或者很少使用抽象類)被用作函數類型。
- 2、函數對象:它們的實例(稱為函數對象)表示函數(functions)或行動(actions)。
函數對象&匿名類(Java8之前的做法):
自從 JDK 1.1 於 1997 年發佈以來,創建函數對象的主要手段就是匿名類(詳見第 24 條)。 下面是一段代碼片段,按照字符串長度順序對列表進行排序,使用匿名類創建排序的比較方法(強制排序順序):
<code>// Anonymous class instance as a function object - obsolete!Collections.sort(words, new Comparator<string>() { public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); }});/<string>/<code>
匿名類&函數對象&策略模式(Java8之前的做法):
匿名類適用於需要函數對象的經典面向對象設計模式,特別是策略模式[Gamma95]。 比較器接口表示排序的抽象策略; 上面的匿名類是排序字符串的具體策略。 然而,匿名類的冗長,使得 Java 中的函數式編程成為一種吸引人的前景。
函數式接口(Java8新增):
在 Java 8 中,語言形式化了這樣的概念,即使用單個抽象方法的接口是特別的,應該得到特別的對待。 這些接口現在稱為函數式接口。
lambda 表達式(Java8新增,替換匿名類):
並且該語言允許你使用 lambda 表達式或簡稱 lambda 來創建這些函數式接口的實例。
Lambdas 在功能上與匿名類相似,但更為簡潔。下面的代碼使用 lambdas 替換上面的匿名類。 樣板不見了,行為清晰明瞭:
<code>// Lambda expression as function object (replaces anonymous class)Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));/<code>
類型推斷:
請注意,代碼中不存在 lambda(Comparator <string>),其參數(s1 和 s2,都是 String 類型)及其返回值(int)的類型。 編譯器使用稱為類型推斷的過程從上下文中推導出這些類型。/<string>
類型推斷的規則:
- 1、在某些情況下,編譯器將無法確定類型,必須指定它們。
- 2、類型推斷的規則很複雜:他們在 JLS 中佔據了整個章節[JLS,18]。 很少有程序員詳細瞭解這些規則,但沒關係。 除非它們的存在使你的程序更清晰,否則省略所有 lambda 參數的類型。
- 3、如果編譯器生成一個錯誤,告訴你它不能推斷出 lambda 參數的類型,那麼指定它。 有時你可能不得不強制轉換返回值或整個 lambda 表達式,但這很少見。
類型推斷&泛型(建議捆綁使用):
- 1、關於類型推斷需要注意一點。 條目 26 告訴你不要使用原始類型,條目 29 告訴你偏好泛型類型,條目 30 告訴你偏向泛型方法。 當使用 lambda 表達式時,這個建議是非常重要的;
- 2、因為編譯器獲得了大部分允許它從泛型進行類型推斷的類型信息。 如果你沒有提供這些信息,編譯器將無法進行類型推斷,你必須在 lambdas 中手動指定類型,這將大大增加它們的冗餘度。
- 3、舉例來說,如果變量被聲明為原始類型 List 而不是參數化類型 List<string>,則上面的代碼片段將不會編譯。 /<string>
比較器構造方法代替 lambda:
<code>Collections.sort(words, comparingInt(String::length));/<code>
Java 8 中的 List 接口的 sort 方法,可以使片段變得更簡短:
<code>words.sort(comparingInt(String::length));/<code>
lambda改造Item34的計算器枚舉實現的示例:
<code>public enum Operation { PLUS ("+", (x, y) -> x + y), MINUS ("-", (x, y) -> x - y), TIMES ("*", (x, y) -> x * y), DIVIDE("/", (x, y) -> x / y); private final String symbol; private final DoubleBinaryOperator op; Operation(String symbol, DoubleBinaryOperator op) { this.symbol = symbol; this.op = op; } @Override public String toString() { return symbol; } public double apply(double x, double y) { return op.applyAsDouble(x, y); }}/<code>
示例代碼說明(DoubleBinaryOperator 接口):
- 請注意,我們使用表示枚舉常量行為的 lambdas 的 DoubleBinaryOperator 接口。
- 這是 java.util.function 中許多預定義的函數接口之一(詳見第 44 條)。 它表示一個函數,它接受兩個 double 類型參數並返回 double 類型的結果。
lambda的缺陷(可讀性)&與Operation 枚舉的對比:
- 1、看看基於 lambda 的 Operation 枚舉,你可能會認為常量特定的方法體已經失去了它們的用處,但事實並非如此。
- 2、與方法和類不同,lambda 沒有名稱和文檔;
- 3、如果計算不是自解釋的,或者超過幾行,則不要將其放入 lambda 表達式中。 一行代碼對於 lambda 說是理想的,三行代碼是合理的最大值。
- 4、如果違反這一規定,可能會嚴重損害程序的可讀性。
- 5、如果一個 lambda 很長或很難閱讀,要麼找到一種方法來簡化它或重構你的程序來消除它。
- 6、此外,傳遞給枚舉構造方法的參數在靜態上下文中進行評估。
- 7、因此,枚舉構造方法中的 lambda 表達式不能訪問枚舉的實例成員。 如果枚舉類型具有難以理解的常量特定行為,無法在幾行內實現,或者需要訪問實例屬性或方法,那麼常量特定的類主體仍然是行之有效的方法。
lambda &匿名類的對比:
- 1、同樣,你可能會認為匿名類在 lambda 時代已經過時了。 這更接近事實,但有些事情你可以用匿名類來做,而卻不能用 lambdas 做。
- 2、Lambda 僅限於函數式接口。
- 3、如果你想創建一個抽象類的實例,你可以使用匿名類來實現,但不能使用 lambda。
- 4、同樣,你可以使用匿名類來創建具有多個抽象方法的接口實例。
- 5、最後,lambda 不能獲得對自身的引用。 在 lambda 中,this 關鍵字引用封閉實例,這通常是你想要的。 在匿名類中,this 關鍵字引用匿名類實例。
- 6、如果你需要從其內部訪問函數對象,則必須使用匿名類。
- 7、Lambdas 與匿名類共享無法可靠地序列化和反序列化實現的屬性。因此,應該很少 (如果有的話) 序列化一個 lambda(或一個匿名類實例)。 如果有一個想要進行序列化的函數對象,比如一個 Comparator,那麼使用一個私有靜態嵌套類的實例(詳見第 24 條)。
總結:
- 1、從 Java 8 開始,lambda 是迄今為止表示小函數對象的最佳方式。
- 2、除非必須創建非函數式接口類型的實例,否則不要使用匿名類作為函數對象。
- 3、另外,請記住,lambda 表達式使代表小函數對象變得如此簡單,以至於它為功能性編程技術打開了一扇門,這些技術在 Java 中以前並不實用。
閱讀更多 編程家園 的文章