自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的类结构图
List接口继承子Collection,Collection接口自JDK1.8加入了两个接口默认方法:
default Stream stream();default Stream parallelStream();
按照上面说的,Java 8 因为API大更新所以加入了接口默认方法,之所以让接口支持了默认方法是因为原来接口机制的限制,1.8之前接口中的方法必须是抽象方法,由子类实现,如果还是按照1.8之前的接口规则,如果想让所有集合类都支持stream()方法,就在Collection接口先定一个抽象stream()方法,那么杯具的事情来了,JDK中所有Collection接口的子类、孙子类等等所有实现类,都需要自己实现一遍stream()方法的具体实现逻辑,哪怕代码逻辑都完全是一样的,每个子类、孙子类都需要实现一遍。
例如ArrayList,它的类继承关系如下:
按照上图的继承关系,我们要么在ArrayList中实现stream(),要么在ArrayList的抽象父类中实现AbstractList中实现stream(),这还只是一个方法,在Java 8中和流相关的新增方法还有很多,对于JDK的开发维护者来说,这样一一实现是一个巨大的工作量。
但是这并不是最悲剧的,最悲剧的是,按照传统接口模式更新的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(); }}
冲突规则总结三:这种情况代码是无法通过编译的,编译器会让我们自己在实现类中实现一个方法解决冲突。
总结就是如果各个类之间存在继承或实现关系,存在默认方法冲突时,优先选择离当前类最近的方法执行,如果子类有对应方法,执行子类方法,如果子类没有,父类有对应方法,执行父类方法,如果父类也没有,接口有,则执行接口中的默认方法,如果接口存在继承关系,也是优先执行子接口的默认方法。
把上面的几种情况的代码执行一下,来加深理解。