01 一文读懂UML类图:核心概念与关系详解

1. 什么是类图?

想象一下,你要向朋友解释一个复杂的软件系统如何工作。你会从哪里开始?可能会先描述系统由哪些部分组成,每个部分负责什么功能,以及它们如何相互协作。这就是类图要做的事情。

1.1 类图的定义

类图(Class Diagram)是统一建模语言(UML)中最常用的一种结构图,它展示了系统的静态结构。简单来说,类图回答了三个关键问题:

  • 系统中有哪些类? - 识别出所有的类
  • 每个类包含什么? - 类的属性和方法
  • 类之间有什么关系? - 类如何相互连接和交互

1.2 为什么需要类图?

在我刚开始编程时,经常陷入"代码先行,设计后补"的误区。直到一个项目因为类关系混乱而不得不重构时,我才真正理解了类图的价值:

现实案例: 我们团队曾开发一个电商系统。起初,每个人都按照自己的理解添加类和方法。几周后,当需要添加促销功能时,发现OrderCartProduct类之间关系错综复杂,修改一个地方会引发多处错误。这时,我们画了一个类图,立即发现了问题:

  • 循环依赖
  • 职责不清的类
  • 缺失的关键关系
java 复制代码
// 重构前的混乱代码示例
class Order {
    public void addProduct(Product p) {
        // 直接操作Cart中的产品列表
        Cart.getInstance().getProducts().add(p);
    }
}

class Cart {
    public void createOrder() {
        // 复制了大量Order的逻辑
    }
}

// 重构后的清晰结构
class Cart {
    private List<CartItem> items;
    public Order checkout() {
        return new Order(this.items);
    }
}

class Order {
    private List<OrderItem> items;
    public Order(List<CartItem> cartItems) {
        // 转换逻辑
    }
}

1.3 类图的用途

  1. 设计阶段:可视化软件架构,发现设计缺陷
  2. 开发阶段:为程序员提供清晰的蓝图
  3. 文档阶段:作为系统设计的永久记录
  4. 沟通阶段:让非技术人员也能理解系统结构

2. 类图的组成

类图由三个基本元素构成:类、属性和方法。让我们通过一个具体的例子来理解。

2.1 类的表示

在类图中,一个类用一个三层的矩形表示:

复制代码
┌───────────────────┐  ← 类名层
│     Student       │
├───────────────────┤  ← 属性层
│ - name: String    │
│ - age: int        │
│ - id: String      │
├───────────────────┤  ← 方法层
│ + getName(): String│
│ + setAge(age: int)│
│ + study(): void   │
└───────────────────┘

访问修饰符符号:

  • + 公共(public) - 对所有类可见
  • - 私有(private) - 仅对本类可见
  • # 保护(protected) - 对子类可见
  • ~ 包内可见(package/default) - 对同包类可见

2.2 属性的详细语法

属性不仅仅是名称和类型,还可以包含更多信息:

复制代码
可见性 名称: 类型 [多重性] = 默认值 {约束}

示例:

java 复制代码
// 对应的Java代码
public class University {
    private String name;
    private List<Student> students = new ArrayList<>();
    private static final int MAX_CAPACITY = 10000;
    
    // 方法...
}

类图中的表示:

复制代码
- name: String
- students: List<Student> [0..*] = new ArrayList<>()
- MAX_CAPACITY: int {readOnly} = 10000

2.3 方法的完整表示

方法同样可以有详细的描述:

复制代码
可见性 方法名(参数列表): 返回类型 {约束}

示例对比:

java 复制代码
// Java代码
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public static double sqrt(double x) {
        return Math.sqrt(x);
    }
}

类图中的表示:

复制代码
+ add(a: int, b: int): int
+ sqrt(x: double): double {static}

2.4 特殊类型的类

1 抽象类
复制代码
┌───────────────────┐
│     <<abstract>>  │
│     Animal        │
├───────────────────┤
│ # name: String    │
├───────────────────┤
│ + eat(): void     │
│ + makeSound(): void {abstract}│
└───────────────────┘

抽象类名通常用斜体表示,或者加上<<abstract>>标记

2 接口
复制代码
┌───────────────────┐
│   <<interface>>   │
│    Drawable       │
├───────────────────┤
│ + draw(): void    │
│ + resize(): void  │
└───────────────────┘

接口通常用<<interface>>标记,方法都是抽象且公开的

3. 类之间的关系:依赖、关联、聚合、组合

这是类图最核心也最容易混淆的部分。理解这些关系的关键是思考类之间连接的强度和生命周期

3.1 依赖关系(Dependency)

一句话概括: "偶然的、临时的、弱的关系"

特点:

  • 最弱的关系类型
  • 临时性关联
  • 不会影响生命周期

场景:

  1. 方法参数
  2. 局部变量
  3. 静态方法调用
  4. 返回值类型

表示: 虚线箭头 →

复制代码
┌──────────┐      ┌──────────┐
│  Teacher │─────>│  Pen     │
└──────────┘      └──────────┘
      依赖(使用钢笔)

代码示例:

java 复制代码
class Teacher {
    // 依赖关系:Pen作为方法参数
    public void writeWith(Pen pen) {
        pen.write("Hello");
    }
    
    // 依赖关系:Pen作为局部变量
    public void signDocument() {
        Pen redPen = new Pen("red");
        redPen.write("Signature");
    }
}

class Pen {
    public void write(String text) {
        System.out.println(text);
    }
}

现实类比: 就像你去咖啡店点咖啡,你依赖咖啡师制作咖啡,但你们之间没有长期关系,咖啡师不是你的"一部分"。

3.2 关联关系(Association)

一句话概括: "长期的、结构性的关系"

特点:

  • 比依赖更强的关系
  • 长期存在
  • 通常表示为类的属性

表示: 实线箭头 → 或双向箭头

复制代码
┌──────────┐      ┌──────────┐
│  老师     │─────>│  Student │
└──────────┘      └──────────┘
      教导(一个老师教多个学生)

代码示例:

java 复制代码
class Teacher {
    // 关联关系:Teacher长期关联多个Student
    private List<Student> students;
    
    public Teacher() {
        this.students = new ArrayList<>();
    }
    
    public void addStudent(Student student) {
        this.students.add(student);
    }
}

class Student {
    private String name;
}

多重性表示:

  • 1 - 一个
  • *0..* - 零个或多个
  • 1..* - 一个或多个
  • 0..1 - 零个或一个
  • 1..3 - 一到三个

示例:

复制代码
┌──────────┐       1      ┌──────────┐
│ Teacher  │◄------------►│ Student  │
└──────────┘       *      └──────────┘
      一个老师对应多个学生

3.3 聚合关系(Aggregation)

一句话概括: "整体与部分,但部分可以独立存在"

特点:

  • 一种特殊的关联关系
  • 表示"has-a"关系
  • 部分可以独立于整体存在
  • 空心菱形指向整体

表示:

复制代码
┌─────────────┐       ┌──────────┐
│ Department  │◇─────>│ Teacher  │
└─────────────┘   *   └──────────┘
  部门包含老师,但老师可以独立存在

代码示例:

java 复制代码
class Department {
    // 聚合关系:部门包含老师,但老师可以独立存在
    private List<Teacher> teachers;
    
    public Department(List<Teacher> teachers) {
        this.teachers = teachers;  // 接收已经存在的老师
    }
    
    // 即使部门解散,老师仍然存在
    public void disband() {
        this.teachers = null;
        // 老师们还可以去其他部门
    }
}

class Teacher {
    private String name;
}

3.4 组合关系(Composition)

一句话概括: "强聚合,部分与整体共存亡"

特点:

  • 比聚合更强的关系
  • 表示"contains-a"关系
  • 部分不能独立于整体存在
  • 实心菱形指向整体

表示:

复制代码
┌──────────┐       1      ┌──────────┐
│  House   │◆─────>│  Room     │
└──────────┘       *      └──────────┘
  房子包含房间,房间不能独立于房子存在

代码示例:

java 复制代码
class House {
    // 组合关系:房子包含房间,房间不能独立存在
    private List<Room> rooms;
    
    public House() {
        // 创建房子时同时创建房间
        this.rooms = new ArrayList<>();
        this.rooms.add(new Room("客厅"));
        this.rooms.add(new Room("卧室"));
    }
    
