为什么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 等到处都在用这些特性
相关推荐
我学上瘾了6 小时前
Spring Cloud的前世今生
后端·spring·spring cloud
波波0077 小时前
ASP.NET Core 健康检查实战:不只是一个 /health 接口
后端·asp.net
小码哥_常7 小时前
Spring Boot 搭建邮件发送系统:开启你的邮件自动化之旅
后端
石榴树下的七彩鱼8 小时前
图片修复 API 接入实战:网站如何自动去除图片水印(Python / PHP / C# 示例)
图像处理·后端·python·c#·php·api·图片去水印
我叫黑大帅8 小时前
为什么TCP是三次握手?
后端·网络协议·面试
我叫黑大帅9 小时前
如何排查 MySQL 慢查询
后端·sql·面试
techdashen9 小时前
Rust项目公开征测:Cargo 构建目录新布局方案
开发语言·后端·rust
消失的旧时光-19439 小时前
Spring Boot 实战(五):接口工程化升级(统一返回 + 异常处理 + 错误码体系 + 异常流转机制)
java·spring boot·后端·解耦
Rust研习社9 小时前
Rust 智能指针 Cell 与 RefCell 的内部可变性
开发语言·后端·rust
夕颜11110 小时前
Skill 机器人 vs Hermes Agent:两种「AI 越用越聪明」的路径
后端