Dating Java8系列之default默认方法

简介: Dating Java8系列之default默认方法


引言

传统上,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。
  • 默认方法可以用于创建可选方法和行为的多继承。
  • 我们有办法解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突。
  • 类或者父类中声明的方法的优先级高于任何默认方法。如果前一条无法解决冲突,那就选择同函数签名的方法中实现得最具体的那个接口的方法。
  • 两个默认方法都同样具体时,你需要在类中覆盖该方法,显式地选择使用哪个接口中提供的默认方法。

作者:翎野君

本篇文章如有帮助到您,请给「翎野君」点个赞,感谢您的支持。

目录
打赏
0
0
0
0
12
分享
相关文章
|
11天前
|
《从头开始学java,一天一个知识点》之:方法定义与参数传递机制
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 🚀 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。上篇:《输入与输出:Scanner与System类》 | 下篇剧透:《方法重载与可变参数》。
43 25
|
5天前
|
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
18 1
Java中的异常处理方法
本文深入剖析Java异常处理机制,介绍可检查异常、运行时异常和错误的区别与处理方式。通过最佳实践方法,如使用合适的异常类型、声明精确异常、try-with-resources语句块、记录异常信息等,帮助开发者提高代码的可靠性、可读性和可维护性。良好的异常处理能保证程序稳定运行,避免资源泄漏和潜在问题。
|
19天前
|
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
45 5
Java容器及其常用方法汇总
Java Collections框架提供了丰富的接口和实现类,用于管理和操作集合数据。
Java容器及其常用方法汇总
|
1月前
|
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
166 3
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
|
1月前
|
java.time常用方法汇总
`java.time` API 是从 Java 8 开始引入的时间日期处理库,旨在替代老旧的 `java.util.Date` 和 `Calendar`。它提供了更简洁、强大和灵活的方式处理日期、时间、时区及时间间隔,支持全球化和时间计算需求。API 包含获取当前时间、创建指定时间、解析和格式化字符串、进行加减运算、比较时间、获取年月日时分秒、计算时间间隔、时区转换以及判断闰年等功能。示例代码展示了如何使用这些功能,极大简化了开发中的时间处理任务。
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
102 9
Java面试必问!run() 和 start() 方法到底有啥区别?
在多线程编程中,run和 start方法常常让开发者感到困惑。为什么调用 start 才能启动线程,而直接调用 run只是普通方法调用?这篇文章将通过一个简单的例子,详细解析这两者的区别,帮助你在面试中脱颖而出,理解多线程背后的机制和原理。
91 12