自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();
}
}
冲突规则总结三:这种情况代码是无法通过编译的,编译器会让我们自己在实现类中实现一个方法解决冲突。
总结就是如果各个类之间存在继承或实现关系,存在默认方法冲突时,优先选择离当前类最近的方法执行,如果子类有对应方法,执行子类方法,如果子类没有,父类有对应方法,执行父类方法,如果父类也没有,接口有,则执行接口中的默认方法,如果接口存在继承关系,也是优先执行子接口的默认方法。
把上面的几种情况的代码执行一下,来加深理解。