Java 8 接口默认方法详解及冲突处理

自Java 8起,接口定义的方法不仅仅只可以是抽象方法了,还可以定义带有具体实现的方法,叫做默认方法。定义这种方法很简单,就是在接口中编写具体方法,在方法前面添加default关键字,那么实现这个接口的类,自动具备了接口的默认方法的行为,这和继承类同时就具有的父类方法非常像,所以很多人也把这个特性认为是Java的多继承的实现,但其实还是有一些区别的,同时也存在一些类似多继承的问题,我们接下来会说到。

先看看接口默认方法的使用格式,如下:

public interface IDefault {
    default void hello() {
        System.out.println("接口中的hello");
    }
}

那么Java 8 为什么要提供接口的默认方法特性呢?

用一句话总结就是,接口的默认方法,能够让接口类库的开发者平滑的进行接口升级改造,而不会对已经使用(实现)接口的用户造成影响。

我们先举一个平时开发中遇到的例子,比如我按照业务开发规范,实现了小明开发的一个订单计算接口 IOrder。

/**
 * @Auther: www.itzhimei.com
 * @Description: 订单计算接口
 */
public interface IOrder {

    /**
     * 计算订单均价
     */
    void calOrderPrice();

    /**
     * 计算订单商品数量
     */
    void calOrderNum();
}

我自己实现接口的代码:

/**
 * @Auther: www.itzhimei.com
 * @Description:
 */
public class MyOrderImpl implements IOrder {
    @Override
    public void calOrderPrice() {
        System.out.println("我自己实现的订单价格计算方法");
    }

    @Override
    public void calOrderNum() {
        System.out.println("我自己实现的订单商品数量计算");
    }
}

上线后一直运行很好,但是突然有一天,我的代码在编译阶段就报错了,无法通过编译,原因是因为小明在他的IOrder接口中又增加了一个计算订单总金额的方法

/**
  * 计算单位时间内订单总金额
*/
void calOrderTotalAmount();

这时的我,就不得不在自己的实现类MyOrderImpl中紧急增加一个实现方法,哪怕是我暂时用不到的,也要去实现,这就是接口。

这时如果小明评估后,将他要新增的订单金额计算方法声明为默认方法,那么对我当前代码就没有任何影响,并且我的代码还自动具备了他新增的订单金额计算功能。

改造后的IOrder接口:

/**
 * @Auther: www.itzhimei.com
 * @Description: 订单计算接口
 */
public interface IOrder {

    /**
     * 计算订单均价
     */
    void calOrderPrice();

    /**
     * 计算订单商品数量
     */
    void calOrderNum();

    /**
     * 计算单位时间内订单总金额
     */
    default void calOrderTotalAmount() {
        System.out.println("小明声明的[订单总金额]默认方法");
    }
}

理解了上面简单的例子,我们继续再展开讲一下。Java 8新增了接口默认方法,最主要的原因是API的大更新,接口默认方法能够支持接口平滑的升级和演变。比如我们常用的集合类List和ArrayList。

我们先看接口List的类结构图:

List接口继承子Collection,Collection接口自JDK1.8加入了两个接口默认方法:

default Stream<E> stream();
default Stream<E> parallelStream();

按照上面说的,Java 8 因为API大更新所以加入了接口默认方法,之所以让接口支持了默认方法是因为原来接口机制的限制,1.8之前接口中的方法必须是抽象方法,由子类实现,如果还是按照1.8之前的接口规则,如果想让所有集合类都支持stream()方法,就在Collection接口先定一个抽象stream()方法,那么杯具的事情来了,JDK中所有Collection接口的子类、孙子类等等所有实现类,都需要自己实现一遍stream()方法的具体实现逻辑,哪怕代码逻辑都完全是一样的,每个子类、孙子类都需要实现一遍。

例如ArrayList,它的类继承关系如下:

按照上图的继承关系,我们要么在ArrayList中实现stream(),要么在ArrayList的抽象父类中实现AbstractList中实现stream(),这还只是一个方法,在Java 8中和流相关的新增方法还有很多,对于Oracle JDK的开发维护者来说,这样一一实现是一个巨大的工作量。

但是这并不是最悲剧的,最悲剧的是,按照传统接口模式更新的JDK发布后,大家更新版本,悲剧的事情开始了,JAVA 面世这么多年,很多三方包对集合类进行了自定义扩展,使用了这些三方包的码农门发现,自己的代码出现了茫茫多的报错,提示有未实现的方法,这对整个JAVA圈是不可接受的,任谁也不会这样去更新自己的版本,所以才有了接口的默认方法。

接口的默认方法冲突解决

有了接口的默认方法,就存在一个问题,接口默认方法和实现类中的方法冲突、接口默认方法和其子接口或父接口默认方法冲突、两个没有关系的接口中具有相同默认方法产生的冲突,我们接下来就逐个说明这些冲突的解决办法,或者说是遇到冲突的处理规则。

