1. 引言
在 Java 中,不能同时继承多个类。这是为了避免“菱形继承问题”(diamond problem)——当两个父类都定义了同名方法时,不清楚应采用哪一个实现。但接口可以实现任意多个!为什么?因为接口只是契约,不包含实现(Java 8 之前没有实现;自 Java 8 起允许有 default 和 static 方法)。因此不会产生代码继承上的混乱。
这就像现实生活中的情况:你可以同时是“司机”、“电脑用户”和“游泳者”。这些“接口”各自描述了一组能力,但并不会强迫你变成另一个人的复制品。
多接口实现的语法
在 Java 中,类可以在关键字 implements 之后用逗号分隔列出多个接口。下面是一个基础示例:
public interface Movable {
void move(int x, int y);
}
public interface Chargeable {
void charge();
}
public class Robot implements Movable, Chargeable {
@Override
public void move(int x, int y) {
System.out.println("机器人移动到点 (" + x + ", " + y + ")");
}
@Override
public void charge() {
System.out.println("机器人正在充电。");
}
}
在这个示例中,Robot 很万能:既能移动又能充电。就像现实中一样,技能越多,被邀请去面试的机会就越多!
2. 为什么需要这样做?实践示例
示例 1:对象的不同“角色”
设想你在设计一个游戏角色:
- 它可以移动(Movable)
- 可以攻击(Attackable)
- 可以保存到文件(Serializable —— 标准 Java 库中就有这个接口)
public interface Attackable {
void attack();
}
public class Hero implements Movable, Attackable, java.io.Serializable {
@Override
public void move(int x, int y) {
System.out.println("英雄移动到新位置。");
}
@Override
public void attack() {
System.out.println("英雄发动攻击!");
}
}
现在你的类可以用于各种不同的上下文:它可以被传入任何要求上述任一接口的方法中。
示例 2:组合标准接口
在标准 Java 库中,常见的接口有 Comparable(用于比较对象)和 Serializable(用于把对象保存到文件或通过网络传输)。有时我们希望对象两者兼具:
public class Person implements Comparable<Person>, java.io.Serializable {
private String name;
private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
现在可以对 Person 对象进行排序(例如在列表中),也可以把它们写入文件。
3. 特点与限制
每个方法只需一份实现
如果两个接口定义了签名相同的方法,你只需实现一次。示例:
public interface A {
void doSomething();
}
public interface B {
void doSomething();
}
public class MyClass implements A, B {
@Override
public void doSomething() {
System.out.println("为两个接口提供的 doSomething 实现。");
}
}
Java 不会报错——关键是签名必须一致。如果方法签名不同,它们就被视为不同的方法——每个都需要实现。
不存在“菱形继承问题”
与多重继承类不同,实现多个接口不会出现继承到同一个方法的两个不同实现的情况。Java 8 之前接口根本没有实现,而自从出现 default 方法之后——如果发生冲突,你必须显式地解决它(详细内容见下一讲)。
没有状态
接口不能包含普通字段(只能有常量——public static final)。因此也不会出现“两个父类型中同名字段”的混淆。
4. 示例:在一个类中实现多个接口
我们给示例应用(比如一个动物园)增加一些能力。假设有一些动物,既能移动也能发出声音:
public interface Movable {
void move(int x, int y);
}
public interface Soundable {
void makeSound();
}
public class Dog implements Movable, Soundable {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public void move(int x, int y) {
System.out.println(name + " 跑向 (" + x + ", " + y + ")");
}
@Override
public void makeSound() {
System.out.println(name + " 说:汪汪!");
}
}
public class Cat implements Movable, Soundable {
private String name;
public Cat(String name) {
this.name = name;
}
@Override
public void move(int x, int y) {
System.out.println(name + " 悄悄地靠近 (" + x + ", " + y + ")");
}
@Override
public void makeSound() {
System.out.println(name + " 说:喵!");
}
}
现在我们可以编写通用方法来处理任何“会移动”或“会发声”的对象:
public static void testMovable(Movable m) {
m.move(10, 20);
}
public static void testSoundable(Soundable s) {
s.makeSound();
}
public static void main(String[] args) {
Dog rex = new Dog("雷克斯");
Cat murka = new Cat("穆尔卡");
testMovable(rex); // 雷克斯 跑向 (10, 20)
testSoundable(murka); // 穆尔卡 说:喵!
}
当然,如果对象同时实现了两个接口,它既可以传给这边的方法,也可以传给那边的方法!
6. 有用的细节
如果接口发生冲突?
有时,两个接口会定义签名相同但语义不同的方法。比如,一个接口期望方法 reset() 重置坐标,而另一个则期望这个方法关闭设备。这种情况下需要格外谨慎:方法仍然只能实现一次,并且需要“同时具备”两种行为(或者至少选定一种要做的事情)。现实中这种情况不多见,但一旦遇到,就应该反思设计是否合理。
包含实现不同接口对象的集合示例
假设我们有一个包含实现不同接口的对象的列表。我们可以遍历它们并调用需要的方法:
Movable[] movables = {
new Dog("沙里克"),
new Cat("巴尔西克"),
new Robot()
};
for (Movable m : movables) {
m.move(0, 0);
}
对任意接口都可以做类似的事情。
7. 多接口实现中的常见错误
错误 1:未实现接口的所有方法。
如果类声明实现了某个接口,但少实现了其中任何一个方法——编译器会立即报错。不要遗漏任何方法,即使它们看起来“多余”。
错误 2:具有相同签名的冲突方法。
如果两个接口定义了同名同签名的方法,只需实现一次。但如果这些方法的语义不同,可能会导致混乱和缺陷。在这种情况下,最好重新审视架构设计。
错误 3:在类中用 extends 来“继承”接口。
在类中实现接口必须使用 implements,而不是 extends。例如:
public class MyClass implements A, B { ... } // 正确
public class MyClass extends A, B { ... } // 错误!
错误 4:试图直接创建接口对象。
接口是契约,不能直接创建:
Movable m = new Movable(); // 编译错误
只能创建实现了该接口的类的对象。
GO TO FULL VERSION