对象之间在现实生活和编程中都有关系。有时很难理解或实现这些关系。 在本教程中,我们将重点介绍 Java
的三种关系类型,这些关系有时很容易混合:组合、聚合和关联。
组成是一种"belong-to"
的关系类型。这意味着其中一个对象是逻辑上较大的结构,其中包含另一个对象。
换句话说,它是其他对象的一部分或成员。 或者,我们通常称之为"has-a"
关系(而不是"is-a"
关系,即继承关系)。
例如,房间属于建筑物,换句话说,建筑物里有一个房间。因此,基本上,无论是称之为"belong-to"
还是"has-a"
只是一个观点的问题。
组合是一种强烈的"has-a"
关系,因为包含对象拥有它。因此,对象的生命周期是绑定的。
这意味着,如果我们销毁所有者对象,其成员也将销毁它。例如,在上一个示例中,房间会因建筑物而被摧毁。
请注意,这并不意味着,没有其任何部分,包含对象就无法存在。例如,我们可以拆掉建筑物内的所有墙壁,
从而摧毁房间。但是这栋建筑仍然存在。 就基数而言,容器可以具有我们想要的尽可能多的部件。
但是,所有部件都需要有一个容器。
在 UML 中,我们使用以下符号表示组合:
请注意,菱形位于包含对象处,是线的基础,而不是箭头。为了清楚起见,我们经常也画箭头:
因此,我们可以将此 UML 构造用于Building-Room的示例:
在 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;
}
}
}
聚合也是一种"has-a"
关系。它与组合的区别是:它不涉及拥有。
因此,对象的生命周期没有绑定:每个对象的生命周期可以彼此独立存在。
例如,汽车及其车轮。我们可以摘下轮子,它们仍然存在。我们可以安装其他(预先存在的)车轮,
或安装这些到另一辆车,一切都会工作得很好。 当然,没有轮子或分离的车轮的汽车不会像有轮子的汽车那样有用。
但这就是为什么这种关系首先存在:组装零件到一个更大的结构,这是能够比它的零件更多的东西。
由于聚合不涉及拥有,因此成员不需要仅绑定到一个容器。例如,三角形由线段组成。但三角形可以共享段作为它们的边。
聚合与组合非常相似。唯一的逻辑区别是聚合是较弱的关系。 因此,UML
表示也非常相似。唯一的区别是菱形块是空的:
对于汽车和车轮,那么,我们会做:
在 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;
}
关联是三者之间最弱的关系。它不是"has-a"
关系,没有一个对象是另一个对象的部分或成员。
关联仅意味着对象"知道"彼此。例如,母亲和她的孩子。
在 UML
中,我们可以使用箭头标记关联:
如果关联是双向的,我们可以使用两个箭头,一个两端带有箭头的箭头,或者一条没有任何箭头的线:
我们可以在 UML
中代表母亲和她的孩子,然后:
在 Java
中,我们可以像对聚合一样对关联建模:
class Child {}
class Mother {
List<Child> children;
}
但是,等等,我们如何判断引用是否意味着聚合或关联?
嗯,我们不能。只有逻辑上的区别:其中一个对象是否是另一个对象的一部分。
此外,我们必须在两端手动维护引用,就像使用聚合一样:
class Child {
Mother mother;
}
class Mother {
List<Child> children;
}
为了清楚起见,有时我们希望在 UML
关系图上定义关系的基数。
我们可以通过将其写入箭头的末端来执行此操作:
请注意,将零写成基数没有意义,因为它表示没有关系。唯一的例外是,当我们想要使用范围来指示可选关系时:
另请注意,由于在组合中正好有一个所有者,所以我们在图表上没有指明它。
让我们看一个(小)更复杂的例子!我们将为一所具有多个院系的大学建模。教授们在各个院系工作,他们互相也是朋友。
我们关闭大学后,这些院系会存在吗?当然不是,所以这是一个组合。 但是教授们仍然存在(希望如此)。我们必须决定哪个更合乎逻辑:
我们是否把教授当作院系的一部分。或者:他们是否是部门的成员?是的,是的。因此,它是一个聚合。除此之外,教授可以在多个部门工作。
教授之间的关系是关联性的,因为说教授是另一个教授的一部分是毫无意义的。 因此,我们可以使用以下 UML 关系图对此示例进行建模:
代码如下所示:
class University {
List<Department> department;
}
class Department {
List<Professor> professors;
}
class Professor {
List<Department> department;
List<Professor> friends;
}
请注意,如果我们依赖于术语"has-a"、"belong-to"、"member-of"和"part-of"
等,我们可以很容易地识别对象之间的关系。
在本文中,我们看到了组合、聚合和关联的属性和表示形式。我们还在 UML
和 Java
中看到了如何对这些关系建模。
与往常一样,这些示例可在 GitHub 上找到。
原文链接::https://www.baeldung.com/java-composition-aggregation-association
作 者:Attila Fejér
译 者:lee