1. 引言
对象之间有关系,无论是在现实生活中还是在编程中。有时很难理解或实现这些关系。
在本教程中,我们将重点介绍 Java 对三种有时容易混淆的关系类型的看法:组合、聚合和关联。
2. 组合
组合是一种“属于”的关系类型。这意味着其中一个对象是逻辑上更大的结构,其中包含另一个对象。换句话说,它是另一个对象的一部分或成员。
或者,我们经常称之为“有一”关系(与“是一”关系相反,后者是继承)。
例如,一个房间属于一个建筑物,或者换句话说,一个建筑物有一个房间。因此,基本上,我们称之为“属于”还是“有一”只是一个观点问题。
组合是一种很强的“有一”关系,因为容器对象拥有它。因此,对象的生命周期是紧密相连的。这意味着,如果我们销毁所有者对象,其成员也将随之销毁。例如,在我们前面的示例中,房间与建筑物一起被摧毁。
请注意,这并不意味着,如果没有其任何部分,容器对象就无法存在。例如,我们可以拆除建筑物内的所有墙壁,从而摧毁房间。但这座建筑仍将存在。
就基数而言,包含对象可以具有我们想要的任意数量的部分。但是,所有部件都需要只有一个容器。
2.1. UML
在 UML 中,我们用以下符号表示组合:
请注意,菱形位于包含对象处,是线的底部,而不是箭头。为了清楚起见,我们也经常画箭头:
因此,我们可以将此 UML 构造用于我们的“建筑物-房间”示例:
2.2. 源代码
在Java中,我们可以用一个非静态的内部类来建模:
class Building {
class Room {}
}
或者,我们也可以在方法体中声明该类。无论它是命名类、匿名类还是 lambda,都无关紧要:
class Building {
Room createAnonymousRoom() {
return new Room() {
@Override
void doInRoom() {}
};
}
Room createInlineRoom() {
class InlineRoom implements Room {
@Override
void doInRoom() {}
}
return new InlineRoom();
}
Room createLambdaRoom() {
return () -> {};
}
interface Room {
void doInRoom();
}
}
请注意,这是必不可少的,我们的内部类应该是非静态的,因为它将其所有实例绑定到包含类。
通常,包含对象想要访问其成员。因此,我们应该存储它们的引用:
class Building {
List<Room> rooms;
class Room {}
}
请注意,所有内部类对象都存储对其包含对象的隐式引用。因此,我们不需要手动存储它来访问它:
class Building {
String address;
class Room {
String getBuildingAddress() {
return Building.this.address;
}
}
}
3. 聚合
聚合也是一种“有一”关系。它与组合的区别在于它不涉及拥有。因此,对象的生命周期没有绑定:它们中的每一个都可以彼此独立存在。
例如,汽车及其车轮。我们可以取下轮子,它们仍然存在。我们可以安装其他(预先存在的)车轮,或者将它们安装到另一辆车上,一切都会正常工作。
当然,没有轮子或分离轮子的汽车不会像有轮子的汽车那样有用。但这就是为什么这种关系首先存在的原因:将零件组装成一个更大的结构,这个结构能够比它的零件做更多的事情。
由于聚合不涉及拥有,因此成员不需要仅绑定到一个容器。例如,三角形由线段组成。但是三角形可以共享线段作为其边。
3.1. UML
聚合与组合非常相似。唯一的逻辑区别是聚合是较弱的关系。
因此,UML 表示形式也非常相似。唯一的区别是钻石是空的:
对于汽车和车轮,我们会这样做:
3.2. 源代码
在Java中,我们可以使用普通的旧引用对聚合进行建模:
class Wheel {}
class Car {
List<Wheel> wheels;
}
成员可以是任何类型的类,但非静态内部类除外。
在上面的代码段中,两个类都有其单独的源文件。但是,我们也可以使用静态内部类:
class Car {
List<Wheel> wheels;
static class Wheel {}
}
请注意,Java 将仅在非静态内部类中创建隐式引用。因此,我们必须在需要的地方手动维护关系:
class Wheel {
Car car;
}
class Car {
List<Wheel> wheels;
}
4. 关联
关联是三者之间最弱的关系。它不是“has-a”关系,没有一个对象是另一个对象的一部分或成员。
关联仅意味着对象“知道”彼此。 例如,一位母亲和她的孩子。
4.1. UML
在UML中,我们可以用箭头标记关联:
如果关联是双向的,我们可以使用两个箭头,一个两端都有箭头的箭头,或者一条没有任何箭头的线:
我们可以在UML中代表一位母亲和她的孩子,然后:
4.2. 源代码
在Java中,我们可以用与聚合相同的方式对关联进行建模:
class Child {}
class Mother {
List<Child> children;
}
但是等等,我们怎么能判断引用是否意味着聚合或关联呢?
好吧,我们不能。区别只是合乎逻辑的:其中一个对象是否是另一个对象的一部分。
此外,我们必须在两端手动维护引用,就像我们对聚合所做的那样:
class Child {
Mother mother;
}
class Mother {
List<Child> children;
}
5. UML 旁注
为了清楚起见,有时我们希望在UML图上定义关系的基数。我们可以通过将它写到箭头的末端来做到这一点:
请注意,将零写为基数是没有意义的,因为这意味着没有关系。唯一的例外是当我们想要使用范围来指示可选关系时:
另请注意,由于在组合中只有一个所有者,因此我们不会在图表上指示它。
6. 一个复杂的例子、
让我们看一个(小)更复杂的例子!
我们将模拟一所大学,它有自己的部门。教授在每个部门工作,他们之间也有朋友。
在我们关闭大学后,这些部门还会存在吗?当然不是,因此它是一个组合。
但教授们仍将存在(希望如此)。我们必须决定哪个更合乎逻辑:我们是否将教授视为部门的一部分。或者:他们是否是部门的成员?是的,他们是。因此,它是一种聚合。最重要的是,教授可以在多个部门工作。
教授之间的关系是联想,因为说一个教授是另一个教授的一部分没有任何意义。
因此,我们可以使用以下 UML 关系图对此示例进行建模:
Java 代码如下所示:
class University {
List<Department> department;
}
class Department {
List<Professor> professors;
}
class Professor {
List<Department> department;
List<Professor> friends;
}
请注意,如果我们依赖术语“has-a”,“属于”,“成员”,“部分”等,我们可以更容易地识别对象之间的关系。
7. 结论
在本文中,我们看到了组合、聚合和关联的属性和表示形式。我们还看到了如何在UML和Java中对这些关系进行建模。
像往常一样,这些示例可以在 GitHub 上找到。