我们先看一个标准的接口默认方法实现模式,没有任何冲突的例子。

声明一个带有接口,并实现一个接口默认方法

public interface IDefault {
    default void hello() {
        System.out.println("接口中的hello");
    }
}

AClass类实现了接口

public class AClass implements IDefault {
}

测试

/**
 * 不存在任何冲突的情况下,实现类直接调用接口默认方法
 */
public class Client {

    public static void main(String[] args) {
        AClass aClass = new AClass();
        aClass.hello();

    }
}

输出:

接口中的hello

1、冲突情况:一个类继承的父类和实现的接口都具有同时方法声明

public interface IADefault {
    default void hello() {
        System.out.println("IADefault接口中的hello");
    }
}

定义父类

public class AParentClass {
    public void hello() {
        System.out.println("AParentClass中的hello");
    }
}

定义子类AClass,继承AParentClass,实现IADefault接口

public class AClass extends AParentClass implements IADefault {

}

测试

/**
 * 继承的父类和实现的接口都具有同时方法声明,
 * 子类这时自己如果没有实现该方法,
 * 那么默认调用父类方法
 * 总结就是不管是类还是父类中的方法,优先级都高于接口中的默认方法
 */
public class Client {

    public static void main(String[] args) {
        AClass aClass = new AClass();
        aClass.hello();
    }
}

输出结果:

AParentClass中的hello

冲突规则总结一:总结就是不管是类还是父类中的方法,优先级都高于接口中的默认方法

2、冲突情况:类实现的接口有继承关系,例如AClass实现了IBDefault,IBDefault接口 继承了IADefault接口,IBDefault和IADefault都有默认方法hello()

public interface IADefault {
    default void hello() {
        System.out.println("IADefault接口中的hello");
    }
}
public interface IBDefault extends IADefault {
    default void hello() {
        System.out.println("IBDefault接口中的hello");
    }
}

AClass实现了IBDefault接口,IBDefault继承了IADefault接口

public class AClass implements IBDefault {

}

测试:

/**
 * 类实现的接口有继承关系,例如AClass实现了IBDefault,IBDefault接口 继承了IADefault接口
 * IBDefault和IADefault都有默认方法hello()
 * 子类这时自己如果没有实现该方法,
 * 那么优先调用IBDefault接口接口的默认方法hello()
 * 总结优先选择最具体的默认方法,其实有点类似于优先选择离类最近的默认方法
 * AClass实现了IBDefault,IBDefault继承了IADefault,很明显IBDefault离AClass最近,所以选择IBDefault的默认方法
 * 类或父类中的同名方法优先级高于这种接口继承的情况
 */
public class Client {

    public static void main(String[] args) {
        AClass aClass = new AClass();
        aClass.hello();
    }
}

输出结果:

IBDefault接口中的hello

冲突规则总结二:优先选择最具体的默认方法,其实有点类似于优先选择离类最近的默认方法。AClass实现了IBDefault,IBDefault继承了IADefault,IADefault接口和IBDefault接口都有hello默认方法,很明显IBDefault离AClass最近,所以选择IBDefault的默认方法类或父类中的同名方法优先级高于这种接口继承的情况。

3、冲突情况:实现类同时实现了两个接口,两个接口有相同的默认方法,并且这两个接口没有任何关系

public interface IADefault {
    default void hello() {
        System.out.println("接口中的hello");
    }
}
public interface IBDefault {
    default void hello() {
        System.out.println("接口中的hello");
    }
}

AClass此时无法通过编译

public class AClass implements IADefault, IBDefault {
    /*@Override
    public void hello() {

    }*/

    //编译报错
    //AClass inherits unrelated defaults for hello()
    // from types IADefault and IBDefault
}

/**
 * 实现类同时实现了两个接口,两个接口有相同的默认方法
 * 无法编译通过,编译器会让我们自己来实现一个方法解决冲突
 *      //编译报错
 *     //AClass inherits unrelated defaults for hello()
 *     // from types IADefault and IBDefault
 */
public class Client {

    public static void main(String[] args) {
        AClass aClass = new AClass();
        aClass.hello();
    }
}

冲突规则总结三:这种情况代码是无法通过编译的,编译器会让我们自己在实现类中实现一个方法解决冲突。

总结就是如果各个类之间存在继承或实现关系,存在默认方法冲突时,优先选择离当前类最近的方法执行,如果子类有对应方法,执行子类方法,如果子类没有,父类有对应方法,执行父类方法,如果父类也没有,接口有,则执行接口中的默认方法,如果接口存在继承关系,也是优先执行子接口的默认方法。

把上面的几种情况的代码执行一下,来加深理解。