Java8默认方法试图进一步简化JavaAPI。不幸的是,这种最近的语言扩展带来了一组复杂的规则,只有少数Java开发人员知道这些规则。本文告诉您为什么引入默认方法会破坏您(用户)的代码。乍一看,默认方法为Java虚拟机的指令集带来了许多新特性。最终,开发该库的人员将能够升级API,而不会导致与客户端代码的兼容性问题。使用默认方法,任何实现库接口的类都会自动适应接口引入的默认方法。一旦用户更新了他实现的类,他就可以轻松地用更有意义的方法覆盖原来的默认方法。更好的是,用户可以调用接口的默认实现并在覆盖方法时添加业务逻辑。到目前为止,一切都很好。但是,在创建接口时添加默认方法可能会使Java代码不兼容。从下面的例子中可以很容易地理解这一点。我们假设一个库需要它的其中一个接口作为输入:interfaceSimpleInput{voidfoo();voidbar();}abstractclassSimpleInputAdapterimplementsSimpleInput{@Overridepublicvoidbar(){//somedefaultbehavior...}}在Java8之前,类似于上面的联合使用接口和适配器类是Java编程语言中非常常见的设计模式。该适配器通常由库提供者提供,用于为库的用户节省一些操作。但是,如果以接口的形式提供,则类似于允许多重继承。让我们进一步假设用户使用以下适配器:最后与图书馆互动。请注意我们如何覆盖bar方法并向默认实现添加其他功能。如果这个库移植到Java8上会怎样?首先,该库很可能会弃用适配器类并使用默认方法来提供该功能。最终,界面将如下所示:interfaceSimpleInput{voidfoo();defaultvoidbar(){//somedefaultbehavior}}使用这个新界面,用户可以更新他的代码以使用默认方法而不是原始适配器类。使用接口而不是适配器类的最大后果是该类可以扩展(扩展)其他类而不是特定的适配器。现在让我们通过移植MyInput类来练习使用默认方法。由于我们现在可以从其他类继承,因此我们从第三方基类继承。我们这里不需要关心这个基类的作用,我们可以假设这对我们的功能有意义。classMyInputtextendsThirdPartyBaseClassimplementsSimpleInput{@Overridepublicvoidfoo(){//dosomething...}@Overridepublicvoidbar(){SimpleInput.super.bar();//dosomethingadditionally...}}为了实现与原始类类似的功能,我们使用Java8的调用指定接口的默认方法的新语法。另外,将我们方法中的一些逻辑移到基类中。说到这里,你可能会拍拍我的肩膀说,这是一次很好的重构!我们已经非常成功地使用了这个库。然而,维护者需要添加另一个接口来提供更多的功能。该接口被ComplexInput接口取代,它继承了SimpleInput接口并增加了新的方法。因为添加默认方法通常是安全的,所以维护者覆盖了SimpleInput的默认方法并提供了更好的默认方法。毕竟,这对于适配器类来说是很常见的事情。interfaceComplexInputtextendsSimpleInput{voidqux();@Overridedefaultvoidbar(){SimpleInput.super.bar();//socomplex,weneedtodomore...}}新特性带来了如此好的效果,以至于ThirdPartyBaseClass的维护者也决定依赖这个库。为此,它在ThirdPartyLibrary中实现了ComplexInput接口。但这对MyInput类意味着什么?为了隐式实现ComplexInput接口,可以继承ThirdPartyBaseClass类,但是调用SimpleInput的默认方法突然变得不合法了。结果,用户的代码无法编译。现在禁止这种调用,因为Java认为这种在非直接子类中调用父类的父类的方法是非法的。您只能在ComplexInput中调用此默认方法,但这需要您显式实现MyInput中的接口。对于图书馆的用户来说,这种变化是意料之中的!更奇怪的是,Java运行时并没有强制执行此限制。JVM验证器允许已编译的类调用SimpleInput::foo方法,即使该类通过继承更新的ThirdPartyBaseClass隐式实现了ComplexClass。此限制仅存在于编译器中。我们可以从中学到什么?简而言之,确保您没有在一个接口中覆盖另一个接口的默认方法,无论是使用默认方法还是使用抽象方法。通常,谨慎使用默认方法。尽管这使得Java的集合接口API很容易发生革命性变化,但本质上,这种继承层次之间的方法调用增加了系统的复杂性。在Java7之前,你只需要沿着线性的类层次结构找到真正被调用的代码。只有当你觉得绝对有必要时才添加这种复杂性。
