Effective Java笔记:复合优先于继承

“复合优先于继承”(Composition over Inheritance)是软件设计中广泛推崇的一种设计原则。在代码复用时,我们应该优先选择复合(composition,亦称为组合),而不是直接使用继承(inheritance)。这个原则可以使代码更加灵活、健壮和易维护。

以下将从更深入的角度分析为什么复合优于继承(优缺点、设计思想等),并结合具体场景和详细代码示例详细阐述。


一、复合和继承的详细区别

1. 复合(Composition)

复合是什么?

复合是通过将另一个类的实例作为成员变量(字段)嵌入到当前类中来实现功能增强或代码复用,而不是直接继承。

复合的设计

复合通常利用**“Has-A”(拥有)**关系来表达一个对象由其他对象组成。例如:

  • 一个“汽车”有一个“发动机”——Car has a Engine
  • 一个“订单”有一个“地址”——Order has an Address
class Engine {
    public void start() {
        System.out.println("Engine started...");
    }
}

class Car {
    private Engine engine;   // 通过复合拥有其他类
    
    public Car(Engine engine) {
        this.engine = engine; // 在构造器中传入需要的依赖
    }

    public void startCar() {
        engine.start();     // 将行为委托给成员变量
    }
}

public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine); // 传入一个 Engine 对象
        car.startCar();            // 输出:Engine started...
    }
}
复合的优点
  • 松耦合: 复合松散关联了两个类,Car 不依赖于 Engine 的实现细节,只依赖它提供的接口或行为。复合使得类更加独立。
  • 灵活: 可以通过替代组合对象来改变行为(比如更换引擎)。
  • 封装: 组合的成员变量对外部是完全隐藏的,外界无法直接访问内部细节。

2. 继承(Inheritance)

继承是什么?

继承是通过“Is-A”关系(从逻辑上表示子类是父类的一种子类型)来复用父类的代码。子类继承了父类的字段和方法。

继承的设计

继承使用**“Is-A”**语义:一个对象可以被看作是它父类的一种类型。例如:

  • 一只鸟(Bird)是一个生物(Animal)——Bird is an Animal
  • 一辆电动汽车(ElectricCar)是一辆汽车(Car)——ElectricCar is a Car
class Animal {
    public void eat() {
        System.out.println("This animal eats food.");
    }
}

class Dog extends Animal { // Dog 是 Animal
    public void bark() {
        System.out.println("Dog barks!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();   // 来自父类
        dog.bark();  // 由子类实现
    }
}
继承的优点
  • 清晰的层次关系:Is-A 关系合理时,继承可以很好地提供代码复用和行为共享。
  • 少量代码复用: 子类通过直接继承父类,无需额外的代码就能复用父类的实现。

3. 复合 vs 继承

以下是复合和继承在详细设计上的区别对比:

特性复合继承
关系类型Has-A(拥有关系)Is-A(父子关系)
耦合性松耦合——改变复合成员对象内部实现不会影响外部强耦合——子类受父类实现的强依赖性
灵活性可以动态替换构成对象,行为更灵活行为固定,难以在运行时更改
封装性更容易保持封装,内部实现细节隐藏子类往往能直接访问父类的字段,封装性较差
代码复用通过组合委托复用代码通过继承实现代码重用
复杂性控制只需要维护组合关系的成员即可子类必须理解父类的代码(尤其在重写时)
适用场景动态行为延展、共享功能子类确实是父类的一种类型,行为一致时适用

二、为什么复合优于继承?

虽然继承可以提供代码复用,但由于其局限性和风险,在设计时我们更应该优先尝试使用复合。以下是复合优于继承的几个重要原因:


1. 继承会导致类之间的强耦合

强耦合是指子类和父类直接绑定在了一起,当父类的内部实现发生变化时,子类可能会无意中受到影响。例如,父类新增一个字段或修改一个方法,可能导致子类的行为和逻辑不再正确。

场景:继承带来的错误变更

class Parent {
    public String message = "Parent message";

    public void print() {
        System.out.println(message);
    }
}

class Child extends Parent {
    @Override
    public void print() {
        message = "Child message";
        System.out.println(message);
    }
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Child(); // 父类引用子类对象
        parent.print();  // 输出: "Child message"
    }
}

问题:子类重写 print 方法改变了父类字段 message 的值,改动父类 API 的隐含语义,可能引发意外的使用错误。此时使用复合可以避免这种情况:

使用复合解决

class Worker {
    public void work() {
        System.out.println("Work method of Worker class.");
    }
}

class Manager {
    private Worker worker; // 复合(组合)

    public Manager(Worker worker) {
        this.worker = worker;
    }

    public void delegateWork() {
        worker.work(); // 行为委托
    }
}

2. 继承破坏封装性

在继承关系中,子类可以直接访问父类的 protected 字段,这打破了封装原则,即父类的实现细节暴露给了子类。

  • 风险: 如果修改父类的字段,从而意外引发子类的 bug。
  • 解决: 在复合中,成员变量是私有的,对外界且做好隔离。

3. 继承限制了动态扩展的能力

继承行为是静态的,即在编译时确定了父子关系(依赖于静态类型),无法在运行时动态扩展功能。

示例:

// 示例:通过复合,动态替换行为
interface Engine {
    void start();
}

class GasEngine implements Engine {
    public void start() {
        System.out.println("Gas engine starts...");
    }
}

class ElectricEngine implements Engine {
    public void start() {
        System.out.println("Electric engine starts...");
    }
}

class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void setEngine(Engine engine) {
        this.engine = engine; // 动态切换引擎
    }

    public void start() {
        engine.start(); // 行为默认委托引擎
    }
}

public class Main {
    public static void main(String[] args) {
        Engine gasEngine = new GasEngine();
        Engine electricEngine = new ElectricEngine();

        Car car = new Car(gasEngine);
        car.start(); // Gas engine starts...

        car.setEngine(electricEngine); // 动态切换
        car.start(); // Electric engine starts...
    }
}

通过复合,可以动态替换行为,无需修改现有类。而继承的静态行为限制了这种扩展能力。


4. 继承可能引入无意义的 Is-A 关系

有时,子类和父类并没有真正的“Is-A”关系,即语义上它们并不符合父子逻辑。如果强行使用继承,只会引发后续的设计困扰。


三、总结设计原则

  1. 优先选择复合(组合)。

    • 避免子类和父类之间的强耦合性。
    • 增强灵活性,可以动态扩展或改变行为。
    • 隐藏实现细节,确保封装性。
  2. 在以下情况下可以选择继承:

    • 子类和父类存在明确的 Is-A 关系(例如:DogAnimal)。
    • 需要重用父类定义的一些通用行为和代码逻辑。
    • 父类是抽象类或接口,并定义了子类的契约关系。
  3. 两者结合使用:

    • 在继承的实现中,也可以依赖复合来减少类之间的耦合,确保设计的灵活性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值