    // 房子被拆除,房间也不复存在
    public void demolish() {
        for (Room room : rooms) {
            // 清理房间资源
        }
        this.rooms = null;
    }
}

class Room {
    private String type;
    
    public Room(String type) {
        this.type = type;
    }
}

3.5 四种关系的对比总结

关系类型 强度 生命周期 代码表现 箭头 现实类比
依赖 最弱 临时 局部变量、参数 虚线箭头 人与出租车
关联 中等 长期 成员变量 实线箭头 老师与学生
聚合 较强 独立 成员变量(外部传入) 空心菱形 车队与汽车
组合 最强 共存 成员变量(内部创建) 实心菱形 树与树叶

4 正确判断类图关系

遇到不确定的关系时,依次问这三个问题:

  • 问题1:生命周期绑定吗?
java 复制代码
java

// 问:A消失时,B还存在吗?
if (A.destroy() && B.stillExists()) {
    return "关联或聚合";
} else {
    return "组合";
}
  • 问题2:谁创建谁?
java 复制代码
java

// 问:B是由A创建的吗?
if (A.creates(B)) {
    return "组合";
} else if (B.isExternalTo(A)) {
    return "关联或聚合";
}
  • 问题3:有整体-部分概念吗?
java 复制代码
java

// 问:B是A的一部分吗?
if (B.isPartOf(A)) {
    // 整体-部分关系
    if (B.canLiveWithout(A)) {
        return "聚合";
    } else {
        return "组合";
    }
} else {
    return "关联";
}

常见错误:

  1. 过度使用组合:不是所有"包含"关系都是组合
  2. 忽略多重性:忘记标注关系的数量
  3. 箭头方向错误:依赖和关联的箭头指向被使用/被关联的类

记忆技巧:

  • 依赖:虚线 → 最弱 → "用过即弃"
  • 关联:实线 → 长期 → "你是我的"
  • 聚合:空心菱形 → "你属于我,但可以离开"
  • 组合:实心菱形 → "你是我的一部分,同生共死"

5 案例

5.1 需求:

  • 提供一个登录和注册用户的界面。
  • 提供一个人事信息的管理界面:展示全部员工信息,提供一个根据名称查询某个员工信息展示,添加员工信息,删除员工信息,修改员工信息。

5.2 流程图

需要创建的类:

  • 启动类:APP
  • 实体类:Employee User
  • 界面类:EmployeeManagerUI EditEmployeeUI AddEmployeeUI

5.3 类图

从代码里查看

  • APP:
    • 在App类内部创建LoginUI类: App依赖LoginUI
  • LoginUI

    • LoginUI类内部的login方法创建EmployeeManagerUI:LoginUI依赖EmployeeManagerUI
    • LoginUI类拥有User实体类的对象成员变量且创建类时就同时初始化了:LoginUI组合User
  • EmployeeManagerUI类

    • EmployeeManagerUI类的拥有AddEmployeeUI成员变量,并将自身引用传递过去:互相关联


    • EmployeeManagerUI类同时拥有Employee成员变量:关联
  • EditEmployee类同AddEmployee类,最终类图结果如下,使用了IDEA的UML导出功能

相关推荐
长安城没有风3 小时前
Java 高并发核心编程 ----- 线程池原理与实践(上)
java·juc
Remember_9933 小时前
Spring 核心原理深度解析:Bean 作用域、生命周期与 Spring Boot 自动配置
java·前端·spring boot·后端·spring·面试
风流倜傥唐伯虎4 小时前
java多线程打印
java·多线程
80530单词突击赢4 小时前
云原生时代:.NET与Java的K8s进化论
java
hhy_smile4 小时前
Special method in class
java·开发语言
我命由我123454 小时前
Android 开发 Room 数据库升级问题:A migration from 6 to 7 was required but not found.
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
黄筱筱筱筱筱筱筱4 小时前
7.适合新手小白学习Python的异常处理(Exception)
java·前端·数据库·python
Stecurry_304 小时前
Springboot整合SpringMVC --从0到1
java·spring boot·后端
Serene_Dream4 小时前
NIO 的底层机理
java·jvm·nio·mmap