UML 类图及六大关系详解:继承、实现、依赖、关联、聚合、组合
本文详细介绍了 UML 类图的基础概念及六大核心关系。内容涵盖普通类、抽象类和接口的表示方法,以及依赖、关联、聚合、组合、泛化和实现的定义与区别。通过 Java 代码示例和 UML 图形规范,阐述了各类关系的生命周期特点、耦合程度及绘制标准,帮助开发者准确设计系统架构。

本文详细介绍了 UML 类图的基础概念及六大核心关系。内容涵盖普通类、抽象类和接口的表示方法,以及依赖、关联、聚合、组合、泛化和实现的定义与区别。通过 Java 代码示例和 UML 图形规范,阐述了各类关系的生命周期特点、耦合程度及绘制标准,帮助开发者准确设计系统架构。

在软件工程中,UML 类图是描述系统静态结构的标准建模语言,而类之间的六种关系是理解系统设计意图的关键。无论是阅读现有系统源码,还是进行新功能的设计开发,准确识别类之间的关系都能显著提升代码质量和可维护性。
类图主要包含三部分:
类名:这个类有什么属性(数据) 方法:这个类能做什么

在所有类图形式中,常用的三种是:普通类、抽象类和接口。它们的表示方式略有差异,但结构一致。
普通类是系统中最基本的构成单元,可以直接实例化。
public class Student {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public void study() {
System.out.println("Studying...");
}
}

抽象类是一种不能被实例化的类,通常作为父类,被子类继承。它可以包含普通方法,也可以包含抽象方法。
public abstract class Person {
protected String name;
protected int age;
public void speak() {
System.out.println("Person speaking...");
}
public abstract void work();
}

接口表示一组行为能力,是一种完全抽象的类型。接口不包含实现,只规定方法签名。从 JDK 8 开始,接口可以包含普通方法(default 方法、static 方法)。
public interface Movable {
void move();
}
除了上图中常见的 <<interface>> 类风格画法外,UML 还允许使用更简洁的棒棒糖符号(Lollipop Notation)来表示接口:

UML 类图还包括枚举(Enum)、泛型类、模板类、元类等,但使用频率远不如普通类、抽象类、接口,因此本篇只介绍最常见的三种。
抽象方法的标准表示方式是在类图中将操作的名称以斜体显示。根据 UML 2.5.1 规范第 9.6.4 节(Operations Notation):
'An abstract operation or property is denoted by writing the name of the operation or property in italics.'
规范明确指出需要将操作的名称以斜体表示。在实际应用中,整个操作签名(包括方法名、参数和返回类型)通常以斜体显示,以保持视觉一致性。
说明:抽象方法优先以斜体表示,但也可以在操作签名后添加 {abstract} 注解。
抽象类的名称同样优先以斜体显示。根据 UML 2.5.1 规范第 9.2.4.1 节(Classifier Notation):
'The name of an abstract Classifier is shown in italics, where permitted by the font in use.'
作为备选或补充,可以在类名后或下方使用文本注解 {abstract},即:
'Alternatively or in addition, an abstract Classifier may be shown using the textual annotation
{abstract}after or below its name.'
说明:文献写 {abstract} 是规范允许的文本注解,实际绘图中通常用刻板印象 <<abstract>>。类名斜体和 {abstract}/<<abstract>> 都表示抽象类。
静态方法的标准表示方式是将特征名称(包括方法名、参数和返回类型)整体下划线。根据 UML 2.5.1 规范第 9.4.4 节(Static Features Notation):
'Static features are underlined in the compartments in which they appear.'
下划线覆盖整个特征名称,而不是仅在方法名前添加下划线前缀。
作为备选,可以在操作签名后添加 {static} 关键字,即:
'An Operation may be specified as static. This is shown by writing
{static}after the Operation signature.'
关于构造方法,UML 规范中没有强制要求在类图中显示构造方法。通常情况下,默认的无参数构造方法可以省略显示,因为它在大多数编程语言中是隐式存在的。只有当构造方法具有特殊参数、多个重载形式或在设计中具有重要意义时,才需要在类图中明确表示。
接口的表示必须在名称上方标注关键字 «interface»。根据 UML 2.5.1 规范第 10.4.4 节(Interface Notation):
'An Interface may be designated using the default notation for Classifier with the keyword «interface».'
仅通过斜体接口名称而不添加 «interface» 关键字不能明确表示接口,因为这无法与抽象类有效区分。
接口有两种主要的表示方式:
接口名称可以选择性地以斜体显示,以强调其抽象性质,但这不是必需的,因为接口的抽象性主要通过 «interface» 关键字来标识。
对于具有包可见性(default/package visibility)的特征,可以使用符号 ~ 来明确表示可见性,也可以省略可见性符号。在 UML 规范中,省略可见性符号表示该特征具有包可见性,因此对于包可见性的特征,既可以明确写出 ~,也可以不写任何可见性符号,两者含义相同。
在 UML 类图中,类与类之间最常见的关系一共有六种。每种关系都表示不同程度的'耦合'或'依赖',从最弱到最强关系如下:
class Printer {
void print(String content) {
System.out.println(content);
}
}
class Document {
String text;
Document(String text) {
this.text = text;
}
String getText() {
return text;
}
}
class Logger {
void log(String msg) {
System.out.println("Log: " + msg);
}
}
class Office {
Document processDocument(Document doc, Printer printer) {
// 方法参数依赖 Printer 和 Document
new Logger().log("Processing document"); // 局部变量依赖 Logger
printer.print(doc.getText());
return new Document("Processed: " + doc.getText()); // 返回值依赖 Document
}
}
图形表示:虚线 + 箭头,由依赖者指向被依赖者。

