在Java8发行之前,如果不破坏现有的实现,是不可不能给接口添加方法的。如果给某个接口添加了一个新的方法,一般来说,现有的实现类是没有这个方法的,因此就会导致编译错误。在Java8中新增了缺省方法(default method)构造,目的就是允许给现有的接口添加方法,但是给现有的接口添加新反法还是充满风险的。
缺省方法的声明中包括一个缺省实现,这是给实现了该接口但没有实现默认方法的所有类使用。虽然Java中增加了缺省方法之后,可以给现有的接口添加方法了,但是并不能确保这些方法在之前存在的实现中就能良好的运行。因为这些默认的方法是被“注入”到现有的实现中的,它们的实现者并不知道,也没有许可。在Java8之前,编写这些实现时,是默认它们的接口永远不需要任何新方法的。
Java8在核心集合接口中增加了许多新的缺省方法,主要是为了便于使用lambda(详见第6章)。Java类库的缺省方法是高质量的通用实现,它们在大多数情况下都正常使用。但是并非每一个可能的实现的所有变体,始终都可以编写一个缺省方法
。
比如,以removeIf方法为例,在Java8中被添加到Collection接口。这个方法用来移除所有指定条件的元素,并用一个boolean函数(或者断言)返回true。缺省实现指定用其迭代器来遍历集合,在每个元素上调用断言(predicate),并利用迭代器的remove方法移除断言返回值为true的元素。其声明大致如下:
// Default method added to the Collection interface in Java 8
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中的静态工厂Collection.SynchronizedCollection。Apache版本额外提供了利用客户端的对象(而不是集合)进行锁定的功能。换句话说,它是一个包装类(详见第18条),它的所有方法在委托给包装集合之前,都在锁定对象上进行同步。
Apache 版本的SynchronizedCollection类依然有人维护,但是到编写本书之时它也没有取代removeIf方法。如果这个类于Java8结合使用,将会继承removeIf的缺省实现。它不会(实际上也无法)保持这个类的基本承诺:围绕着每一个方法调用执行自动同步。缺省实现压根不知道同步这回事,也无权访问包含该锁定对象的域。如果客户在SynchronizedCollection实例上调用removeIf方法,同时另一个线程对该集合进行修改,就会导致ConcurrentModificationException
或者其他异常行为。
为了避免在类似的Java平台类库实现中发生这种异常,如Collection.SynchronizedCollection
返回的包装私有类,JDK维护人员就必须覆盖默认的removeIf实现,以及像它一样的其他方法,以便在调用缺省方法实现之前执行必要的同步。不属于Java平台组成部分的预先存在的集合实现,过去无法做出与接口变化相类似的改变,现在有些已经可以做到了。
有了缺省方法,接口的现有实现就不会出现编译时没有报错或警告,运行时却失败的情况
。这个问题虽然并非普遍,但也不是孤立的意外事件。Java8在集合接口中添加的许多方法是极易受影响的,有些现有实现已知将会受到影响。
建议尽量避免利用缺省方法在现有的接口上添加新的方法,除非有特殊需要,但就算在那样的情况下也应该慎重考虑:缺省的方法实现是否会破化现有的接口实现。然而,在创建接口的时候,用缺省方法提供标准的方法实现是非常方便的,它简化了实现接口的任务(详见第20条)。
还要注意的是,缺省方法不支持从接口中删除方法,也不支持修改现有的方法的签名。对接口进行这些修改肯定会破坏现有的客户端代码。
结论很明显:尽管缺省方法现在已经是Java平台的组成部分,但谨慎设计接口仍然是至关重要的
。虽然缺省方法可以在现有的接口上添加方法,但这么做还是存在着很大的风险。就算接口中只有细微的缺陷都可能永远给用户带来不愉快;假如接口有严重的缺陷,则可能摧毁包含它的API。
因此,在发布程序之前,测试每一个新的接口就显得尤其重要。程序员应该以不同的方法实现每一个接口。最起码不应该少于三种实现。编写多个客户端程序,利用每个新接口的实例来执行不同的任务,这一点也同样重要。这些步骤对确保每个接口都能够满足其既定的所有用途起到了很大的帮助。它还有助于在接口发布之前及时发现其中的缺陷。使你依然能够轻松的把它们纠正过来。或与接口程序发布之后也能纠正,但是千万别指望它啦!
网友评论