1. 引言
很久以前(在 Java 8 之前),接口非常严格:你只能在其中声明抽象方法(无实现)和常量(public static final)。这在当时很方便,直到出现了一个大问题:库的演进。
设想一个场景
你开发了一个很受欢迎的库,其中有这样一个接口:
public interface Movable {
void move(int x, int y);
}
全世界成千上万的程序员都在编写实现了这个接口的类。几年后你意识到大家都缺少一个 reset() 方法,用来把对象恢复到初始位置。于是你在接口中添加了它:
public interface Movable {
void move(int x, int y);
void reset();
}
然后“世界末日”就来了:所有使用你这个接口的项目都无法编译了!因为现在它们必须实现这个新方法,但当时没人知道它的存在。迁移瞬间变成了痛点。
default 方法——解决方案!
Java 8 引入了default 方法:现在可以直接在接口中添加带实现的方法!所有旧的实现类都会自动获得一个标准实现,现有代码不会被破坏。而且如果你愿意——完全可以按自己的方式重写这个方法。
2. default 方法的语法
default 方法是在接口内部带有实现的方法,用关键字 default 标记。
public interface Movable {
void move(int x, int y);
default void reset() {
// 典型实现: 回到坐标原点
move(0, 0);
}
}
说明:
- 接口中的方法默认是 public 和 abstract,但 default 方法不是抽象的,而是带有方法体。
- 关键字 default 总是写在方法的返回类型之前。
在类中是什么样子?
public class Robot implements Movable {
private int x, y;
@Override
public void move(int x, int y) {
this.x = x;
this.y = y;
System.out.println("机器人已移动到 (" + x + ", " + y + ")");
}
// 不需要实现 reset() - 将使用 default 版本!
}
现在,如果我们在 Robot 对象上调用 reset(),将会使用接口 Movable 中的实现:
public class Main {
public static void main(String[] args) {
Movable robot = new Robot();
robot.move(10, 20); // 机器人已移动到 (10, 20)
robot.reset(); // 机器人已移动到 (0, 0)
}
}
3. 标准库中的 default 方法
default 方法的加入并非偶然,而是为了让 Java 中庞大的标准接口在不破坏旧代码的前提下持续演进。
示例:接口 List(Java 8+)
在 Java 8 中,给 List 接口添加了带实现的方法,比如 forEach、replaceAll、sort:
default void forEach(Consumer<Entity> action) {
for (Entity e : this) {
action.accept(e);
}
}
如果你实现了自己的列表却没有重写 forEach,它依然可以工作——这都要归功于 default 方法。
关于泛型类型(Consumer<Entity>)的更多内容,你会在第26级学到 :P
4. 为什么需要 default 方法?
- 让 API 在不破坏代码的情况下演进:可以在接口中添加新方法,而不需要在所有现有类中去实现它们。
- 通用的行为模板:可以声明默认行为,让类直接复用或自行重写。
- 降低重复:当大多数实现的行为都相同,就无需在每个类中复制代码。
类比
想象你有一份房屋租赁合同(接口)。过去上面写的是:“承租人必须支付水费”。后来又补充了:“承租人必须支付电费”。如果没有 default 方法,你就得和所有租户重签一遍合同!而有了 default 方法——只需在接口中加一条默认约定;如果有人需要另行约定,他们也可以按自己的方式处理。
5. default 方法的限制与特性
default 方法不能覆盖 Object 类的方法
你不能在接口中声明与 Object 的 equals、hashCode 或 toString 相同签名的 default 方法。这是为了避免混乱:因为任何 Java 对象已经自带这些方法。
// 编译错误!
interface Broken {
default boolean equals(Object obj) { return false; }
}
default 方法的冲突
如果一个类实现了两个接口,而这两个接口各自都有签名相同的 default 方法,该怎么办?Java 编译器会坦诚告诉你:“请你自己决定,我不知道该怎么做!”
interface A {
default void hello() { System.out.println("Hello from A"); }
}
interface B {
default void hello() { System.out.println("Hello from B"); }
}
class C implements A, B {
// 必须显式解决冲突:
@Override
public void hello() {
// 你可以选择调用哪个接口的方法,或者提供自己的实现
A.super.hello(); // 或者 B.super.hello();
}
}
如果不在类 C 中实现 hello(),就会出现编译错误。
default 方法可以调用接口中的其他方法
default 方法可以调用接口中的其他方法,即便它们是抽象的。只要在实现类中提供了对应实现即可。
interface Printer {
void print(String text);
default void printTwice(String text) {
print(text);
print(text);
}
}
6. 示例:用 default 方法演进应用
来看一个在接口 Movable 中使用 default 方法的示例:
public interface Movable {
void move(int x, int y);
default void reset() {
move(0, 0);
}
}
还有一个实现了该接口的类 Robot:
public class Robot implements Movable {
private int x = 5;
private int y = 7;
@Override
public void move(int x, int y) {
this.x = x;
this.y = y;
System.out.println("机器人已移动到 (" + x + ", " + y + ")");
}
// 不实现 reset() - 直接使用 default 方法!
}
现在来调用两个方法:
public class Main {
public static void main(String[] args) {
Movable robot = new Robot();
robot.move(10, 20); // 机器人已移动到 (10, 20)
robot.reset(); // 机器人已移动到 (0, 0)
}
}
如果希望 Robot 以某种特殊方式重置——只需在类中重写 reset():
@Override
public void reset() {
System.out.println("机器人正在关机并返回基地!");
move(0, 0);
}
7. default 方法与多接口实现
当一个类实现多个接口时,default 方法尤其有用。但需要注意:如果两个接口包含签名相同的 default 方法,编译器会要求你显式解决冲突。
冲突示例
interface A {
default void show() { System.out.println("A"); }
}
interface B {
default void show() { System.out.println("B"); }
}
class C implements A, B {
@Override
public void show() {
// 显式选择要用哪个接口的 default 方法
A.super.show(); // 或 B.super.show();
}
}
8. 示意图:default 方法的调用机制
+-------------------+
| Movable |
|-------------------|
| +move(int, int) | <- 抽象方法
| +reset() | <- default 方法
+-------------------+
^
|
+-------------------+
| Robot |
|-------------------|
| +move(int, int) | <- 提供实现
| | (未实现 reset())
+-------------------+
|
调用 reset()
|
使用接口 Movable
中的默认实现
9. 使用 default 方法时的常见错误
错误 1:试图声明没有实现的 default 方法。
default 方法必须有方法体!如果你写成 default void foo();,编译器会立刻提示:“你是不是忘了写花括号?”
错误 2:来自不同接口的 default 方法发生冲突。
如果一个类实现了两个带有相同 default 方法的接口,你必须显式解决冲突——否则编译器不会让代码通过。
错误 3:试图声明与 Object 方法同签名的 default 方法。
不能在接口中提供 equals、hashCode 或 toString 的 default 实现——这些名字只能声明为抽象方法。
错误 4:忘了 default 方法并不是“魔法”,它只是个方便的工具。
default 方法并不改变“接口即契约”的原则。如果默认行为不适用——请在类中重写该 default 方法。
GO TO FULL VERSION