Item 42: Prefer lambdas to anonymous classes(λ 表達式優於匿名類)

Historically, interfaces (or, rarely, abstract classes) with a single abstract method were used as function types. Their instances, known as function objects, represent functions or actions. Since JDK 1.1 was released in 1997, the primary means of creating a function object was the anonymous class (Item 24). Here’s a code snippet to sort a list of strings in order of length, using an anonymous class to create the sort’s comparison function (which imposes the sort order):

在歷史上,帶有單個抽象方法的接口(或者抽象類,但這種情況很少)被用作函數類型。它們的實例(稱爲函數對象)表示函數或操作。自從 JDK 1.1 在 1997 年發佈以來,創建函數對象的主要方法就是匿名類(Item-24)。下面是一個按長度對字符串列表進行排序的代碼片段,使用一個匿名類來創建排序的比較函數(它強制執行排序順序):

// 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());
    }
});

Anonymous classes were adequate for the classic objected-oriented design patterns requiring function objects, notably the Strategy pattern [Gamma95]. The Comparator interface represents an abstract strategy for sorting; the anonymous class above is a concrete strategy for sorting strings. The verbosity of anonymous classes, however, made functional programming in Java an unappealing prospect.

匿名類對於需要函數對象的典型面向對象設計模式來說已經足夠了,尤其是策略模式 [Gamma95]。Comparator 接口表示排序的抽象策略;上述匿名類是對字符串排序的一種具體策略。然而,匿名類的冗長使函數式編程在 Java 中變得毫無吸引力。

In Java 8, the language formalized the notion that interfaces with a single abstract method are special and deserve special treatment. These interfaces are now known as functional interfaces, and the language allows you to create instances of these interfaces using lambda expressions, or lambdas for short. Lambdas are similar in function to anonymous classes, but far more concise. Here’s how the code snippet above looks with the anonymous class replaced by a lambda. The boilerplate is gone, and the behavior is clearly evident:

在 Java 8 中官方化了一個概念,即具有單個抽象方法的接口是特殊的,應該得到特殊處理。這些接口現在被稱爲函數式接口,允許使用 lambda 表達式創建這些接口的實例。Lambda 表達式在功能上類似於匿名類,但是更加簡潔。下面的代碼片段,匿名類被 lambda 表達式替換。已經沒有了原有刻板的樣子,意圖非常明顯:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));

Note that the types of the lambda (Comparator<String>), of its parameters (s1 and s2, both String), and of its return value (int) are not present in the code. The compiler deduces these types from context, using a process known as type inference. In some cases, the compiler won’t be able to determine the types, and you’ll have to specify them. The rules for type inference are complex: they take up an entire chapter in the JLS [JLS, 18]. Few programmers understand these rules in detail, but that’s OK. Omit the types of all lambda parameters unless their presence makes your program clearer. If the compiler generates an error telling you it can’t infer the type of a lambda parameter, then specify it. Sometimes you may have to cast the return value or the entire lambda expression, but this is rare.

注意,lambda 表達式(Comparator<String>)、它的參數(s1 和 s2,都是字符串)及其返回值(int)的類型在代碼中不存在。編譯器使用稱爲類型推斷的過程從上下文中推斷這些類型。在某些情況下,編譯器無法確定類型,你必須顯式指定它們。類型推斷的規則很複雜:它們在 JLS 中佔了整整一章 [JLS, 18]。很少有程序員能詳細理解這些規則,但這沒有關係。省略所有 lambda 表達式參數的類型,除非它們的存在使你的程序更清晰。 如果編譯器生成一個錯誤,告訴你它不能推斷 lambda 表達式參數的類型,那麼就顯式指定它。有時你可能必須強制轉換返回值或整個 lambda 表達式,但這種情況很少見。

