Java 中的组合、聚合和关联

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 上找到。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值