Java8特性第四讲:Java8的接口默认方法实现
本文是Java8特性第四讲,主要讲解Java 8的接口默认方法实现。理解Java 8 默认方法需理解几个问题: 1、为什么会出现默认方法? 2、接口中出现默认方法,且类可以实现多接口的,那和抽象类有啥区别? 3、多重实现的默认方法冲突怎么办?
文章目录
1、什么是默认方法,为什么要有默认方法
1.1、案例
一个接口A,Clazz类实现了接口A。
public interface A {
default void foo(){
System.out.println("Calling A.foo()");
}
}
public class Clazz implements A {
public static void main(String[] args){
Clazz clazz = new Clazz();
clazz.foo();//调用A.foo()
}
}
代码是可以编译的,即使Clazz类并没有实现foo()方法。在接口A中提供了foo()方法的默认实现。
1.2、什么是默认方法
简单说,就是接口可以有实现方法,而且不需要实现类去实现其方法。只需在方法名前面加个default关键字即可。
1.3、为什么出现默认方法
为什么要有这个特性? 首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
背景:Java8中给很多API提供了新的方法,比如常用的java.util.List接口增加了replaceAll、sort、spliterator这三个方法;根据现行的Java语法规范,List的所有子类都需要对这三个方法进行实现,这显然是不能接受的,尤其是众多第三方类库对JDK有依赖,要求他们一同修改也是做不到的。
- 为了解决这个问题,也保证JDK的兼容性,Java8引入了一种新的机制:接口可以声明带有实现的方法。
语法规则
- 1、使用default关键字
- 2、一个接口中可以含有多个带有默认实现的方法
java.util.List中新增的三个方法源码
public interface List<E> extends Collection<E> {
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}
2、java 8抽象类与接口对比
这一个功能特性出来后,有人反馈,java 8的接口都有实现方法了,跟抽象类岂不是差不多?其实还是有的,请看下表对比。。
相同点 | 不同点 |
---|---|
都是抽象类型 | 抽象类不可以多重继承,接口可以(无论是多重类型继承还是多重行为继承) |
都可以有实现方法(以前接口不行) | 抽象类和接口所反映出的设计理念不同。其实抽象类表示的是”is-a”关系,接口表示的是”like-a”关系 |
都可以不需要实现类或者继承者去实现所有方法,(以前不行,现在接口中默认方法不需要实现者实现) | 接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能改变其值;抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。 |
3、多重继承的冲突
由于同一个方法可以从不同接口引入,自然而然的会有冲突的现象,同一个类可能会从不同的接口中继承了具有相同的签名的方法,这又该如何解决呢?
默认方法判断冲突的规则如下:
1.一个声明在类里面的方法优先于任何默认方法(classes always win)
2.否则,则会优先选取路径最短的。
3.1、举例子
场景1:
public interface A {
default String test() {
return "A";
}
}
public class B {
public String test() {
return "B";
}
}
public class C extends B implements A {
public static void main(String[] args) {
System.out.println(new C().test());
}
}
- 这个例子中C分别中A和B中都继承了test()方法,但是B是类,而A是接口,所以B中的test()方法优先级更高,因此此次输出结果应该是:B
场景二:
public interface D extends A {
default String test() {
return "D";
}
}
public class E implements A, D {
public static void main(String[] args) {
System.out.println(new E().test());
}
}
- 这个例子中E分别中A和D这两个接口中继承了test()方法,但是D是A的子接口,所以D接口中的test()方法优先级更高,因此此处输出应该是:D
场景三:
public class F implements A {
}
public class G extends F implements A, D {
public static void main(String[] args) {
System.out.println(new G().test());
}
}
- 这种场景下虽然F实现了A接口,但是由于它并没有重写test()方法,所以优先级依然是D接口高,因此此处输出依然是:D
场景四:
public interface H {
default String test() {
return "H";
}
}
public class K implements A, H {
public static void main(String[] args) {
System.out.println(new K().test());
}
@Override
public String test() {
return H.super.test();
}
}
- 这个场景中,由于A和H没有继承关系,所以在JVM无法识别出到底要调用哪一个接口中的方法,所以需要在类K中显式的覆盖test()方法,指明具体调用哪个接口中的方法,当然也可以自己重写方法实现;
4、默认方法在项目中的使用
场景1:在规则引擎实现类中,我们使用默认方法标识执行阶段,这样子类不用重写该方法,可以节省代码量。
/**
* 业务检查
**/
public interface BizChecker extends Checker {
/**
* 适用什么阶段
*
* @return
*/
@Override
@NotNull
default PhaseEnum phase() {
return PhaseEnum.BIZ_CHECK;
}
}
场景2:在错误码接口中,将获取错误字符串方法设置为默认方法,可以节省代码量。
/**
* 错误码接口
**/
public interface ErrorCode {
/**
* 错误码 必须在{@link ErrorCodeEnum} 中定义
* @return
* @see {@link ErrorCodeEnum#getErrCode()}
*/
String getErrCode();
/**
* 错误描述
* @return
*/
String getErrDesc();
/**
* 是否用在response里对外提示
* @return
*/
boolean isFirstTip();
/**
* 获取错误字符串:code+desc
* @return
*/
default String getErrString() {
return "code:" + getErrCode() + ",desc:" + getErrDesc() + ",useInResp:" + isFirstTip();
}
}
场景3:限流器默认方法
/**
* @Description 限流器
*/
public interface RateLimiter {
default void acquire() {
acquire(1);
}
/**
* 限流
* @param permits 请求资源数
*/
void acquire(int permits);
}
场景4:处理商品限价,默认方法放在接口上
public interface GoodsFirmHighestPricePretreated {
/**
* 处理最高限价针对商品简化
*/
default Response<Boolean> handleFirmHighestPrice(FullItemDTO fullItemDTO, GoodsPriceContext context) {
if (CollectionUtils.isEmpty(fullItemDTO.getBaseItem().getSkus())) {
return Response.ok(true);
}
}
}
5、总结
默认方法给予我们修改接口而不破坏原来的实现类的结构提供了便利,目前java 8的集合框架已经大量使用了默认方法来改进了,当我们最终开始使用Java 8的lambdas表达式时,提供给我们一个平滑的过渡体验。也许将来我们会在API设计中看到更多的默认方法的应用。