一、默认方法概述
传统Java接口的弊端:Java程序接口是将相关方法安装约定组合到一起的方式,实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。而一旦类库需要更新接口,向其中增加新的方法,此方式就会出现问题。更为棘手的情况是,现存的实体类往往不在接口设计者的控制范围内,这些实体类为了适配新的接口约定也需要进行修改。Java8的API在现存的接口上引入了很多新方法,这种变化带来的问题更加严重。
Java 8为了解决这一问题引入了一种新的机制,Java8中的接口支持在声明方法的同时提供实现,通过以下两种方式实现:
1)Java8允许在接口内声明静态方法。
2)Java8引入了一个新功能:默认方法,通过默认方法可以指定接口方法的默认实现,即接口可以提供方法的具体实现。实现接口的类如果不显示的提供该方法的具体实现,就会自动继承默认的实现。
List接口中的sort新方法定义:
default void sort(Comparator super E> c){
Collections.sort(this, c);
}
默认方法的返回类型前用default修饰,表示这是一个默认方法。
List
numbers.sort(Comparator.naturalOrder()); //sort是List接 口的默认方法
Comparator.naturalOrder方法是一个全新的静态方法,它返回一个Comparator对象,并按照
自然序列对其中的元素排序。
default Stream
return StreamSupport.stream(spliterator(), false);
}
stream方法中调用了 SteamSupport.stream方法来返回一个流,其中spliterator()也是Collection接口的一个默认方法。
默认方法的主要目标用户是类库的设计者,它的引入是为了以兼容的方式解决像Java API这样的类库演进问题的。
所以,向接口添加方法是造成这些问题的根源;一旦接口发生变化,实现这些接口的类往往也需要更新,提供新添方法的实现才能适配接口的变化。引入默认方法的目的:它让类可以自动地继承接口的一个默认实现。
默认方法为方法的多继承提供了一种更灵活的机制,可以帮助你更好地规划代码结构:类可以从多个接口继承默认方法。
不同类型的兼容性:二进制、源代码和函数行为
变更对Java程序的影响大体可以分成三种类型的兼容性,分别是:二进制级的兼容、源代码级的兼容,以及函数行为的兼容。
二进制级的兼容性表示现有的二进制执行文件能无缝持续链接(包括验证、准备和解析) 和运行。为接口添加方法就是二进制级的兼容,如果新添加的方法不被调用,接口已经实现的方法可以继续运行,不会出现错误。
源代码级的兼容性表示引入变化后,现有的程序依然能成功编译通过。向接口添加新的方法就不是源码级的兼容,因为遗留代码并没有实现新引入的方法,它们无法顺利通过编译。
函数行为的兼容性表示变更发生后,程序接受同样的输入能得到同样的结果。为接口添加新的方法就是函数行为兼容的,因为新添加的方法在程序中并未被调用。
默认方法是Java 8中引入的一个新特性,借此以兼容的方式改进API。接口包含的方法签名在它的实现类中可以不提供实现,缺失的方法实现会作为接口的一部分由实现类继承(所以命名为默认实现),而无需由实现类提供。任何一个实现了接口的类都会自动继承其默认方法的实现。
默认方法在Java 8的API中已经大量地使用了,如Collection接口的stream方法、List接口的sort方法、很多函数式接口(Predicate、Function、Comparator)也引入了新的默认方法,如Predicate.and或者Function.andThen。
注:函数式接口只包含一个抽象方法,默认方法是种非抽象方法。
Java 8中的抽象类和抽象接口区别:
一个类只能继承一个抽象类,但是一个类可以实现多个接口。
一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。
所有的Collection类都实现了一个名为java.util.Collection的接口。默认方法是一种以源码兼容方 式向接口内添加实现的方法,这样实现Collction的所有类(包括并不隶属Collection API的用户扩展类)都能使用removeIf的默认实现。
二、默认方法的使用模式
1、可选方法
我们可能会碰到过这种情况,类实现了接口,不过却刻意地将一些方法的实现留白。如Iterator接口,它定义了hasNext、next,还定义了remove方法,由于用户通常不会使用该方法,remove方法常被忽略。因此,实现Interator接口的类通常会为remove方法放置一个空的实现,这些都是些无用的模板代码。
采用默认方法之后,你可以为这种类型的方法提供一个默认的实现,这样实体类就无需在自己的实现中显式地提供一个空方法。在Java 8中,Iterator接口就为remove方法提供了 一个默认实现。通过这种方式,可以减少无效的模板代码。实现Iterator接口的每个类无需再声明一个空的remove方法,因为它已经有一个默认的实现。
2、行为的多继承
Java的类只能继承单一的类,但一个类可以实现多个接口。
类型的多继承
上例中ArrayList继承了一个类,实现了六个接口。因此ArrayList实际是七个类型的直接子类,分别是:AbstractList、List、RandomAccess、Cloneable、Serializable、 Iterable和Collection。在某种程度上,这就是类型的多继承。
Java 8中接口方法可以包含实现,类可以从多个接口中继承它们的行为(即实现的代码)。
利用正交方法的精简接口
示例:定义多个具有不同特质的形状。有的形状需要调整大小,但不需要有旋转的功能;有的需要能旋转和移动,但不需要调整大小。
定义Rotatable接口,并提供两个抽象方法setRotationAngle和getRotationAngle:
public interface Rotatable {
void setRotationAngle(int angleInDegrees);
int getRotationAngle();
default void rotateBy(int angleInDegrees){
setRotationAngle((getRotationAngle () + angle) % 360);
}
}
实现了Rotatable的所有类都需要提供setRotationAngle和getRotationAngle的实现,同时它们也会继承rotateBy的默认实现。
定义两个接口Moveable和Resizable,它们都包含了默认实现。:
public interface Moveable {
int getX();
int getY();
void setX(int x);
void setY(int y);
default void moveHorizontally(int distance){setX(getX() + distance);}
default void moveVertically(int distance){setY(getY() + distance);}
}
public interface Resizable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
default void setRelativeSize(int wFactor, int hFactor){
setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}
}
组合接口
通过组合这些接口,你可以创建不同的实体类。比如Monster可以移动、旋转和缩放。
public class Monster implements Rotatable, Moveable, Resizable { … }
Monster类会自动继承Rotatable、Moveable和Resizable接口的默认方法。
Monster m = new Monster(); //构造函数会设置Monster的坐 标、高度、宽度及默认仰角
m.rotateBy(180); //调用由Rotatable中继承的rotateBy方法
m.moveVertically(10); //调用由Moveable中继承的moveVertically方法
声明另一个Sun类,它要能移动和旋转,但是不能缩放。
public class Sun implements Moveable, Rotatable { … }
通过精简的接口,你能获得最有效的组合,因为你可以只选择你需要的实现。
--参考文献《Java8实战》