CodeGym /课程 /JAVA 25 SELF /继承的问题与限制

继承的问题与限制

JAVA 25 SELF
第 17 级 , 课程 4
可用

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 类,DogCatBird 以及十来个其他类都从它继承。如果你改变了 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:为复用而盲目使用继承。
这常常导致类之间出现意外耦合,增加测试与维护成本。请选择组合与委托。

1
调查/小测验
继承和层级第 17 级,课程 4
不可用
继承和层级
继承和层级
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION