乍一看, 默认方法为Java虚拟机的指令集带来了一个很棒的新功能。 最后,库开发人员能够开发已建立的API,而不会对其用户代码造成不兼容性。 使用默认方法,当将新方法引入该接口时,任何实现库接口的用户类都会自动采用默认代码。 而且,一旦用户更新了他的实现类,他就可以使用对他的特定用例更有意义的东西来覆盖默认值。 更好的是,用户可以从重写的方法中调用接口的默认实现,并在其周围添加逻辑。
到目前为止,一切都很好。 但是,向已建立的接口添加默认方法会使Java代码无法编译。 在查看示例时,这是最容易理解的。 让我们假设一个库需要其接口之一的类作为输入:
interface SimpleInput {void foo();void bar();
}abstract class SimpleInputAdapter implements SimpleInput {@Overridepublic void bar() {// some default behavior ...}
}
在Java 8之前,接口和相应适配器类的上述组合是Java编程语言中相当普遍的模式。 图书馆供应商通常会提供适配器,以节省图书馆用户的键入时间。 但是,还额外提供了接口以允许多重继承的近似。
让我们进一步假设用户使用了此适配器:
class MyInput extends SimpleInputAdapter{@Overridepublic void foo() {// do something ...}@Overridepublic void bar() {super.bar();// do something additionally ...}
}
通过此实现,用户最终可以与库进行交互。 请注意,该实现如何覆盖bar方法以向默认实现添加一些功能。
那么,如果库迁移到Java 8,会发生什么呢? 首先,该库很可能会弃用适配器类,并将功能移至默认方法。 结果,该接口现在将如下所示:
interface SimpleInput {void foo();default void bar() {// some default behavior}
}
使用此新界面,用户可以更新其代码以适应默认方法,而不必使用适配器类。 使用接口而不是适配器类的最大好处是能够扩展除特定适配器之外的另一个类。 让我们付诸实践,并迁移MyInput
类以使用默认方法。 因为我们现在可以扩展另一个类,所以让我们另外扩展一些第三方基类。 这个基类的作用在这里并不特别相关,因此让我们假设这对我们的用例有意义。
class MyInput extends ThirdPartyBaseClass implements SimpleInput {@Overridepublic void foo() {// do something ...}@Overridepublic void bar() {SimpleInput.super.foo();// do something additionally ... }
}
为了实现与原始类中类似的行为,我们利用Java 8的新语法来调用特定接口的默认方法。 另外,我们将myMethod
的逻辑移到了一些基类MyBase
。 拍拍我们的肩膀。 很好的重构!
我们正在使用的库取得了巨大的成功。 但是,维护人员需要添加另一个接口以提供更多功能。 此接口表示一个CompexInput
,它使用其他方法扩展了SimpleInput
。 因为默认方法通常被认为可以安全添加 ,所以维护者还重写了SimpleInput
的默认方法,并添加了一些行为以提供更好的默认值。 毕竟,在实现适配器类时,这样做很普遍:
interface ComplexInput extends SimpleInput {void qux();@Overridedefault void bar() {SimpleInput.super.bar(); // so complex, we need to do more ...}
}
这项新功能非常强大,以至ThirdPartyBaseClass
的维护者决定也依赖此库。 为此,他为ThirdPartyLibrary
实现了ComplexInput
接口。
但这对MyInput
类意味着什么? 由于通过扩展ThirdPartyBaseClass
隐式实现ComplexInput
,调用SimpleInput
的默认方法突然变得非法。 结果,用户的代码不再编译。 而且,由于Java认为此调用与调用间接超类的super-super方法一样是非法的,因此现在通常也禁止调用此方法。 相反,您可以调用默认方法
ComplexInput
类。 但是,这需要您首先在MyInput
显式实现此接口。 对于图书馆的用户来说,这种变化很有可能是出乎意料的!
奇怪的是,Java运行时没有进行这种区分。 JVM的验证程序将允许已编译的类调用SimpleInput::foo
即使已加载的类在运行时通过扩展ThirdPartyBaseClass
的更新版本隐式实现了ComplexClass
。 这里只有编译器抱怨。
但是我们从中学到什么呢? 简而言之,请确保不要在另一个接口中覆盖默认方法。 既不使用其他默认方法,也不使用抽象方法。 通常,要小心使用默认方法。 它们可以像Java的collection接口一样轻松地简化已建立的API的演变,但它们本身却很复杂,因为它们允许执行类型层次结构中的方法调用。 在Java 7之前,您只需要通过遍历线性类层次结构来查找实际调用的代码。 仅当您确实觉得有必要时才添加这种复杂性。
翻译自: https://www.javacodegeeks.com/2014/05/java-8-default-methods-can-break-your-users-code.html