分享

Java 8新特性1--接口的变化

 自然科学思维 2017-04-24


走入Java8的世界

Java 8发行版是自Java 5以来最具革命性的版本。Java 8 为Java语言、编译器、类库、开发工具与JVM带来了大量新特性。

技术的更新是非常迅速的,作为一个技术人员,应当对新技术保存足够的敏锐和兴趣,并及时迎合到新的技术中来。我们将在这个Java8的系列文章中逐步带领大家走入Java8的世界。

接口的变化

在Java8之前,Java中,实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。

但是,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题。

现实情况是,现存的实体类往往不在接口设计者的控制范围之内,这些实体类为了适配新的接口约定也需要进行修改。

由于Java 8的API在现存的接口上引入了非常多的新方法,这种变化带来的问题也愈加严重。

Java 8为了解决这一问题引入了一种新的机制。

Java 8中的接口现在支持在声明方法的同时提供实现,这听起来让人惊讶!通过两种方式可以完成这种操作。

其一,Java 8允许在接口内声明静态方法。

其二,Java 8引入了一个新功能,叫默认方法。

接口的默认方法

通过默认方法 你可以指定接口方法的默认实现。因此,实现接口的类如果不实现该方法,就会自动继承默认的实现。这种机制可以使你平滑地进行接口的优化和演进。

那么,我们该如何辨识哪些是默认方法呢?非常简单。默认方法由default修饰符修饰, 并像类中声明的其他方法一样包含方法体。

Java 8中, 大量的默认方法已经被添加到核心的JDK接口中了. 

示例:

private interface Defaulable {      default String notRequired() {        return 'Default implementation';    }         }        
private static class DefaultableImpl implements Defaulable { }
    private static class OverridableImpl implements Defaulable {    @Override    public String notRequired() {          return 'Overridden implementation';    } }

Defaulable接口用关键字default声明了一个默认方法notRequired()。

Defaulable接口的实现者之一DefaultableImpl实现了这个接口,并且让默认方法保持原样。

Defaulable接口的另一个实现者OverridableImpl用自己的方法覆盖了默认方法。

注意:接口不能提供对Object类的任何方法的默认实现。特别是,这意味着从接口里不能提供对equals,hashCode或toString的默认实现。

接口的静态方法

Java 8带来的另一个有趣的特性是接口可以声明并且可以提供实现静态方法。例如:

private interface DefaulableFactory {      static Defaulable create( Supplier< Defaulable > supplier ) {        return supplier.get();    } }

在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。

默认方法允许继续使用现有的Java接口,而同时能够保障正常的编译过程。

这方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……

解决冲突的规则

我们知道Java语言中一个类只能继承一个父类,但是一个类可以实现多个接口。

随着默认方 法在Java 8中引入,有可能出现一个类继承了多个方法而它们使用的却是同样的函数签名。

这种 情况下,类会选择使用哪一个函数?在实际情况中,像这样的冲突可能极少发生,但是一旦发生 这样的状况,必须要有一套规则来确定按照什么样的约定处理这些冲突。

假设有以下几个接口:

public interface A {
   default void hello() {        System.out.println('Hello from A');    } }
   
public interface B extends A {
   default void hello() {        System.out.println('Hello from B');    } }
   
public class C implements B, A {
   public static void main(String... args) {
           new C().hello();    } }

猜猜打印输出 的是什么?

解决问题的三条规则

如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条 规则可以进行判断。

(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优 先级。

(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择 拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。

(3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法。否则将不能编译通过。

依据此规则,上面的示例将会使用B接口中的方法。

冲突及如何显式地消除歧义

前面的例子能够应用前两条判断规则解决。让我们更进一步,假设B不再继承A呢?

public interface A {
        void hello() {            System.out.println('Hello from A');        } }
        
public interface B {
       void hello() {            System.out.println('Hello from B');        } }
       public class C implements B, A { }

这时规则(2)就无法进行判断了,因为从编译器的角度看没有哪一个接口的实现更加具体,两个都差不多。A接口和B接口的hello方法都是有效的选项。所以,Java编译器这时就会抛出一个 编译错误,因为它无法判断哪一个方法更合适。

冲突的解决

解决这种两个可能的有效方法之间的冲突,没有太多方案;你只能显式地决定你希望在C中使用哪一个方法。

为了达到这个目的,你可以覆盖类C中的hello方法,在它的方法体内显式地调用你希望调用的方法。

Java 8中引入了一种新的语法X.super.m(...),其中X是你希望调用的m方法所在的父接口。

举例来说,如果你希望C使用来自于B的默认方法,它的调用方式看起来就如 下所示:

public class C implements B, A {
   void hello(){        B.super.hello();    } }

显式地选择调用接口B中的方法

尽管默认方法非常强大,但是在使用默认方法时我们需要小心注意一个地方:在声明一个默认方法前,请仔细思考是不是真的有必要使用默认方法,因为默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。

小结

  • Java 8中的接口可以通过默认方法和静态方法提供方法的代码实现。

  • 默认方法的开头以关键字default修饰,方法体与常规的类方法相同。

  • 默认方法的出现能帮助库的设计者以后向兼容的方式演进API。

  • 默认方法可以用于创建可选方法和行为的多继承。

  • 类或者父类中声明的方法的优先级高于任何默认方法。如果前一条无法解决冲突,那就
    供的默认方法。




给公众号发送关键词可以搜索文章哦,试试吧


@不迷失|知识改善生活

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多