One caveat should be added concerning type inference. Item 26 tells you not to use raw types, Item 29 tells you to favor generic types, and Item 30 tells you to favor generic methods. This advice is doubly important when you’re using lambdas, because the compiler obtains most of the type information that allows it to perform type inference from generics. If you don’t provide this information, the compiler will be unable to do type inference, and you’ll have to specify types manually in your lambdas, which will greatly increase their verbosity. By way of example, the code snippet above won’t compile if the variable words is declared to be of the raw type List instead of the parameterized type List<String>.

關於類型推斷,有些警告應該被提及。Item-26 告訴你不要使用原始類型,Item-29 告訴你要優先使用泛型,Item-30 告訴你要優先使用泛型方法。在使用 lambda 表達式時,這些建議尤其重要,因爲編譯器獲得了允許它從泛型中執行類型推斷的大部分類型信息。如果不提供此信息,編譯器將無法進行類型推斷,並且必須在 lambda 表達式中手動指定類型,這將大大增加它們的冗長。舉例來說,如果變量聲明爲原始類型 List 而不是參數化類型 List<String>,那麼上面的代碼片段將無法編譯。

Incidentally, the comparator in the snippet can be made even more succinct if a comparator construction method is used in place of a lambda (Items 14. 43):

順便說一下,如果使用 comparator 構造方法代替 lambda 表達式(Item-14),那麼代碼片段可以變得更加簡潔:

Collections.sort(words, comparingInt(String::length));

In fact, the snippet can be made still shorter by taking advantage of the sort method that was added to the List interface in Java 8:

事實上,通過 Java 8 中添加到 List 接口的 sort 方法,可以使代碼片段變得更短:

words.sort(comparingInt(String::length));

The addition of lambdas to the language makes it practical to use function objects where it would not previously have made sense. For example, consider the Operation enum type in Item 34. Because each enum required different behavior for its apply method, we used constant-specific class bodies and overrode the apply method in each enum constant. To refresh your memory, here is the code:

在語言中添加 lambda 表達式使得在以前沒有意義的地方使用函數對象變得實際。例如,考慮 Item-34 中的操作枚舉類型。因爲每個枚舉的 apply 方法需要不同的行爲,所以我們使用特定於常量的類體並覆蓋每個枚舉常量中的 apply 方法。爲了喚醒你的記憶,以下是代碼:

// Enum type with constant-specific class bodies & data (Item 34)
public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override
    public String toString() { return symbol; }

    public abstract double apply(double x, double y);
}

Item 34 says that enum instance fields are preferable to constant-specific class bodies. Lambdas make it easy to implement constant-specific behavior using the former instead of the latter. Merely pass a lambda implementing each enum constant’s behavior to its constructor. The constructor stores the lambda in an instance field, and the apply method forwards invocations to the lambda. The resulting code is simpler and clearer than the original version:

Item-34 指出,枚舉實例字段比特定於常量的類體更可取。Lambda 表達式使得使用前者取代後者來實現特定於常量的行爲變得容易。只需將實現每個枚舉常量的行爲的 lambda 表達式傳遞給它的構造函數。構造函數將 lambda 表達式存儲在實例字段中,apply 方法將調用轉發給 lambda 表達式。生成的代碼比原始版本更簡單、更清晰:

// Enum with function object fields & constant-specific behavior
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);
    }
}

Note that we’re using the DoubleBinaryOperator interface for the lambdas that represent the enum constant’s behavior. This is one of the many predefined functional interfaces in java.util.function (Item 44). It represents a function that takes two double arguments and returns a double result.

注意,我們對錶示枚舉常量行爲的 lambda 表達式使用了 DoubleBinaryOperator 接口。這是 java.util.functionItem-44)中許多預定義的函數式接口之一。它表示接受兩個雙參數並返回雙結果的函數。

