Java 8的default method是一个好的设计吗?

java8新增了default method,接口中新增的方法子类即使没有实现,编译器也不会报错。 我觉得这种设计糟透了。修改接口是一件慎重的事情,需要把改动通知到所有的子类,否则,编译、运行时都会报错。但是通过默认方法却可以规避这个错误。 举个例子,我们有一个上传文件的接口,只有一个upload方法,后来又增加了一个delete方法。由于是默认方法,使用者认为子类已经实现了该方法于是放心大胆地delete,导致子类里的一些资源被误…
关注者
77
被浏览
5901
首先要看Java 8的default method的设计初衷、它要解决的问题是什么。
先放俩传送门:

语言里的所有功能都有它原本的设计目的,也有被滥用/错误使用的可能性。
既然default method的初衷是让已有接口能平滑演进添加新功能,这是不是就意味着我们应该滥用它来胡乱给接口方法塞默认实现呢?

接口方法的默认实现也要遵循基本法。—— ▣-▣

以Java 8的 java.lang.Iterable<T> 接口为例。它上面新添加了forEach()方法,并为它提供了默认实现。它的默认实现长什么样子?
hg.openjdk.java.net/jdk
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
这个默认实现只依赖于该接口自身的其它方法能够满足的功能。把for-each循环的语法糖解开会更明显:
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        // for (T t : this) {
        for (Iterator<T> it = this.iterator(); it.hasNext(); ) {
            T t = it.next();
            {
                action.accept(t);
            }
        }
    }
可以看到,这个默认实现依赖于this的 iterator() 方法,而该方法正是 Iterable<T> 接口的主方法,有待实现接口的类去实现。这用法就很正确。
由于Java的default method本质上是virtual extension method,这个 forEach() 实现还可以被具体的实现类所覆写,提供更高效、更合适的实现。例如说在 java.util.ArrayList<E> 里它就有更高效的实现版本:
hg.openjdk.java.net/jdk
    @Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
这个版本直接使用普通for循环,避免了创建Iterator对象的开销。

同理,我们可以看看Java 8的 java.util.List<T> 接口。它在Java 8新增了 sort() 方法并提供了默认实现。它的默认实现是这样的:
hg.openjdk.java.net/jdk
    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
这个默认实现其实就是把以前 java.util.Collections 的sort()方法的实现搬了过来,然后把 Collections.sort() 变成对新的 List.sort() 的调用:
    @SuppressWarnings({"unchecked", "rawtypes"})
    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }
这样,如果实现类有更高效的实现,就可以用上更高效的版本了,例如ArrayList.sort():
    @Override
    @SuppressWarnings("unchecked")
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }
同样避免了默认实现中ListIterator对象的创建,还减少了一次两次数组拷贝,简直好。

可见在它的正常使用场景中,它不但可以演进接口让它添加新的方法,还由于它是virtual extension method,可以由具体实现类提供更高效的实现。而这是例如C#的static extension method所做不到的。

那么回到题主的问题。题主说:
举个例子,我们有一个上传文件的接口,只有一个upload方法,后来又增加了一个delete方法。由于是默认方法,使用者认为子类已经实现了该方法于是放心大胆地delete,导致子类里的一些资源被误删了。
这个锅只能题主的团队自己背。
想想看,就算没有default method,以Java 7及以前的已有功能,照样能构造出一模一样的问题。

首先有一个 IUploader 接口,然后假如有一个抽象基类来提供一些通用功能,然后有一些具体的派生类继承抽象基类,供各个场景具体使用。
假如这个接口原本没有 delete() 方法,后来在接口上新加了这个方法,而且有人手贱在抽象基类上添加了 delete() 方法的一个默认实现,而且该实现并不适用于所有的具体派生类应有的场景。结果使用者一调用,biaji,资源就误删了。

这锅应该让Java的接口背么,应该让Java的抽象类背么?都不是,只能让手贱的哪个人背。
同样,如果题主举的例子是有人在接口上新加了个方法并提供了不合适的默认实现,这锅该Java的default method功能背么?当然不。并不是语言有了个新功能就应该不分青红皂白在所有地方乱用它的。
可以平滑演进的接口就适合使用default method,而语义上不可以平滑演进就不应该使用default method。