《Effective Java 3rd》3分鐘速成:(42) Lambda表達式

前言:

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 中以前並不實用。
《Effective Java 3rd》3分鐘速成:(42) Lambda表達式


分享到:


相關文章: