为什么Java 接口中的存在 Static 和 Default 方法?

1. 概述

在我阅读源码过程中 存在接口中使用很多 static和default的情况,本文就是使用实际的例子题体会接口中的 Static 和 Default 方法 有什么作用。

2. 为什么需要接口中的 Default 方法?

和普通的接口方法一样,default 方法默认是 public 的,不需要特别声明。

不同的是,我们在方法签名开头加上 default 关键字,并且提供具体实现。

bash 复制代码
public interface MyInterface {

    // 普通接口方法

    default void defaultMethod() {
        // default 方法的实现
    }
}

为什么要有 default 方法?主要是为了向后兼容。

想象一下,你发布了一个接口,有好多类实现了它。现在你想给接口加个新方法,结果所有实现类都编译不过了,因为它们都没实现这个新方法。

有了 default 方法就不一样了。你给接口加个 default 方法,实现类自动就有这个方法了,不用改任何代码。

这样既加了新功能,又不破坏现有代码。

3. Default 方法实战

看个实际例子。

假设有个 Vehicle 接口,就一个 Car 实现类(实际可能有很多,这里简化一下):

bash 复制代码
public interface Vehicle {

    String getBrand();

    String speedUp();

    String slowDown();

    default String turnAlarmOn() {
        return "Turning the vehicle alarm on.";
    }

    default String turnAlarmOff() {
        return "Turning the vehicle alarm off.";
    }
}

然后写个实现类:

bash 复制代码
public class Car implements Vehicle {

    private String brand;

    // 构造函数/getter

    @Override
    public String getBrand() {
        return brand;
    }

    @Override
    public String speedUp() {
        return "The car is speeding up.";
    }

    @Override
    public String slowDown() {
        return "The car is slowing down.";
    }
}

最后定义个 main 类,创建 Car 实例并调用方法:

bash 复制代码
public static void main(String[] args) {
    Vehicle car = new Car("BMW");
    System.out.println(car.getBrand());
    System.out.println(car.speedUp());
    System.out.println(car.slowDown());
    System.out.println(car.turnAlarmOn());
    System.out.println(car.turnAlarmOff());
}

注意 Vehicle 接口里的 default 方法 turnAlarmOn()turnAlarmOff() 在 Car 类里是自动可用的。

而且,如果以后我们给 Vehicle 接口添加更多 default 方法,应用还能继续运行,不需要强制让实现类提供新方法的实现。

default 方法最常见的用途就是给某个类型逐步添加新功能,同时不破坏那些实现类。

另外,我们还可以用 default 方法在已有抽象方法的基础上提供额外功能:

bash 复制代码
public interface Vehicle {

    // 其他接口方法

    double getSpeed();

    default double getSpeedInKMH(double speed) {
       // 转换逻辑
    }
}

4. 多重继承的冲突

default 方法用起来挺好,但有个坑:如果一个类实现了多个接口,这些接口都有同名的 default 方法,会怎样?

比如这样:

bash 复制代码
public interface Alarm {

    default String turnAlarmOn() {
        return "Turning the alarm on.";
    }

    default String turnAlarmOff() {
        return "Turning the alarm off.";
    }
}

现在 Car 类同时实现 Vehicle 和 Alarm 接口:

bash 复制代码
public class Car implements Vehicle, Alarm {
    // ...
}

这代码编译不过,因为 Car 类继承了两套同名的 default 方法,编译器不知道该用哪个。

解决办法很简单,自己实现一个:

bash 复制代码
@Override
public String turnAlarmOn() {
    // 自定义实现
}

@Override
public String turnAlarmOff() {
    // 自定义实现
}

也可以指定用某个接口的实现,比如用 Vehicle 的:

bash 复制代码
@Override
public String turnAlarmOn() {
    return Vehicle.super.turnAlarmOn();
}

@Override
public String turnAlarmOff() {
    return Vehicle.super.turnAlarmOff();
}

或者用 Alarm 的:

bash 复制代码
@Override
public String turnAlarmOn() {
    return Alarm.super.turnAlarmOn();
}

@Override
public String turnAlarmOff() {
    return Alarm.super.turnAlarmOff();
}

甚至两个都用:

bash 复制代码
@Override
public String turnAlarmOn() {
    return Vehicle.super.turnAlarmOn() + " " + Alarm.super.turnAlarmOn();
}

@Override
public String turnAlarmOff() {
    return Vehicle.super.turnAlarmOff() + " " + Alarm.super.turnAlarmOff();
}

5. Static 方法

Java 8 还支持在接口里写 static 方法。

static 方法不属于对象,也不是实现类的一部分,只能通过接口名调用。

给 Vehicle 加个 static 方法:

bash 复制代码
public interface Vehicle {

    // 普通方法 / default 方法

    static int getHorsePower(int rpm, int torque) {
        return (rpm * torque) / 5252;
    }
}

定义和调用都很简单:

bash 复制代码
Vehicle.getHorsePower(2500, 480));

为什么要这样设计?主要是让相关的工具方法可以放在一起,不用单独建个工具类。

抽象类也能做到,但抽象类可以有构造函数和状态,而接口更轻量。

这样就不用到处建 XxxUtilsXxxHelper 这种类了。

6. 实际例子

看看 Java 8 的集合框架就知道这些特性有多好用了。

List 接口加了不少 default 方法:

bash 复制代码
list.forEach(System.out::println);
list.sort(Comparator.naturalOrder());
list.replaceAll(String::toUpperCase);

还有 static 方法(Java 9+):

bash 复制代码
List<String> list = List.of("A", "B", "C");

Comparator 接口里全是 static 工厂方法:

bash 复制代码
Comparator<String> comp = Comparator.naturalOrder();
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Comparator<Person> byName = Comparator.comparing(Person::getName);

配合 default 方法可以链式调用:

bash 复制代码
Comparator<Person> combined = Comparator
    .comparing(Person::getAge)
    .thenComparing(Person::getName)
    .reversed();

Stream 也一样:

bash 复制代码
Stream<String> stream = Stream.of("A", "B", "C");
Stream<String> empty = Stream.empty();
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);

7. 什么时候用?

Default 方法适合:

  • 给老接口加新功能,又不想改实现类
  • 提供一些可选的默认行为
  • 基于其他抽象方法做一些便利方法

Static 方法适合:

  • 工具方法,比如 Collections.sort()
  • 工厂方法,比如 Comparator.comparing()
  • 验证或创建实例的辅助方法

8. 总结

从面向对象的角度看,接口里有具体实现确实有点奇怪。理论上接口应该只定义 API,不该有行为。

不过为了保持向后兼容,static 和 default 方法算是个不错的妥协。

记住几点就行:

  • Default 方法让你可以给接口加新东西,不用改所有实现类
  • Static 方法让你可以把相关工具方法放一起,不用专门建工具类(提高接口的聚集程度 在接口内部一站式结解决)
  • 碰到多重继承冲突,用 InterfaceName.super.method() 指定用哪个
  • Java 8 的集合框架、Comparator、Stream 等到处都在用这些特性
相关推荐
用户571155176831 小时前
深入解析Spring BeanPostProcessor
后端
掘金者阿豪3 小时前
🚀 CentOS Stream 9服务器Docker部署KWDB:从零到跨模查询实战全记录
后端
yang_xin_yu3 小时前
一文带你精通泛型PECS原则与四大核心函数式接口
后端
孟陬3 小时前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
树獭叔叔3 小时前
13-KV Cache与位置编码表:大模型推理加速的核心技术
后端·aigc·openai
想用offer打牌4 小时前
一站式了解四种限流算法
java·后端·go
嘻哈baby4 小时前
用 C++ 写线程池是怎样一种体验?
后端
嘻哈baby4 小时前
SQL Server 和 Oracle 以及 MySQL 有哪些区别?
后端
绝无仅有4 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构