1. Java 中的继承限制
只有类的单继承。 在 Java 中,一个类只能继承自另一个类。这称为单继承。例如,这样——可以:
class Animal { }
class Dog extends Animal { }
但这样——不可以:
class Animal { }
class Robot { }
// 错误! Java 不支持类的多重继承
class RoboDog extends Animal, Robot { }
如果尝试声明这样的类,编译器会提示:"class RoboDog cannot extend multiple classes"。为什么?因为多重继承会导致二义性:如果两个父类都有相同签名的方法,该使用哪一个?这就是著名的“菱形问题”(diamond problem)。
接口在 Java 中可以实现任意多个,不过我们尚未学习,稍后再谈。
构造器不继承。 即便父类有一个很方便的构造器,它也不会自动出现在子类中。需要在子类构造器里通过 super(...) 显式调用父类构造器。
私有成员不继承。 父类中所有私有(private)字段和方法在子类中都不可访问。它们“存在”于对象内部,但无法直接访问。
2. 脆弱层次结构的问题
类之间的强耦合。 当你创建类的层次结构时,子类会与父类紧密耦合。如果你修改了基类,这可能会影响(甚至破坏)它的所有子类。想象你有一个 Animal 类,Dog、Cat、Bird 以及十来个其他类都从它继承。如果你改变了 Animal 的结构(例如在构造器中添加了新的必填参数),就不得不遍历所有子类并更新它们的代码。在大型项目中这尤其痛苦。
“破坏性”继承的问题。 有时子类会无意中改变基类所依赖的行为。比如,父类在某个方法内部调用了自己的另一个方法,而子类重写了该方法并改变了其逻辑。结果父类的行为就不再符合预期。
class Animal {
void makeSound() {
System.out.println("Some sound");
}
void sleep() {
System.out.println("Animal is going to sleep...");
makeSound(); // 父类在另一个方法里调用了自己的方法
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.sleep();
}
}
程序会输出什么?
Animal is going to sleep...
Woof!
父类原本以为 makeSound() 会调用自己的实现,但实际却调用了子类版本!如果子类用不同的逻辑重写了方法,这可能导致意料之外的缺陷。
3. 脆弱基类问题 (fragile base class problem)
在大型项目中这是个真实存在的问题。如果你修改了基类(例如添加字段、改变方法实现),就有可能破坏所有子类的行为。有时问题不会立刻暴露,定位这类错误可能需要数小时甚至数天。
示例:假设你有一个带有 draw() 方法的 Shape 类。你决定在 Shape 中添加一个新的 drawShadow() 方法,并在其中调用 draw()。但某个子类(Circle)重写了 draw(),于是当对 Circle 调用 drawShadow() 时,行为可能会出乎意料。
4. 强耦合与重构困难
当类通过继承耦合在一起时,修改一个类就可能牵一发而动全身。这会降低代码的灵活性,增加重构与扩展的难度。有时为了添加新功能,你甚至不得不重写整条继承链。
现实中的例子
class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class Bicycle extends Vehicle { /* ... */ }
class Bus extends Vehicle { /* ... */ }
忽然来了个需求:“那我们加一个电动滑板车吧!”。但电动滑板车既是交通工具,也是电子设备。怎么办?如果你试图继续扩展这套层次结构以容纳所有新实体,它会很快变得难以管理。
5. 没有语义关系却为了复用而继承的问题
很多初学者(甚至不止初学者)会为了复用代码而滥用继承,即使类之间并不存在“是一个”(is-a)的关系。这会导致错误的架构。
错误继承示例
class DatabaseUtils {
void connect() { /* ... */ }
void disconnect() { /* ... */ }
}
class User extends DatabaseUtils { // 用户并不是 "数据库工具"!
String name;
}
更合理的做法是使用组合:将 DatabaseUtils 保持为独立的类,在需要的地方调用其方法,而不是从它继承。
6. 继承的替代方案
组合(has-a)
当一个对象“包含”另一个对象时,使用组合。例如,Car 类可以有一个 Engine 字段:
class Engine { /* ... */ }
class Car {
private Engine engine;
// ...
}
委托
与其去扩展某个类,不如把具体任务委托给另一个对象来完成。这样可以保持灵活性并降低组件间的耦合。
接口
在 Java 中,一个类可以实现任意多个接口。这使你可以在不依赖僵硬层次结构的情况下灵活组合行为。接口我们稍后会详细讨论。
何时该使用继承?
仅当类之间存在明确的“是一个”(is-a)关系时才使用继承:
- 猫是动物(Cat extends Animal)
- 圆是图形(Circle extends Shape)
- 管理员是用户(Admin extends User)
不要仅仅为了复用代码而使用继承——为此可以采用组合和委托。
7. 若干实践示例
示例:过度复杂的层次结构
class Animal { }
class Mammal extends Animal { }
class Cat extends Mammal { }
class PersianCat extends Cat { }
class SuperPersianCat extends PersianCat { }
如果你的层次结构超过了三层——就该停下来想一想了:是否到了该收手的时候?过深的层次结构会增加理解和维护的难度。
示例:扁平层次结构
class Animal { }
class Cat extends Animal { }
class Dog extends Animal { }
class Bird extends Animal { }
class Fish extends Animal { }
class Spider extends Animal { }
class Platypus extends Animal { }
class Dragon extends Animal { }
如果你有几十个子类,每个子类只在一个方法上有所不同,那么也许应该考虑使用接口或组合。
8. 使用继承的常见错误
错误 №1:没有“是一个”关系。
如果子类其实并不是父类的一种,架构就会变得别扭且迅速失控。比如,User 不应继承自 DatabaseUtils,哪怕这看起来“很方便”。
错误 №2:以改变契约的方式重写方法。
如果你重写了方法并改变其逻辑,使其不再符合父类的期望,就会引发意外错误。比如,若基类期望 draw() 用来绘制图形,而在子类中它却突然开始执行危险的副作用——这将是灾难性的。
错误 №3:层次过深或过于扁平。
过深的层次结构使代码难以理解;过于扁平则会导致重复。
错误 №4:企图绕过语言限制。
尝试用“权宜之计”(复制粘贴、“工具”型超类)来实现多重继承,最终只会带来混乱。
错误 №5:为复用而盲目使用继承。
这常常导致类之间出现意外耦合,增加测试与维护成本。请选择组合与委托。
GO TO FULL VERSION