Java 8:當重載遇上lambda

jopen 9年前發布 | 19K 次閱讀 Java 8 Java開發

譯文出處: deepinmind    原文出處: JOOQ

要設計出好的API絕非易事。真的是很不容易。如果你希望用戶能給你的API點個贊的話,設計的時候需要考慮得非常周全。你必須得在以下幾點中找到一個平衡點:

  1. 實用性
  2. 可用性
  3. 向后兼容
  4. 向前兼容

我在前面的優秀的API是如何煉成的一文中也提到了同樣的問題。今天,我們來看一下,

Java 8是如何破壞了這個游戲規則的:

Java 8:當重載遇上lambda

沒錯,就是破壞。

重載的便利性主要體現在以下兩個方面:

  1. 提供了不同的參數類型
  2. 提供了參數的默認值

JDK中就有許多類似的例子:

public class Arrays {

// Argument type alternatives
public static void sort(int[] a) { ... }
public static void sort(long[] a) { ... }

// Argument default values
public static IntStream stream(int[] array) { ... }
public static IntStream stream(int[] array, 
    int startInclusive, 
    int endExclusive) { ... }

}</pre>

jOOQ的API當然也充分利用了這一優點。由于jOOQ是SQL的一個DSL,我們甚至還有點濫用了:

public interface DSLContext {
    <T1> SelectSelectStep<Record1<T1>> 
        select(SelectField<T1> field1);

<T1, T2> SelectSelectStep<Record2<T1, T2>> 
    select(SelectField<T1> field1, 
           SelectField<T2> field2);

<T1, T2, T3> SelectSelectStep<Record3<T1, T2, T3>> s
    select(SelectField<T1> field1, 
           SelectField<T2> field2, 
           SelectField<T3> field3);

<T1, T2, T3, T4> SelectSelectStep<Record4<T1, T2, T3, T4>> 
    select(SelectField<T1> field1, 
           SelectField<T2> field2, 
           SelectField<T3> field3, 
           SelectField<T4> field4);

// and so on...

}</pre>

像Ceylon這類的語言還對重載的便捷性作了更進一步的定義,它們認為在Java中只有上述這類場景才應該使用重載。因此,Ceylon語言的作者將重載從這門語言中徹底地移除了,他用聯合類型和默認值來作為替代方案。比方說:

// Union types
void sort(int[]|long[] a) { ... }

// Default argument values IntStream stream(int[] array, int startInclusive = 0, int endInclusive = array.length) { ... }</pre>

想了解更多關于Ceylon的知識,可以看下“我希望能引入到Java中的十個Ceylon特性”。

但在Java中,很不幸的是,我們并不能使用聯合類型或者是參數默認值。因此,為了讓API的使用者更方便一些,我們只能使用重載。

然而,如果你的參數是一個函數式接口的話并且還牽涉到方法重載的話,Java 7與Java 8中的情況則是天壤之別。JavaFX中便有一個現成的例子。

JavaFX中“不甚友好”的ObservableList

JavaFX對JDK中的集合類型進行了增強,使得它們成為了“可觀察的”對象。不要把它同Observable混淆了,那是JDK1.0中已經廢棄的一個類型。

JavaFX中的Observable類是這樣定義的:

public interface Observable {
  void addListener(InvalidationListener listener);
  void removeListener(InvalidationListener listener);
}

幸運的是,InvalidationListener還是一個函數式接口(譯者注:函數式接口指的是只有一個方法的接口):

@FunctionalInterface
public interface InvalidationListener {
  void invalidated(Observable observable);
}

這樣就太好了,于是我們就可以這么寫了:

Observable awesome = 
    FXCollections.observableArrayList();
awesome.addListener(fantastic -> splendid.cheer());

(看膩了foo/bar/baz這樣的東西了吧,我給大家來點歡樂點的。接下來也會延續這個風格。foo/bar已經是老掉牙的東西了。譯者注:awesome/fantastic/cheer。)。

當然,我們聲明的很可能并不是Observable,并不是Observable,而是一個更實用的ObservableList,很不幸的是,這樣的話情況就變得復雜了:

ObservableList<String> awesome = 
    FXCollections.observableArrayList();
awesome.addListener(fantastic -> splendid.cheer());

這樣寫的話,第二行的位置就會出現一個編譯錯誤:

awesome.addListener(fantastic -> splendid.cheer());
//      ^^^^^^^^^^^ 
// The method addListener(ListChangeListener<? super String>) 
// is ambiguous for the type ObservableList<String>

因為,ObservableList其實是這樣的:

public interface ObservableList<E> 
extends List<E>, Observable {
    void addListener(ListChangeListener<? super E> listener);
}

而ListChangeListener的定義又是:

@FunctionalInterface
public interface ListChangeListener<E> {
    void onChanged(Change<? extends E> c);
}

在Java 8以前,這兩個監聽者的類型是完全不同的,當然現在其實也仍然不同。當然了,通過傳遞命名類型來調用的話還是很方便的。如果這么寫的話,前面的代碼也還仍然能夠跑通:

ObservableList<String> awesome = 
    FXCollections.observableArrayList();
InvalidationListener hearYe = 
    fantastic -> splendid.cheer();
awesome.addListener(hearYe);

或者這么寫:

ObservableList<String> awesome = 
    FXCollections.observableArrayList();
awesome.addListener((InvalidationListener) 
    fantastic -> splendid.cheer());

再或者:

ObservableList<String> awesome = 
    FXCollections.observableArrayList();
awesome.addListener((Observable fantastic) -> 
    splendid.cheer());

這樣就能沒有歧義了。不過說實話,如果使用lambda表達式還要聲明類型的話,真的有點太煞風景了。現代的IDE一般都能夠進行自動補全,可以像編譯器一樣進行類型推導。

假設我們現在要調用另一個addListener()方法,這個方法接收的是一個ListChangeListener接口。那么你只能這么寫:

ObservableList<String> awesome = 
    FXCollections.observableArrayList();

// Agh. Remember that we have to repeat "String" here ListChangeListener<String> hearYe = fantastic -> splendid.cheer(); awesome.addListener(hearYe);</pre>

或者:

ObservableList<String> awesome = 
    FXCollections.observableArrayList();

// Agh. Remember that we have to repeat "String" here awesome.addListener((ListChangeListener<String>) fantastic -> splendid.cheer());</pre>

亦或是:

ObservableList<String> awesome = 
    FXCollections.observableArrayList();

// WTF... "extends" String?? But that's what this thing needs... awesome.addListener((Change<? extends String> fantastic) -> splendid.cheer());</pre>

切勿重載,務必謹慎

API設計是非常復雜的。以前便是如此,現在則更為嚴峻。采用了Java 8之后,如果你的API方法中的參數包含函數式接口的話,在進行重載前最好三思。即便你決定要進行重載了,最好還是再確認一遍,是否真有必要這么做。

不相信?看一下JDK中的API吧。比方說java.util.stream.Stream類型。看看里面函數式接口的個數相同且接口方法內的參數個數也相同的方法有多少(比如說前面這個例子中的addListener())?

沒有。

這里重載的方法有些是參數個數不同的。比如:

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

<R, A> R collect(Collector<? super T, A, R> collector);</pre>

這樣調用collect()方法的時候就不會產生歧義了。

如果參數數量一樣的情況下,而參數(函數式接口)自身方法的參數個數也是一樣的話,方法名則會不同。比如:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

當然了,調用者會覺得很窩火,因為你事先還得想好不同的類型下該調用哪個方法。

不過這是這一窘境唯一的解決方案了。因此,記住了:

當重載遇上lambda,絕對不是什么好事!

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!