Java 8:当重载遇上lambda

Published: 05 Feb 2015 Category: java

要设计出好的API绝非易事。真的是很不容易。如果你希望用户能给你的API点个赞的话,设计的时候需要考虑得非常周全。你必须得在以下几点中找到一个平衡点:

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

我在前面的优秀的API是如何炼成的一文中也提到了同样的问题。今天,我们来看一下,

Java 8是如何破坏了这个游戏规则的:

image

没错,就是破坏。

重载的便利性主要体现在以下两个方面:

  • 提供了不同的参数类型
  • 提供了参数的默认值

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) { ... }
}

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...
}

像Ceylon这类的语言还对重载的便捷性作了更进一步的定义,它们认为在Java中只有上述这类场景才应该使用重载。因此,Ceylon语言的作者将重载从这门语言中彻底地移除了,他用联合类型和默认值来作为替代方案。比方说:

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

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

想了解更多关于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);

或者:

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

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

亦或是:

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

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

切勿重载,务必谨慎

API设计是非常复杂的。以前便是如此,现在则更为严峻。采用了Java 8之后,如果你的API方法中的参数包含函数式接口的话,在进行重载前最好三思。即便你决定要进行重载了,最好还是再确认一遍,是否真有必要这么做。

不相信?看一下JDK中的API吧。比方说java.util.stream.Stream类型。看看里面函数式接口的个数相同且接口方法内的参数个数也相同的方法有多少(比如说前面这个例子中的addListener())?

没有。

这里重载的方法有些是参数个数不同的。比如:

R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner);

R collect(Collector<? super T, A, R> collector); 这样调用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,绝对不是什么好事!

英文原文链接