Effective Java 3rd 条目21 为后代设计接口

在Java8之前,添加方法到接口而没有破坏已存实现,是不可能的。如果你添加了一个新方法到接口,已存实现通常缺少这个方法,导致编译时期错误。在Java8中,添加了默认方法构造(default method construct)[JLS 9.4],让方法添加到已存接口。但是添加新方法到已存接口充满了危险性。

默认方法的声明包括一个默认实现(default implementation),实现接口的所有类使用了这个默认实现,而不是实现默认方法。虽然默认方法的添加使得添加方法到已存接口变得可能,但是这些方法是否在先存实现中起作用,是没有保证的。在它们的实现者不知情下,默认方法“注入”到已存实现。早于Java8中,编写这些实现是完全知情的:它们的接口将不再获得任何新方法。

许多新默认方法添加到了Java8中的核心集合接口,主要是为了方便lambda的使用(第6章)。Java库的默认方法是通用目的高质量实现,而且在大多数情况下它们正常工作。但是编写一个默认方法,这个方法维护了每个可能实现的所有变量,这不总是可行的

例如,考虑removeIf方法,在Java8中它被添加到Collection接口。这个方法移除给定boolean函数(或者谓词(predicate))返回true的所有元素。默认实现被指定使用它的迭代器遍历这个集合,在每个元素上调用这个谓词,使用这个迭代器的remove方法移除谓词返回true的元素。这个声明大概看上去如下:

// 在Java8中添加到Collection接口的默认方法
default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter); 
    boolean result = false; 
    for (Iterator<E> it = iterator(); it.hasNext(); ) {
        if (filter.test(it.next())) {
            it.remove();
            result = true;
        } 
    } 
    return result;
}

这是为removeIf方法可以编写的通用目的最好实现,但是可悲的是,它在一些现实的Collection实现上失败了。例如,考虑org.apache.commons.collections4.collection.SynchronizedCollection。这个类是来自Apache Commons库中,和java.util中Collections.synchronizedCollection静态工厂返回的那个,是相似的。Apache版本为锁定额外提供了这个功能:代替集合,使用客户端提供的对象。换句话说,它是一个包装类(条目18),它的所有方法,在代理到包装的集合之前,在一个锁定对象上同步。

Apache SynchronizedCollection类还在积极地维护着,但是本文撰写时,它没有覆写removeIf方法。如果这个类连同Java8一起使用,那么因此它将会继承removeIf的默认实现,这没有,实际上不能,维持这个类的基本承诺:为每个方法调用自动同步。这个默认实现不知道同步,而且不能够访问包含锁定对象的域。如果一个客户端,面对由另外线程并行修改这个集合时,调用SynchronizedCollection实例的removeIf方法,那么可能导致SynchronizedCollection或者其他未指定行为。

在相似的Java平台库实现中,比如,Collections.synchronizedCollection返回的包私有类,为了防止这个发生,JDK维护者不得不覆写默认removeIf实现和像这个方法的其他方法,在调用默认实现之前进行必要的同步。不属于Java平台部分的先存集合实现,没有机会随着接口改变做相似的修改,所以其他的人不得不这么做。

对于默认方法,一个接口的现存实现可能正常编译,没有错误或者警告,但是在运行时失败。虽然不是非常常见,但是这个问题也不是一个孤立的事件。添加到Java8中集合接口的一些方法,被认为是易感染的,而且一些已存实现被认为是受感染的。

通常应该避免使用默认方法添加新方法到已存接口,除非这个需求是紧要关头,这种情况下你应该仔细考虑,默认方法实现是否破坏已存接口实现。然而默认方法是相当有用的,当接口创建时它提供了标准方法实现,以减轻实现这个接口的任务(条目20)。

还值得注意的是,默认方法不是设计来支持从接口中移除方法或者改变已存方法的签名。这些接口改变都是不可能没有破坏已存客户端的。

这个寓意是明确的。即使默认方法现在是Java平台的一部分,但是非常小心设计接口仍旧至关重要。虽然默认方法使得添加放到已存接口变得可能,但是这样做有很大的危险。如果一个接口含有一个微小的缺陷,那么这可能永远会激怒它的用户;如果接口有严重缺陷,那么它可能使得包含它的API失败。

所以,在发布接口之前,测试每个新接口极其重要。许多程序员应该以不同的方式实现每个接口。至少,你应该争取三个不同的实现。同样重要的是,编写多个客户端程序,使用每个新接口的实例执行各种任务。保证每个接口满足它的所有预期用途,有很长的路要走。这些步骤让你在发布接口之前发现它们中的错误,而且你仍旧可以轻易地改正。虽然在一个接口发布之后改正一个接口缺陷是可能的,但是你不可能依靠于此

猜你喜欢

转载自blog.csdn.net/tigershin/article/details/79360926
今日推荐