如果是双向依赖的情况,首尾连接处需画两个箭头,但是这种情况极其少见且不推荐。
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class Person {
Address address; // Person 持有 Address
}
图形表示:实线 + 箭头,由关联类指向被关联类。

有时会存在特殊的双向关联情况:类 A 持有类 B 的引用,同时类 B 也持有类 A 的引用。
class Person {
Address address; // Person 持有 Address
}
class Address {
Person person; // Address 持有 Person
}

接下来会介绍聚合 (Aggregation) 和组合 (Composition)。需要说明的是:聚合和组合关系实际上是特殊的关联关系。
关联(Association) ← 最宽泛 ├─ 聚合(Aggregation) ← 整体 - 部分,部分可独立 └─ 组合(Composition) ← 整体 - 部分,部分不可独立
聚合和组合一定是关联关系,但是关联不一定是聚合或组合关系
class Student {
String name;
Student(String name) {
this.name = name;
}
}
class School {
List<Student> students; // 学校聚合学生
void addStudent(Student s) {
if (students == null) students = new ArrayList<>();
students.add(s);
}
}
图形表示:实线 + 空心菱形,由部分指向整体。

为什么这是'聚合'?
因此:学生属于学校,但学生可以独立存在 → 这是'聚合'。
class Engine {
Engine() {
System.out.println("Engine created");
}
}
class Car {
private Engine engine = new Engine(); // 引擎组合在汽车中
Car() {
System.out.println("Car created");
}
}
图形表示:实线 + 实心菱形,由部分指向整体。

为什么这是'组合'?
因此:部分(Engine)不能脱离整体(Car) → 这是'组合'。
提醒:需要注意的是,聚合和组合作为特殊的关联关系,在图形表示上分别在整体端(包含端)使用
空心菱形和实心菱形来标识。至于另一端(部分端)是否需要画箭头,则取决于是否需要明确表达导航方向。 无论是聚合关系还是组合关系,从整体到部分的导航性都是关系本身固有的语义,因为整体都需要通过引用来访问部分对象。因此,在部分端画箭头可以更明确地表达这种导航关系,但由于这种导航性在聚合和组合关系的语义中都已经存在,画箭头都是可选的,可以根据需要明确导航方向时使用,不画箭头也是符合规范的。 这种处理方式在聚合关系和组合关系中是统一的,因为两种关系在导航性的语义上具有相同的特性:整体都需要通过某种引用机制来访问其组成部分,无论这种拥有关系是弱的聚合关系还是强的组合关系。
在 UML 中**依赖(Dependency)与关联/组合/聚合(Association/Composition/Aggregation)是完全不同的两类关系,判断规则也完全不一样。 依赖关系看的是'使用':只要一个类在代码里出现过另一个类型的名字(包括变量类型、参数类型、返回值、new 的右边),就表示它依赖该类型。依赖是最弱的关系,属于'用到就算'。 组合/聚合看的是'结构':它们映射的是类的内部组成关系,因此只根据成员变量的声明类型(左边)**来判断,不看 new 时实际创建的子类。因为 UML 描述的是静态设计结构,而不是运行时对象。
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
图形表示:实线 + 空心三角箭头,由子类指向父类。

interface Runnable {}
class Task implements Runnable {}
图形表示:虚线 + 空心三角箭头,由实现类指向接口。

在 UML 类图中,六种关系从弱到强的顺序通常为:
依赖 → 关联 → 聚合 → 组合 → 泛化(继承) → 实现
画类图时没有硬性规定必须用越强越好,整体原则是尽量按真实业务关系选择最准确的那一种:
最后需要强调:聚合与组合本质上都属于关联的更精确形式。一旦确认某关系是聚合/组合,就应该直接画它们,而不是退回画成普通关联。通过以上原则,就能让你的类图相对更加准确。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online