2、UML类图详解:类、接口、继承、实现、关联、聚合、组合、依赖关系的UML表示

UML类图,说白了就是程序员之间的“建筑图纸”。

我刚入行那会儿,觉得画图浪费时间。代码能跑就行,画什么图?直到有一次接手一个老项目,光理清类之间的关系就花了两天。嗯,从那以后,我老老实实把UML捡起来了。

今天咱们就把UML类图中最核心的几种关系捋一遍。你想想看,搞懂了这些,看设计模式就跟看连环画一样简单。

2.1 类与接口的表示

先说说最基础的——类和接口长什么样。

类的表示

一个类用矩形框表示,分三层:

  • 第一层:类名(如果是抽象类,用斜体)
  • 第二层:属性(字段)
  • 第三层:方法(操作)

符号说明:

  • + 表示 public
  • - 表示 private
  • # 表示 protected
  • ~ 表示 package/default
  • 下划线表示静态成员
// 对应的Java代码
public class User {
    private Long id;
    public String name;
    protected int age;
    
    public void login() { }
    private void encryptPassword() { }
}

接口的表示

接口也是矩形框,但顶部加了个 <<interface>> 的构造型标记。或者你也可以在类名上方画个小圆圈(棒棒糖表示法)。我个人习惯用前者,更直观。

// 对应的Java代码
public interface UserService {
    void save(User user);
    User findById(Long id);
}

2.2 继承与实现

这两个关系最常用,也最容易混淆。

继承(泛化)

空心三角形 + 实线表示。三角形指向父类。

说白了就是“is a”的关系。猫是动物,学生是人。

// 父类
public class Animal {
    public void eat() { }
}

// 子类继承父类
public class Dog extends Animal {
    public void bark() { }
}

实现

空心三角形 + 虚线表示。三角形指向接口。

这就是“像契约一样”的关系——我承诺实现你定义的行为。

// 接口
public interface Flyable {
    void fly();
}

// 类实现接口
public class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("鸟儿飞翔");
    }
}
我的小技巧:记不住实线虚线?你就想——继承是“亲爹亲妈”,关系硬,用实线;实现是“干爹干妈”,关系虚一点,用虚线。

2.3 关联关系

关联是UML里最常用的关系。用实线箭头表示,箭头指向被关联的一方。

说白了就是“A知道B的存在”。比如一个老师知道他的学生是谁。

public class Teacher {
    private List<Student> students; // 老师关联了学生
    
    public void teach() { }
}

public class Student {
    private String name;
}

关联还可以带方向:

  • 单向关联:一个箭头,A→B
  • 双向关联:两个箭头,或者没有箭头的一条实线
  • 自关联:自己指向自己(比如树节点)

还可以标注多重性:

表示含义
0..1零或一个
1恰好一个
0..*零或多个
1..*一或多个
*任意多个

2.4 聚合与组合

这两个是关联关系的“加强版”,也是面试最爱问的。

聚合

空心菱形 + 实线箭头表示。菱形在整体那一端。

聚合是“has a”的关系,但整体和部分生命周期不同。部分可以脱离整体独立存在。

举个例子:一个班级有多个学生。班级解散了,学生还在,可以转到别的班。

public class ClassRoom {
    private List<Student> students; // 聚合关系
    
    public ClassRoom(List<Student> students) {
        this.students = students;
    }
}

public class Student {
    // 学生可以独立存在
}

组合

实心菱形 + 实线箭头表示。菱形在整体那一端。

组合是更强的“has a”关系,整体和部分生命周期相同。部分不能脱离整体独立存在。

我记得有一次重构代码,发现有人把组合用成了聚合,结果对象销毁时内存泄漏了。嗯,这个坑我踩过。

public class House {
    private Room room; // 组合关系
    
    public House() {
        this.room = new Room(); // 房间随房子一起创建
    }
    
    // 房子销毁时,房间也跟着销毁
}

public class Room {
    // 房间不能脱离房子独立存在
}
核心区别一句话:聚合是“整体没了,部分还在”;组合是“整体没了,部分也没了”。

2.5 依赖关系

依赖是最弱的关系。用虚线箭头表示,箭头指向被依赖的一方。

依赖说白了就是“临时用一下”。A的方法里用到了B,但A并不长期持有B的引用。

public class Driver {
    public void drive(Car car) { // 依赖关系:方法参数
        car.run();
    }
}

public class Car {
    public void run() { }
}

依赖的常见场景:

  • 方法参数
  • 局部变量
  • 静态方法调用
  • 返回值类型
我曾经踩过的坑:过度使用依赖注入框架,把本该是关联的关系硬写成依赖。结果代码可读性极差,维护起来想骂人。记住:能用关联就别用依赖,能用组合就别用聚合。

2.6 知识体系总览

下面这张图把UML类图的几种关系串起来了。我建议你保存下来,写代码前瞄一眼。

UML类图关系总览 类 (Class) + 属性 + 方法() <<interface>> 接口 + 方法() 继承(泛化) 实现 关联 实线箭头 被关联方 聚合 空心菱形 部分 组合 部分 依赖:虚线箭头(临时使用)

2.7 实战建议

说了这么多理论,来点实际的。

怎么判断用哪种关系?

我一般问自己三个问题:

  1. 是不是“is a”? → 继承或实现
  2. 是不是“has a”? → 关联(再细问:生命周期是否同步?同步→组合,不同步→聚合)
  3. 是不是“uses a”? → 依赖

画图的时机

我个人习惯在写代码前先画类图。不用画太细,把核心类和关系理清楚就行。画完图再写代码,思路清晰很多,重构次数也少了。

工具推荐:我平时用 StarUML 和 PlantUML。PlantUML 可以用代码画图,方便版本管理。你如果喜欢手绘,白板+手机拍照也行——关键是理清关系,不是画得多漂亮。

好了,UML类图的关系就这些。你想想看,是不是比想象中简单?下次看设计模式的时候,先看类图,再看代码,事半功倍。


公众号:蓝海资料掘金营,微信deep3321