
我们画了这么多年的 UML 类图很多人其实一直没搞明白“类”和“接口”之间到底有哪几种关系。不夸张地说光一个依赖和关联的区别就能在面试里挂掉一半人。这篇博文我用项目里真实建模的经验把 UML 类图里类与接口、类与类之间的几种关系讲透顺带附上 StarUML、IDEA 生成类图和 Visio 画图的一些实操细节帮你把这块知识补完整。UMLUnified Modeling Language统一建模语言里的类图是面向对象设计里最常用的一种静态结构图。它解决的问题很直白把系统中的“对象”抽象成类把对象之间的“关联方式”用标准化的符号画出来让团队在写代码之前就对齐设计也让后来接手的人能快速理解代码结构。类、接口以及它们之间的关系就是类图里最核心的组成元素。你要能看懂并画出继承、实现、依赖、关联、聚合、组合这六种关系那类图基本就过关了。这篇内容适合谁看正在学 UML 的计算机专业学生、工作中需要画架构图或设计文档的后端/客户端开发、准备面试的求职者以及带团队做技术方案评审的 Leader。不管你是画图新手还是已经画了几年图的老手关系语义这块我建议你完整过一遍因为很多细节比如聚合和组合到底怎么区分、依赖和关联的边界在哪里就连一些框架源码的 UML 图也是画错的。1. 先理清 UML 类图到底画的是什么类图本质上是面向对象设计的一张“蓝图”。它把系统中参与协作的类、接口、枚举等模型元素摆出来然后用连线表示它们之间的关系。画类图不是为了好看而是为了回答几个问题系统里有哪些“角色”类这些角色各自能干什么方法角色之间怎么协作关系谁拥有谁、谁依赖谁把这个理清了写代码其实就是填坑的事。1.1 类图里的基本模型元素类图里最常见的模型元素有四种类Class用矩形框表示上中下三栏分别是类名、属性、方法。属性常用“可见性 名称:类型 默认值”的格式方法常用“可见性 名称(参数):返回类型”的格式。接口Interface在 UML 2.x 里接口的表示方式有两种一种是顶部标注«interface»的类矩形框另一种是“圆圈半圆弧”棒棒糖表示法。两种表示在工具里可以切换表达的含义一致。抽象类类名用斜体表示这是很多初学者容易忽略的细节。包Package把相关类分组对应 Java 里的 package、C 里的 namespace通常只在这张图特别复杂时才画出包边界。1.2 类和接口的本质区别类是可以直接实例化的具体类型抽象类除外需要子类实现后实例化它既有状态属性又有行为方法。接口则是一组契约它没有状态严格意义上 Java 8 之后的 static final 字段除外只声明能力。一个类可以实现多个接口但只能继承一个父类在 Java/C# 这种单继承语言里这是画继承关系时首先要牢记的约束。1.3 为什么关系比类本身更重要单独画一个类没有任何意义类的价值体现在它和其他类、接口的协作关系上。你在代码里看到的class B extends A、class C implements InterfaceD、class E { F f; }、class G { void x(H h) {} }这些在 UML 类图里各有各的对应符号。搞混了符号设计文档就失真了团队照着错误的图去实现很容易越走越偏。2. 六种关系的符号和语义一张表先看明白UML 类图中的关系主要有六种继承泛化、实现、依赖、关联、聚合、组合。它们分别是三条虚线和实线、三个箭头的组合变化我用一张表把它们拉通对比一下。关系类型英文名表示法箭头方向耦合强度代码标志继承泛化Generalization实线 空心三角箭头子类指向父类强extends实现Realization虚线 空心三角箭头实现类指向接口中implements依赖Dependency虚线 普通箭头使用者指向被使用者弱局部变量、参数、返回值关联Association实线 普通箭头可双向指向被关联方中成员变量聚合Aggregation实线 空心菱形菱形在整体端整体指向部分箭头可省中弱成员变量整体不管理部分生命周期组合Composition实线 实心菱形菱形在整体端整体指向部分箭头可省强成员变量整体管理部分生命周期先记一个总原则实线比虚线强实心比空心强箭头指向被依赖/被使用的方向。后面展开说。注意UML 规范里箭头方向是“从客户端指向服务端”——也就是谁持有谁、谁调用谁箭头就指向谁。很多网图把这个画反了看的时候多留个心眼。3. 逐一拆解六种关系的核心细节3.1 继承泛化——代码里的 extends继承描述的是“is a kind of”的关系也就是“是一个”。Student继承Person那么 Student 就是一个 Person。在类图上继承是实线 空心三角箭头三角箭头指向父类。画继承关系时要注意三点空心三角一定要画在父类那一端。方向反了整个图别人就看不懂了。抽象类作为父类时类名用斜体显示方法也可以标成斜体表示抽象方法。在工具里勾选isAbstract属性后类名会自动变斜体。子类可以重写父类的方法子类也可以调用父类的公共/受保护方法这些在类图上一般不额外标注但在方法上可以加override字样不同工具对这个的支持程度不一样。实际项目里我见过不少刚入行的同学把继承画成虚线箭头这就是把继承和实现搞混了。判断口诀很简单看到 extends 用实线空心三角看到 implements 用虚线空心三角。3.2 实现——代码里的 implements实现关系描述的是“类对接口契约的履行”。类实现了接口就必须实现接口里定义的抽象方法除非这个类是抽象类。类图上的表示是虚线 空心三角箭头指向接口。接口在 Java 8 之后可以有 default 方法在 UML 里也可以用构造型标注interface的矩形框来表示。具体画的时候接口名上方加«interface»字样成员方法不需要写方法体。实现关系有两个常见场景容易画错接口 A 继承接口 B这依然是继承关系是实线空心三角不是虚线空心三角。只有“类实现接口”才是实现关系“接口继承接口”是泛化关系。一个类实现多个接口类图里要画多条虚线空心三角箭头指向各个接口不要图省事把它们合并成一条线——除非你的图特别大工具里允许线束合并但语义上仍然是多个实现关系。3.3 依赖——代码里的局部变量、参数、返回值依赖是六种关系里耦合最弱的一种也是最容易被忽略或滥用的一种。它的含义是一个类在某个方法里“临时使用”了另一个类但这个类并不是它的成员变量。典型的代码形态有方法参数类型void save(Order order)说明OrderService依赖Order。方法返回值类型Order findById(Long id)说明依赖Order。方法内部 new 出来的类var uuid UUID.randomUUID()说明依赖 UUID。抛出的异常类型throws IOException也是依赖。类图上的表示是虚线 普通箭头箭头指向被依赖的类。实线是关联虚线是依赖这是区分它们的最直观特征。我画图时的一个心得依赖关系不要画太多因为每个方法都可能依赖好几个类画多了图就变成一团乱麻。一般只画“设计上有意义”的依赖比如跨模块的关键调用。类级别的依赖可以在代码生成类图时按需过滤像 IDEA 的 Diagrams 功能就可以右键选择过滤某些依赖。3.4 关联——代码里的成员变量关联关系是“has a”的弱拥有关系表达的是一个类把另一个类作为自己的属性持有。在代码里最典型的体现就是成员变量。public class Customer { private ListOrder orders; }这个例子中Customer和Order之间就是关联关系顾客持有订单列表。类图上是实线 普通箭头箭头指向被持有的类。双向关联可以不画箭头只画一条实线在线的两端标注多重性1、0..、1..。关联关系里最重要的细节是多重性Multiplicity它表达了对象之间的数量关系。常用的符号符号含义1恰好一个0..1零个或一个*零个或多个1..*一个或多个0..n零到 n 个这个信息在实际建模中非常关键。比如用户和订单一个用户可以有多个订单订单必须属于一个用户。那么画出来就是User 端标注 1Order 端标注 *。代码层面就是 User 里有 ListOrderOrder 里有 User user。关联还有一个容易忽略的点它可以有导航方向navigation direction。如果只画一条实线没有箭头表示双向关联双方都能看到对方如果画了箭头表示单向关联只有箭头起点能看到终点。代码层面对应单向关联只有一方有目标类的属性双向关联双方都有对方的引用。3.5 聚合——整体与部分但可以分开活聚合是一种特殊的关联关系表达的是“整体拥有部分但两者生命周期独立”的关系。典型的例子学校和老师。学校没了老师依然存在还可以去别的学校任教。班级和学生的关系也可以理解为聚合学生转到别的班照样上学。类图表示是实线 空心菱形菱形画在整体那一端拥有方。可以带箭头也可以不带箭头标准画法一般是从整体菱形端指向部分。聚合和普通关联在代码层面没有显著区别都是成员变量。区别在设计语义上如果整体销毁了部分还应该在就是聚合如果整体销毁了部分也必须跟着销毁就是组合。实际项目里怎么画出两者的区别建议从生命周期角度去判断。每次画图遇到拿不准的就问自己一句整体没了部分还活着吗活着 —— 聚合。一起没 —— 组合。3.6 组合——强拥有的生命周期绑定组合关系表达的是“整体和部分同生共死”。典型例子人和心脏。人死了心脏也就停止工作了从业务模型的角度心脏不能脱离人体而存在。订单和订单项也是组合订单删除了订单明细也没意义了。类图表示是实线 实心菱形菱形在整体端。实心这个细节是区分聚合和组合的唯一画法差异也是考试和面试里最常考的考点之一。在代码层面组合通常意味着整体类在构造时直接创建部分类的实例而不是外部传入public class Order { private ListOrderItem items new ArrayList(); }这里 Order 创建并管理 OrderItem 的生命周期OrderItem 无法脱离 Order 单独存在业务上是这样就是典型的组合。而如果 OrderItem 是外部传入的Order 只是引用它那可能只是聚合或普通关联。画组合关系时注意菱形一定要画在“整体”那一端不是部分端。很多人画反是因为把“OrderItem 挂在 Order 下面”理解成了 Order 指向 OrderItem但其实菱形所在的那一端是容器。3.7 接口之间的特殊关系接口不仅可以被类实现接口之间也可以继承比如public interface MapK,V { interface EntryK,V { } } public interface SortedMapK,V extends MapK,V { }SortedMap 继承 Map这是接口之间的泛化关系用实线空心三角。这种关系在集合框架的类图里很常见。还有一种情况接口可以作为“标记接口”Marker Interface比如 Java 里的 Serializable、Cloneable。它们没有方法只是给类打一个标签。在类图里类实现这些标记接口也是用虚线的空心三角箭头只是接口框里没有任何方法行。4. 类图工具的实操StarUML、IDEA、Visio 怎么画概念理清之后工具实操就是下一个坎。这里我把三个常用工具的用法和坑都过一遍。4.1 StarUML 画类图StarUML 是很多学校教学和中小团队首选的建模工具因为它免费、轻量支持 UML 2.x 的大部分图形。用 StarUML 画类图的基本步骤新建工程选择Model里的Class Diagram双击创建。左侧工具栏选择Class或Interface在画布上点击放置。双击类名在右侧Properties面板里填写属性Attribute和方法Operation注意可见性符号public、-private、#protected。选中工具栏的Generalization从子类拖到父类工具自动生成实线空心三角。选中Realization从实现类拖到接口工具自动生成虚线空心三角。选中Association、Aggregation、Composition、Dependency对应工具从一个类拖到另一个类。StarUML 里画聚合和组合默认生成的线可能没有菱形标记。这时候你需要右键点击连线在Format-Line里找到Source Ends/Target Ends的箭头样式设置把 End 类型改成Aggregation空心菱形或Composition实心菱形。这个操作隐藏得有点深很多新手在这里卡住。4.2 IDEA 里生成类图如果你是 Java 开发者最推荐的类图绘制方式不是手动画而是直接让 IDEA 帮你生成。IDEA 自带 Diagrams 功能能根据源码自动生成类图包含继承、实现、关联、依赖等关系还能在图上直接调整布局。操作很简单右键点击包名或类名选择Diagrams-Show Diagram快捷键 CtrlAltShiftU然后会自动打开一个类图标签页。在这个图里你可以右键空白处选择Show Dependencies/Show Popup等选项来控制显示哪些关系。右键某个类选择Show Implementations、Show Subtypes快速查看继承树。用Layout菜单下的Layout Diagonal、Layout Hierarchic调整布局让图更整齐。把自动生成的关系图导出成图片右键空白处Export to Image。IDEA 生成的类图会自动把extends画成实线空心三角implements画成虚线空心三角非常准确。不过它默认不显示关联关系成员变量对应的实线箭头只显示继承/实现/依赖。想显示关联关系需要在图里的空白处右键选择Show ...面板里勾选Fields和Methods可见性并且打开Show Dependencies的详细级别。具体菜单在不同 IDEA 版本里略有差异但核心入口都是 Diagrams。4.3 Visio 画 UML 类图Visio 是微软家的王牌绘图工具适合做架构文档、PPT 配图。用 Visio 画 UML 类图步骤如下新建图表搜索“UML”模板选择UML Class模板新版本叫UML 类。左侧形状栏里有类、接口、泛化、实现、聚合、组合、依赖等形状。把类形状拖到画布双击打开形状的“属性”窗口填写属性和方法。从工具箱拖出关系线将线的一端粘到类的边缘另一端粘到另一个类的边缘。Visio 的一个坑是默认的 UML 类模板里聚合和组合的菱形方向可能不对你需要右键连线选择设置形状格式在线的两端箭头样式里调整。另外Visio 画出来的关系线如果连错了位置拖动类时线可能不会跟着走这时要确保关系线的端点确实“粘”在了类的形状上而不是画在附近。4.4 工具选型建议根据我的实际经验快速看代码结构、做代码评审首选 IDEA 自动生成类图零成本、准确。设计阶段画草图、做教学示例StarUML 或 draw.io 都可以轻量易用。产出正式架构文档要嵌入 Word/ConfluenceVisio、draw.io 导出 PNG/SVG 效果好。多人协作建模可以考虑 PlantUML写代码生成图适合放 Git 仓库里做版本管理。如果你是个喜欢用 Markdown 写文档的人PlantUML 是个非常好的选择。它用文本描述类、接口、关系然后生成图片可以在 CI 里自动渲染避免手动画图的漂移问题。它的语法很直观startuml class User { -id: Long -name: String getName(): String } interface UserRepository { findById(id: Long): User } class UserServiceImpl implements UserRepository { findById(id: Long): User } UserServiceImpl -- UserRepository UserServiceImpl -- User enduml这段描述里implements生成实现关系--生成依赖--可以生成关联。这类工具特别适合“文档即代码”的团队。5. 画类图时的常见误区与排查心得最后这部分是我这些年看别人画图、自己画图踩坑总结出来的很多都是面试里反复被问到的点写出来给大家避雷。5.1 最容易画错的六个点问题错误做法正确做法判断依据继承和实现混用把 implements 画成实线implements 用虚线空心三角看代码关键字extends vs implements聚合组合不分统一画成实线箭头按生命周期语义区分整体销毁后部分是否存活依赖关联不分所有临时引用都画实线局部变量/参数/返回值用虚线看字段是否存在菱形端方向反菱形画在部分端菱形画在整体端谁是容器谁是成员箭头方向反箭头指向调用方箭头指向被调用方谁依赖谁没标多重性关联线两端空白标上 1、*、0..1业务规则决定数量5.2 依赖和关联到底怎么区分这是面试里出现频率最高的问题之一。很多人的困惑在于一个类持有另一个类的成员变量同时也把它作为方法参数传进来那到底算关联还是算依赖答案很简单关键在于这个引用存不存续。如果 A 的字段里存了 B 的引用这个引用在 A 的整个生命周期内都存在——这是关联。如果 A 只是在某个方法里临时用到 B方法执行完 B 就丢了——这是依赖。举个具体例子public class OrderService { private final OrderRepository repository; // 字段生命周期跟 OrderService 一样长 private final IdGenerator idGenerator; public Order createOrder(Customer customer, ListOrderItem items) { Order order new Order(idGenerator.nextId(), customer, items); // 依赖 Customer/OrderItem/IdGenerator return repository.save(order); // 依赖 OrderRepository } }上面的代码里OrderService 和 OrderRepository 是关联关系成员变量OrderService 和 Customer/OrderItem/Order 是依赖关系局部变量/返回值。画类图时可以把 OrderService 指向 OrderRepository 画成实线箭头而 OrderService 指向 Customer 等方法临时使用的类画成虚线箭头或干脆不画。5.3 聚合和组合的判断难题聚合和组合在代码层面几乎长得一样都是成员变量所以很多人干脆不区分全画成关联。这样虽然也能用但丢失了设计语义后来接手的人无法从类图里看出生命周期的约束。我分享一个实际判断技巧找 delete 操作的级联规则。如果删除整体时部分也跟着删除数据库里的级联删除代码里的 remove 连带清空就是组合。如果删除整体时部分对象仍然独立存在就是聚合。举例用户和地址用户删了地址可以留着换个人用——聚合。订单和订单项订单删了订单项毫无意义——组合。画图之前先想清楚业务语义不要机械地看到“有 List 字段”就画组合。实际上很多情况下所谓“组合”都只是普通关联。5.4 类图不是越细越好最后想提醒一点UML 类图是沟通工具不是代码的完整镜像。画图的核心目的是让人快速理解结构而不是把每一个 getter/setter、每一个工具方法都堆上去。一个类几十个方法全画出来只会让人发晕。我自己的经验是每个类在类图上最多列 3~5 个关键方法其余的在文档里说明。关联关系也只画模块间关键路径方法内部的细碎依赖忽略。让图保持“一眼能看懂”的状态比“完整无遗漏”重要得多。另一个实用技巧是分层画图。先画一张全局的包图Package Diagram展示各个模块之间的依赖关系再针对每个核心模块画一张类图展示具体的类与接口。这样既避免了单张图爆炸又保证了信息完整。IDEA 里自带的 Diagrams 可以直接从 Module 或 Package 层级下降非常方便。5.5 类图在团队协作中的实际妙用除了设计评审类图在团队里还有一个不太被提起的用途——新人 onboarding。让新人先看整体类图再去看代码理解速度会快很多。尤其是微服务架构下一个服务内部的领域模型到底怎么组织的类图比几千行的代码直观得多。我在项目中经常做的一件事是代码经过一个迭代之后用 IDE 自动生成的类图替换掉文档里过时的老图。因为文档里的图如果长期不更新就会变成一张“历史遗迹”比没有图更有害。自动生成类图或者 PlantUML 最大的价值就是解决了传统画图工具的“维护漂移”问题。如果团队里没有专门的建模工具我强烈建议在代码仓库里维护一套 PlantUML 源文件改代码时顺手改一下图成本低收益高。环顾当前主流的接口与类关系建模这个领域并不缺少知识缺少的是把知识落到图上、落到代码里的那份准确。我见过不少项目设计文档上画得漂漂亮亮的聚合组合代码里根本对不上号。类图的本质不是画得好不好看而是能否忠实地映射代码的结构让看图的人少走弯路。