Looking at the lambda-based Operation enum, you might think constantspecific method bodies have outlived their usefulness, but this is not the case. Unlike methods and classes, lambdas lack names and documentation; if a computation isn’t self-explanatory, or exceeds a few lines, don’t put it in a lambda. One line is ideal for a lambda, and three lines is a reasonable maximum. If you violate this rule, it can cause serious harm to the readability of your programs. If a lambda is long or difficult to read, either find a way to simplify it or refactor your program to eliminate it. Also, the arguments passed to enum constructors are evaluated in a static context. Thus, lambdas in enum constructors can’t access instance members of the enum. Constant-specific class bodies are still the way to go if an enum type has constant-specific behavior that is difficult to understand, that can’t be implemented in a few lines, or that requires access to instance fields or methods.

查看基於 lambda 表達式的操作 enum,你可能會認爲特定於常量的方法體已經過時了,但事實並非如此。與方法和類不同,lambda 表達式缺少名稱和文檔;如果一個算法並非不言自明,或者有很多行代碼,不要把它放在 lambda 表達式中。 一行是理想的,三行是合理的最大值。如果你違反了這一規則,就會嚴重損害程序的可讀性。如果 lambda 表達式很長或者很難讀,要麼找到一種方法來簡化它,要麼重構你的程序。此外,傳遞給 enum 構造函數的參數在靜態上下文中計算。因此,enum 構造函數中的 lambda 表達式不能訪問枚舉的實例成員。如果枚舉類型具有難以理解的特定於常量的行爲,無法在幾行代碼中實現,或者需要訪問實例字段或方法,則仍然需要特定於常量的類。

Likewise, you might think that anonymous classes are obsolete in the era of lambdas. This is closer to the truth, but there are a few things you can do with anonymous classes that you can’t do with lambdas. Lambdas are limited to functional interfaces. If you want to create an instance of an abstract class, you can do it with an anonymous class, but not a lambda. Similarly, you can use anonymous classes to create instances of interfaces with multiple abstract methods. Finally, a lambda cannot obtain a reference to itself. In a lambda, the this keyword refers to the enclosing instance, which is typically what you want. In an anonymous class, the this keyword refers to the anonymous class instance. If you need access to the function object from within its body, then you must use an anonymous class.

同樣,你可能認爲匿名類在 lambda 表達式時代已經過時了。這更接近事實,但是有一些匿名類可以做的事情是 lambda 表達式不能做的。Lambda 表達式僅限於函數式接口。如果想創建抽象類的實例,可以使用匿名類,但不能使用 lambda 表達式。類似地,你可以使用匿名類來創建具有多個抽象方法的接口實例。最後,lambda 表達式無法獲得對自身的引用。在 lambda 表達式中,this 關鍵字指的是封閉實例,這通常是你想要的。在匿名類中,this 關鍵字引用匿名類實例。如果你需要從函數對象的內部訪問它,那麼你必須使用一個匿名類。

Lambdas share with anonymous classes the property that you can’t reliably serialize and deserialize them across implementations. Therefore, you should rarely, if ever, serialize a lambda (or an anonymous class instance). If you have a function object that you want to make serializable, such as a Comparator, use an instance of a private static nested class (Item 24).

Lambda 表達式與匿名類共享無法通過實現可靠地序列化和反序列化它們的屬性。因此,很少(如果有的話)序列化 lambda(或匿名類實例)。如果你有一個想要序列化的函數對象,比如比較器,那麼使用私有靜態嵌套類的實例(Item-24)。

In summary, as of Java 8, lambdas are by far the best way to represent small function objects. Don’t use anonymous classes for function objects unless you have to create instances of types that aren’t functional interfaces. Also, remember that lambdas make it so easy to represent small function objects that it opens the door to functional programming techniques that were not previously practical in Java.

總之,在 Java 8 中,lambda 表達式是迄今爲止表示小函數對象的最佳方式。不要對函數對象使用匿名類,除非你必須創建非函數式接口類型的實例。 另外,請記住,lambda 表達式使表示小函數對象變得非常容易,從而爲 Java 以前不實用的函數式編程技術打開了大門。


Back to contents of the chapter(返回章節目錄)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章