Java 中的继承和组合 (Is-a vs Has-a 关系)
最后更新:2018 年 4 月 10 日
1. 概述
继承和组合——以及抽象、封装和多态——是面向对象编程 (OOP) 的基石。
在本教程中,我们将介绍继承和组合的基础知识,并重点关注识别这两种关系类型的差异。
2. 继承的基础知识
继承是一种强大但被过度使用和滥用的机制。
简单来说,通过继承,基类(又称基础类型)定义了给定类型共有的状态和行为,并允许子类(又称子类型)提供该状态和行为的专门版本。
为了清楚地了解如何使用继承,让我们创建一个简单的例子:一个基类Person,定义人员的通用字段和方法,而子类Waitress和Actress提供额外的、更精细的方法实现。
这是Person 类
public class Person {
private final String name;
// other fields, standard constructors, getters
}
这些是子类
public class Waitress extends Person {
public String serveStarter(String starter) {
return "Serving a " + starter;
}
// additional methods/constructors
}
public class Actress extends Person {
public String readScript(String movie) {
return "Reading the script of " + movie;
}
// additional methods/constructors
}
此外,让我们创建一个单元测试来验证Waitress和Actress类的实例也是Person类的实例,从而表明“is-a”条件在类型级别得到满足
@Test
public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() {
assertThat(new Waitress("Mary", "[email protected]", 22))
.isInstanceOf(Person.class);
}
@Test
public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() {
assertThat(new Actress("Susan", "[email protected]", 30))
.isInstanceOf(Person.class);
}
重要的是要强调继承的语义方面。除了重用Person 类的实现之外,我们创建了一个明确的“is-a”关系,即基类型Person和子类型Waitress和Actress之间。“is-a”关系。 服务员和女演员实际上是人。
这可能会促使我们思考:在哪些用例中,继承是正确的选择?
如果子类型满足“is-a”条件,并且主要提供向下层级类中添加的功能,那么继承就是最佳选择。
当然,允许方法重写,只要重写的方法保留由利斯科夫替换原则 促进的基类型/子类型可替换性即可。
此外,我们应该记住,子类型会继承基类型的 API,在某些情况下可能过于复杂或仅仅是不希望的。
否则,我们应该使用组合。
3. 设计模式中的继承
虽然普遍认为应该尽可能优先使用组合而不是继承,但有一些典型的用例,继承仍然有其用武之地。
3.1. 分层超级类型模式
在这种情况下,我们使用继承将通用代码移动到基类(超级类型),按层级进行划分。
这是在领域层中这种模式的基本实现
public class Entity {
protected long id;
// setters
}
public class User extends Entity {
// additional fields and methods
}
我们可以将相同的方法应用于系统的其他层,例如服务层和持久层。
3.2. 模板方法模式
在模板方法模式中,我们可以使用基类来定义算法的不变部分,然后在子类中实现可变部分
public abstract class ComputerBuilder {
public final Computer buildComputer() {
addProcessor();
addMemory();
}
public abstract void addProcessor();
public abstract void addMemory();
}
public class StandardComputerBuilder extends ComputerBuilder {
@Override
public void addProcessor() {
// method implementation
}
@Override
public void addMemory() {
// method implementation
}
}
4. 组合的基础知识
组合是 OOP 提供的另一种用于重用实现的机制。
简而言之,组合允许我们对由其他对象组成的建模对象,从而定义它们之间的“has-a”关系。
此外,组合是关联的最强形式,这意味着组成或包含在某个对象中的对象在该对象被销毁时也会被销毁。
为了更好地理解组合的工作方式,假设我们需要使用表示计算机的对象。
计算机由不同的部件组成,包括微处理器、内存、声卡等等,因此我们可以将计算机及其每个部件都建模为独立的类。
下面展示一个简单的Computer类的实现方式
public class Computer {
private Processor processor;
private Memory memory;
private SoundCard soundCard;
// standard getters/setters/constructors
public Optional<SoundCard> getSoundCard() {
return Optional.ofNullable(soundCard);
}
}
以下类模拟了一个微处理器、内存和声卡(为简洁起见,接口已省略)
public class StandardProcessor implements Processor {
private String model;
// standard getters/setters
}
public class StandardMemory implements Memory {
private String brand;
private String size;
// standard constructors, getters, toString
}
public class StandardSoundCard implements SoundCard {
private String brand;
// standard constructors, getters, toString
}
很容易理解为什么推崇组合优于继承。在所有可以建立给定类与其他类之间语义上正确的“拥有-a”关系的情景中,组合都是正确的选择。
在上面的例子中,Computer类满足了与模拟其部件的类之间的“拥有-a”条件。
同样值得注意的是,在这种情况下,包含的Computer对象仅当所包含的对象不能在另一个Computer对象中使用时,才拥有这些对象的所有权。 如果可以,我们将使用聚合而不是组合,聚合不意味着所有权。
5. 没有抽象的组合
或者,我们可以通过在Computer类中硬编码依赖关系,而不是在构造函数中声明它们来定义组合关系。
public class Computer {
private StandardProcessor processor
= new StandardProcessor("Intel I3");
private StandardMemory memory
= new StandardMemory("Kingston", "1TB");
// additional fields / methods
}
当然,这将是一种僵化、紧耦合的设计,因为我们将使Computer类强烈依赖于Processor和Memory的具体实现。
我们将无法利用接口和依赖注入提供的抽象级别。
使用基于接口的初始设计,我们可以获得松耦合的设计,这也有利于测试。
6. 结论
在本文中,我们学习了 Java 中继承和组合的基础知识,并深入探讨了这两种关系类型之间的差异(“是-a” vs. “拥有-a”)。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















