翎野君/文
引言
传统上,Java程序的接口是将相关方法按照约定组合到一起。实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。
不断迭代的API
默认方法的引入就是为了,以兼容的方式,解决像 Java API这样的类库,演进迭代问题。
理解演进迭代
为了理解为什么一旦API发布之后,它的演进就变得非常困难,我们假设你是一个Github上的开源作者,兴致勃勃的写了一个开源项目,然后放到了Github上面。
没过多久你的项目就被其他用户Fork到本地,然后开始使用了起来,并且在项目中对你发布的一些接口进行了实现。
发布API几个月之后,你突然意识到接口中遗漏了一些功能。需要调整原来的接口,在其中新增方法,这样的话接口的易用性会更好。
不过,事情并非如此简单!你要考虑已经使用了你接口的用户,他们可能已经按照自身的需求实现了你的接口,倘若你更新了接口的API并重新进行了发布,那么所以实现了你的接口的地方,都需要进行改动。
简而言之,向接口添加方法是诸多问题的罪恶之源;一旦接口发生变化,实现这些接口的类往往也需要更新,提供新添方法的实现才能适配接口的变化。如果你对接口以及它所有相关的实现有完全的控制,这可能不是个大问题。但是这种情况是极少的。
这就是引入默认方法的目的:它让类可以自动地继承接口的一个默认实现。
概述
1.默认方法
默认方法是Java 8中引入的一个新特性,希望能借此以兼容的方式改进API。现在,接口包含的方法签名在它的实现类中也可以不提供实现。那么,谁来具体实现这些方法呢?实际上,缺失的方法实现会作为接口的一部分由实现类继承(所以命名为默认实现),而无需由实现类提供。
默认方法由default修饰符修饰,并像类中声明的其他方法一样包含方法体。比如,你可以像下面这样在集合库中定义一个名为Sized的接口,在其中定义一个抽象方法size,以及一个默认方法isEmpty:
public interface Sized {
int size();
default boolean isEmpty() {
return size() == 0;
} }
这样任何一个实现了Sized接口的类都会自动继承isEmpty的实现。
2.使用默认方法
可选方法
你肯定碰到过这种情况,一个类实现了接口,不过却将一些实现方法进行留白,没有实现。
我们以Iterator接口为例来说。Iterator接口定义了hasNext、next,还定义了remove方法。Java 8 之前,由于用户通常不会使用该方法,remove方法常被忽略。因此,实现Interator接口的类通常会为remove方法放置一个空的实现,这些都是没有意义毫无用处的代码。采用默认方法之后,你可以为这种类型的方法提供一个默认的实现,这样实体类就无需在自己的实现中显式地提供一个空方法。比如,在Java 8中,Iterator接口就为remove方法提供了一个默认实现。
interface Iterator<T> { boolean hasNext();
T next();
default void remove() {
throw new UnsupportedOperationException();
}
}
通过这种方式,你可以减少无效的模板代码。实现Iterator接口的每一个类都不需要再声明一个空的remove方法了,因为它现在已经有一个默认的实现。
行为的多继承
Java的类只能继承单一的类,但是一个类可以实现多接口。
下面是Java API中对ArrayList类的定义:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
}
这个例子中ArrayList继承了一个类,实现了四个接口。因此ArrayList实际是 五个类型的直接子类,分别是:AbstractList,List,RandomAccess,Cloneable,Serializable。所以,在某种程度上,我们就有了类型的多继承。
由于Java 8中接口方法可以包含实现,类可以从多个接口中继承它们的行为(即实现的代码)。 让我们从一个例子入手,看看如何充分利用这种能力来为我们服务。
public interface MoveService {
void run();
default void flash() {
System.out.println("闪现!!!");
}
}
public interface SkillService {
void q();
void w();
void e();
default void r() {
System.out.println("默认大招:伤害100点");
}
}
public class Shooter implements MoveService, SkillService {
@Override
public void run() {
System.out.println("寒冰射手 走~~");
}
@Override
public void q() {
System.out.println("寒冰 q");
}
@Override
public void w() {
System.out.println("寒冰 w");
}
@Override
public void e() {
System.out.println("寒冰 e");
}
public void r(){
System.out.println("寒冰 伤害100点!!同时冰冻对方5s!!!");
}
public static void main(String[] args) {
new Shooter().r();
}
}
方法冲突
我们知道Java语言中一个类只能继承一个类,但是一个类可以实现多个接口。
随着默认方法在Java 8中引入,有可能出现一个类继承了多个方法而它们使用的却是同样的函数签名。这种情况下,在一个类中使用父类的默认方法,这样会有冲突吗,没有的话,那会选择哪一个呢?
1.解决冲突的三条规则
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。
- 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
- 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
- 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
2.冲突示例
类中的方法优先级最高
public interface PlayerService {
default void stop() {
System.out.println("播放器--停止!!!");
}
}
public class SonyPlayerServiceImpl implements PlayerService {
public void stop() {
System.out.println("Sony播放器--停止!!!");
}
public static void main(String[] args) {
new SonyPlayerServiceImpl().stop();
}
}
选择提供了最具体实现的默认方法的接口
public interface PlayerService {
default void showLyric() {
System.out.println("PlayerService : show lyric");
}
}
public interface RecordService extends PlayerService{
default void showLyric() {
System.out.println("RecordService : show lyric");
}
}
public class Question1 implements RecordService {
public static void main(String[] args) {
new Question1().showLyric();
System.out.println("应该选择的是提供了最具体实现的默认方法的接口。由于RecordService比PlayerService更具体,所以应该选择由于RecordService比PlayerService更具体的showLyric方法。");
}
}
冲突和显示的消除歧义
public class Question2 implements PlayerService,RecordService {
@Override
public void showLyric() {
PlayerService.super.showLyric();
RecordService.super.showLyric();
}
public static void main(String[] args) {
new Question2().showLyric();
}
}
小结
- Java 8中的接口可以通过默认方法提供方法的代码实现。
- 默认方法的开头以关键字default修饰,方法体与常规的类方法相同。
- 默认方法的出现能帮助库的设计者以后向兼容的方式演进API。
- 默认方法可以用于创建可选方法和行为的多继承。
- 我们有办法解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突。
- 类或者父类中声明的方法的优先级高于任何默认方法。如果前一条无法解决冲突,那就选择同函数签名的方法中实现得最具体的那个接口的方法。
- 两个默认方法都同样具体时,你需要在类中覆盖该方法,显式地选择使用哪个接口中提供的默认方法。
作者:翎野君
本篇文章如有帮助到您,请给「翎野君」点个赞,感谢您的支持。