先赞后看, 养成习惯
文本已收录至 GitHub 开源仓库 https://github.com/bingyanglu/Lu_JavaNodes 码云仓库地址 Lu_JavaNodes https://gitee.com/bingqilinpeishenme/Lu-JavaNodes , 包含教程涉及所有思维导图, 案例代码和后续讲解视频, 欢迎 Star 增砖添瓦.
前言
在传统的接口语法中, 接口中只可以有抽象方法. 在是在实际的使用中, 我们往往会需要用到很多和接口相关的功能(方法), 这些功能会单独的拿出开放在工具类中.
工具类: 类中所有的方法都是静态的
例如: Collection 和 Collocations,Collection 是一个集合接口, 而我们需要很多集合相关的操作, 像集合的排序, 搜索等等, 这时候人们会把这些静态方法放在 Collections 工具类中.
在传统 Java 中我们经常会看到这样的情况, 有一个接口叫 A, 这时候就会有一个类叫 As,As 中全是和 A 接口有关的静态方法.
例如: Executor 和 Executors
这样的一种方式总归来说是有点不方便. 于是在 JDK8 中 Java 对于接口做了一些改动, 允许将静态方法直接写入接口中.(接口中可以定义静态方法, 静态方法肯定不是抽象的, 是有实现的).
接口的静态方法
代码案例
根据上述内容, 我们来定义一个接口, 在接口中写入一个静态方法.
- public class TestStaticInterface {
- public static void main(String[] args) {
- // 静态方法可以通过类名直接调用 接口可以说是特殊的类 所以通过接口名可以调用接口中的静态方法
- HelloInterface.printHello();
- }
- }
- interface HelloInterface{
- int hhh();
- // 定义静态方法
- static void printHello(){
- System.out.println("Hello");
- }
- }
运行代码可以看到如下结果
静态方法有什么用呢?
静态方法实际上是很实用的, 最基本的用法: 我们可以把产生接口对象的方法放在接口中.
什么意思??? 好, 接下来我们通过代码演示一下.
假设现在我们有一个 Animal 接口, 那么这时候如果要获得一个 Animal 类型的对象, 我们要怎么做呢?
传统方法, 创建一个 Animals 工具类, 在其中有一个 static Animal createDog()可以获取一个 Animal 类型的对象, 代码如下
- public class TestStaticInterface {
- public static void main(String[] args) {
- // 通过工具类获取对象
- Animal animal = Animals.createDog();
- }
- }
- class Animals{
- // 静态方法获取对象
- static Animal createDog(){
- // 局部内部类
- class Dog implements Animal{
- }
- // 返回对象
- return new Dog();
- }
- }
但是当你拥抱 JDK8 的时候, 一切都不一样了, 因为有接口静态方法, 可以直接将接口对象的获取放在接口的静态方法中. 代码如下
- public class TestStaticInterface {
- public static void main(String[] args) {
- // 通过接口的静态方法获取一个 Animal 类型的对象
- Animal animal = Animal.createDog();
- }
- }
- interface Animal{
- // 静态方法获取对象
- static Animal createDog(){
- // 局部内部类
- class Dog implements Animal{
- }
- // 返回对象
- return new Dog();
- }
- }
在 JDK 的 API 中是怎么使用静态方法的
接下来我们通过 Java 中的 API 来验证一下这种使用方法. 通过 API 文档, 可以找到 Comparator 接口(比较器), 在这个接口中现在就有很多的静态方法(JDK8). 如图
通过这些静态方法, 就可以通过接口直接获取比较器对象.
- public class TestStaticInterface {
- public static void main(String[] args) {
- // 通过 Comparator 接口获取一个自然排序的比较器(自然排序就是 String 中默认实现的排序逻辑)
- Comparator<String> comparator = Comparator.naturalOrder();
- // 创建集合
- List<String> list = Arrays.asList("b","a","c");
- // 通过比较器对集合进行排序
- list.sort(comparator);
- for (String s : list) {
- System.out.println(s);
- }
- }
- }
传统接口的另一个问题: 向后兼容性不好
现在接口已经有了静态方法, 但是传统的接口还有另一个问题. 我们举例说明:
假设你正在公司中做项目, 在你的代码中, 有一个 UserService 的接口, 接口中有一个方法 String getUsernameById().
- interface UserService{
- String getUsernameById();
- }
该接口因为在项目中存在老长时间了, 所以实现类众多, 有 100 个实现类.
one day, 领导希望你给这个接口中添加一个新的接口方法 String getIdByUsername(). 这样的需求意味着要修改 100 个实现类, 不要说写代码了, 删库跑路的心都有了.
这是一个极端的案例, 但是说明了一个事儿, 传统的接口向后兼容性不好, 不易于维护和改造.
而这个问题, 在 JDK8 中得到了解决, 解决方法就是: 接口的默认方法.
接口的默认方法
Java 8 中允许接口中包含具有具体实现的方法, 该方法称为 "默认方法", 默认方法使用 default 关键字修饰.
在接口中使用 default 表示这个方法有实现, 接口中所有的方法都是 public
示例代码
- interface UserService{
- String getUsernameById();
- // 默认方法
- default void m1(){
- System.out.println("这是一个默认方法");
- }
- }
- class UserServiceImpl implements UserService{
- @Override
- public String getUsernameById() {
- return null;
- }
- }
示例代码的问题
看了这样的一段代码, 你一定会有一些疑问, 我们一起来解决一下.
接口中的默认方法, 实现类能不能继承到?
答: 这个当然是可以的, 并且在实现类中依然可以进行方法的覆盖.
如果 UserServiceImpl 再有一个父类, 父类中也有 m1 方法, 那么 UserServiceImpl 继承到的是父类还是接口中的 m1 方法?
- interface UserService{
- String getUsernameById();
- // 默认方法
- default void m1(){
- System.out.println("这是一个默认方法");
- }
- }
- // 父类
- class UserSer{
- public void m1(){
- System.out.println("这是一个默认方法");
- }
- }
- class UserServiceImpl extends UserSer implements UserService{
- @Override
- public String getUsernameById() {
- return null;
- }
- }
答: 在实现类中继承到的是父类中的. 因为接口默认方法有 "类优先" 的原则.
接口默认方法的 "类优先" 原则
若一个接口中定义了一个默认方法, 而另外一个父类或接口中 又定义了一个同名的方法时
选择父类中的方法. 如果一个父类提供了具体的实现, 那么
接口中具有相同名称和参数的默认方法会被忽略.
接口冲突. 如果一个父接口提供一个默认方法, 而另一个接 口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法), 那么必须覆盖该方法来解决冲突
对于 JDK8 接口新语法的思考
关于接口新语法的讲解实际上已经结束了, 但是想要和大家一起延伸一下思考, 看下面一个案例.
- interface IA{
- default void m2(){
- System.out.println("IA");
- }
- }
- interface IB{
- default void m2(){
- System.out.println("IB");
- }
- }
- class ImplC implements IA,IB{
- // 接口冲突 通过覆盖解决
- public void m2(){
- System.out.println("Impl");
- }
- }
以上代码实际上就是 "类优先" 原则第二条接口冲突的演示代码, 而我要思考的问题不是这个, 而是: 1. 在实现类中, 如何使用 super,2. 如果 IA 和 IB 接口中的 m2 方法返回值不同怎么办?
1. 在实现类中, 如何使用 super?
第一个问题, 比较好解决, 因为有 m2 来自两个接口, 所以我们如果要调用 super 的话, 需要说明要调用那个接口的 super, 语法: 接口名. super.m2()
实现类继承的方法来自两个接口, 必须覆盖, 否则引用不明确. 要调用 super, 也必须指明要调用那个接口.
其实这个问题来自多继承, 过去接口比较简单, 调用 super 肯定不会调用接口, 接口中方法都是抽象的, 现在不一样了, 父类和接口中都有方法实现, 这时候再要调用就要指明要调用谁了.
虽然 Java 一直都是单继承, 但是这个语法实际上已经是向多继承靠近了. 只不过并没有把多继承正式的引入 Java, 所以会有一定的不足, 这就是我们的第二个思考题.
2. 如果 IA 和 IB 接口中的 m2 方法返回值不同怎么办?
这其实也是一个标准的多继承的问题, 在现版本没有解决.
在 C++ 中其实就简单了, 可以指定要覆盖谁
总结
学过了接口的静态方法和默认方法, 仿佛发现了一个事儿, 接口和抽象类越来越像了, 那么这时候再问你那个问题: 接口和抽象类有什么区别?
这个问题留给大家, 好像以前背答案开始不好使了.
最后我们简单总结一下 JDK8 接口语法的新变化: 在 JDK8 以后的接口中, 允许有静态方法和默认方法 (default) 修饰
求关注, 求点赞, 求转发
来源: https://www.cnblogs.com/bingyang-py/p/12342178.html