第14章 设计模式入门:单例、工厂、建造者与代理
本系列是《Java 高级用法》篇,承接《Java 基础入门》21 章的内容,面向已学完基础篇、想进阶的小白读者。这一章是"设计模式"的入门课:不追求背全 23 种模式,而是把最常用的 4 类讲透------单例、工厂、建造者、代理,顺带认识观察者模式。看完你能看懂框架源码里的一大半套路,面试也能聊出干货。
背景介绍 :设计模式(Design Pattern)是前辈们总结的"在特定场景下反复被验证的解决方案",相当于编程界的"菜谱"。它不是新语法,而是代码组织方式的经验总结。基础篇写的都是"一个类干一件事"的小程序,但真实项目动辄几十万行,怎么让类与类之间协作得优雅、可维护、可扩展?设计模式回答的就是这个问题。
本章目标:学完你能手写五种单例并说清各自优劣;能解释"为什么用工厂代替到处 new";能用 Builder 解决"构造器参数爆炸";能看懂动态代理是怎么回事(Spring AOP 的根基);面试被问"单例怎么保证线程安全""JDK 动态代理的原理""观察者和策略模式的区别"都能接住。

一、什么是设计模式,为什么要学
设计模式是"在特定场景下反复被验证的解决方案"。23 种经典模式按用途分成三大类:创建型 (怎么创建对象:单例、工厂、建造者)、结构型 (怎么组合类与对象:代理、装饰器)、行为型(怎么分配职责、怎么协作:观察者、策略)。本章主角是创建型 3 个 + 结构型 1 个(代理)+ 行为型 1 个(观察者)。
设计模式解决的典型痛点:代码重复、难以修改、到处 new 导致耦合(耦合指类与类之间依赖太深,改一个要动一片)。先看一个反面教材:
java
// 反面教材:创建"形状"的逻辑散落在各处,每处都要写一遍 if-else
public class BadCode {
public static void main(String[] args) {
String shape = "circle";
if ("circle".equals(shape)) {
System.out.println("画一个圆"); // 画图功能里
} else if ("square".equals(shape)) {
System.out.println("画一个方形");
}
if ("circle".equals(shape)) {
System.out.println("面积=πr²"); // 面积功能里又写一遍
} else if ("square".equals(shape)) {
System.out.println("面积=边长²");
}
// 如果再加一个 triangle(三角形),上面两处 if-else 都要改------漏改一处就出 bug
}
}
为什么要学设计模式? ① 面试必考(单例、工厂是高频题);② 看懂框架源码:Spring、MyBatis 内部大量使用工厂、代理、观察者等模式;③ 让代码更好改:需求变化时只改"该改的地方"(本章会反复强调这个)。但要记住:模式是工具不是目的,小项目硬套模式反而更难看,第六节扩展知识会专门讲"不要过度设计"。
二、单例模式:全局只有一个实例

单例模式(Singleton)保证一个类在整个程序里只有一个对象,比如线程池、数据库连接池、配置文件读取器都只需要一份。核心三点:构造方法私有(外部不能 new)、自己保存唯一实例、提供全局访问入口。类比:一个公司只能有一个 CEO,所有人都通过"董事长办公室"(getInstance)找他,不能自己另立门户。
1. 饿汉式:类加载时就创建
java
public class EagerSingleton {
// 类加载时就创建好,天生线程安全(线程安全=多线程同时访问不出错)
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {} // 构造方法私有,外部无法 new
public static EagerSingleton getInstance() { return INSTANCE; }
}
// 使用:怎么调用都是同一个对象
EagerSingleton a = EagerSingleton.getInstance();
EagerSingleton b = EagerSingleton.getInstance();
System.out.println(a == b); // 输出:true(同一个实例,== 比较的是引用地址)
为什么叫"饿汉"? 不管用不用,先把实例"吃"到嘴里。优点:简单、线程安全(JVM 保证类加载只发生一次,static final 字段初始化线程安全);缺点:类一加载就占内存,对象很重而没用它时就浪费了。适用:对象不重、程序启动就要用(如配置读取器)。
2. 懒汉式:用的时候才创建
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // 用的时候才创建
instance = new LazySingleton();
}
return instance;
}
}
为什么这个写法有坑? 两个线程同时第一次调用,可能都看到 instance == null 各自 new 一个------单例失效(产生两个对象)。这正是多线程的经典陷阱。用代码演示一下:
java
// 演示懒汉式线程不安全:100 个线程同时首次调用 getInstance
// 需要 import java.util.*
final java.util.Set<LazySingleton> instances =
java.util.Collections.synchronizedSet(new java.util.HashSet<>());
for (int i = 0; i < 100; i++) {
new Thread(() -> instances.add(LazySingleton.getInstance())).start();
}
Thread.sleep(1000); // 等所有线程跑完(正式代码不要用 sleep 等线程,这里只是演示)
System.out.println("实例个数=" + instances.size());
// 输出:可能大于 1(例如 2、3......线程越多越容易触发;有时碰巧是 1,但"有时对"不等于"安全")
为什么可能得到多个实例? instance = new LazySingleton() 不是一步:判断 null、创建对象、赋值。线程 A 判断完 null 还没赋值,线程 B 也判断 null 为真,两个线程各 new 一个------后赋值的覆盖先赋值的,先创建的那个对象就"丢了",但确实产生了两个对象。
3. 双重检查锁定:既懒加载又线程安全
java
public class DoubleCheckSingleton {
// volatile:保证多线程下 instance 的可见性(一个线程改了,别的线程立刻能看到)
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // 第一次检查:避免每次都加锁
synchronized (DoubleCheckSingleton.class) { // 加锁,只让一个线程进来
if (instance == null) { // 第二次检查:防止重复创建
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
为什么需要 volatile? instance = new Xxx() 底层不是一步:分配内存、初始化、赋引用。没有 volatile,另一个线程可能看到"还没初始化完"的半成品对象(指令重排导致"赋引用"先于"初始化"执行)。volatile 是这道题的考点,面试常问"为什么双重检查要加 volatile"------标准答案:防止指令重排,保证"看到 instance 非 null 时,它一定初始化完成"。
执行流程拆解(两个线程同时首次调用):线程 A 进锁创建实例;线程 B 第一次检查发现 null,等在锁外;A 释放锁,B 进锁做第二次检查------发现已经不是 null,直接返回 A 创建的实例。第二次检查就是"防重复创建"的关键。
4. 静态内部类:推荐的生产写法
java
public class HolderSingleton {
private HolderSingleton() {}
// 静态内部类:只有被用到时 JVM 才加载它(JVM 加载类天然线程安全)
private static class Holder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() { return Holder.INSTANCE; }
}
为什么推荐? 兼得饿汉的线程安全和懒汉的懒加载:Holder 不被调用就不加载;JVM 保证类加载只发生一次,天然线程安全还不用写锁。面试常问:"静态内部类和双重检查有什么区别?"------双重检查要写 volatile + synchronized,稍不注意就写错;静态内部类把"线程安全"交给 JVM 类加载机制,代码最不容易出错。
5. 枚举单例:最简洁也最安全
java
public enum EnumSingleton {
INSTANCE; // 枚举天然只有一个实例
public void doSomething() { System.out.println("枚举单例干活"); }
}
EnumSingleton.INSTANCE.doSomething(); // 输出:枚举单例干活
// 枚举单例也能带字段、带方法,和普通类一样用
public enum ConfigSingleton {
INSTANCE;
private String appName = "我的应用";
public String getAppName() { return appName; }
public void setAppName(String name) { this.appName = name; }
}
System.out.println(ConfigSingleton.INSTANCE.getAppName()); // 输出:我的应用
为什么枚举能当单例? JVM 保证枚举实例全局唯一,且天然防反射、防序列化破坏(第五节的扩展知识会演示"反射怎么破坏普通单例、却拿枚举没办法")。
五种写法对比(面试常考):
| 写法 | 线程安全 | 懒加载 | 防反射/序列化 | 缺点 |
|---|---|---|---|---|
| 饿汉式 | ✅ | ❌(类加载就建) | ❌ | 可能浪费内存 |
| 懒汉式 | ❌ | ✅ | ❌ | 多线程失效 |
| 双重检查 | ✅ | ✅ | ❌ | 细节多,易写错 |
| 静态内部类 | ✅ | ✅ | ❌ | 写法稍绕 |
| 枚举 | ✅ | 类加载时 | ✅ | 灵活性稍差 |
总结:面试答"你会用哪种"------静态内部类或枚举;要谈"为什么"------饿汉简单但可能浪费内存;懒汉省内存但需处理线程;双重检查性能好但细节多;静态内部类优雅;枚举最安全。
三、工厂模式:把"创建对象"集中管理
工厂模式的核心思想:不直接 new,而是让专门的"工厂"负责创建对象。调用方只关心"我要圆",不关心"圆怎么造"。类比:你去餐厅点"宫保鸡丁",不用自己买菜、切菜、炒菜------后厨(工厂)负责,你只报菜名。
1. 简单工厂
java
interface Shape { void draw(); }
class Circle implements Shape {
public void draw() { System.out.println("画圆"); }
}
class Square implements Shape {
public void draw() { System.out.println("画方形"); }
}
// 简单工厂:一个方法根据参数决定创建哪个对象
class ShapeFactory {
public static Shape create(String type) {
if ("circle".equals(type)) return new Circle();
else if ("square".equals(type)) return new Square();
throw new IllegalArgumentException("未知形状: " + type); // 参数不合法:快速失败,而不是返回 null
}
}
Shape s = ShapeFactory.create("circle"); // 调用方不再写 new
s.draw(); // 输出:画圆
// 边界:未知类型 -> 抛异常
try {
ShapeFactory.create("triangle");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出:未知形状: triangle
}
为什么好? 第一节反面教材里"散落的 if-else"被收拢到工厂一处,新增形状只改工厂,调用方完全不用动。缺点是:工厂里 if-else 越加越长(职责膨胀)------加一种形状就要改工厂,违反"开闭原则"。
2. 工厂方法:把"创建"延迟到子类
工厂方法把"创建对象"抽象成接口方法,由不同子类工厂各自实现------新增形状时不改旧代码、只加新类,符合"开闭原则"(对扩展开放、对修改关闭)。
java
interface ShapeFactory2 { Shape create(); }
class CircleFactory implements ShapeFactory2 {
public Shape create() { return new Circle(); } // 只负责造圆
}
class SquareFactory implements ShapeFactory2 {
public Shape create() { return new Square(); } // 只负责造方形
}
ShapeFactory2 factory = new CircleFactory(); // 想换形状就换工厂
Shape s = factory.create();
s.draw(); // 输出:画圆
// 新增三角形:只加两个类(Triangle + TriangleFactory),旧代码一行不改
简单工厂 vs 工厂方法 :简单工厂一个类集中判断,代码少但违反开闭原则;工厂方法每个形状一个工厂类,扩展不动旧代码但类变多。小项目用简单工厂,大项目用工厂方法。面试常问:"简单工厂算设计模式吗?"------不算在 23 种经典模式里,它是工厂方法/抽象工厂的简化版,但面试可以拿来对比着讲。
3. 抽象工厂:生产"一族"相关的产品
抽象工厂解决"一组产品要成套出现"的问题:比如一套 UI 皮肤(深色)包含按钮、复选框、输入框,它们必须风格统一,不能深色按钮配浅色复选框。
java
// 一族产品:按钮 + 复选框
interface Button { void render(); }
interface Checkbox { void render(); }
class DarkButton implements Button { public void render() { System.out.println("深色按钮"); } }
class DarkCheckbox implements Checkbox { public void render() { System.out.println("深色复选框"); } }
class LightButton implements Button { public void render() { System.out.println("浅色按钮"); } }
class LightCheckbox implements Checkbox { public void render() { System.out.println("浅色复选框"); } }
// 抽象工厂:声明"生产一族产品"的接口
interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
class DarkFactory implements UIFactory {
public Button createButton() { return new DarkButton(); }
public Checkbox createCheckbox() { return new DarkCheckbox(); }
}
class LightFactory implements UIFactory {
public Button createButton() { return new LightButton(); }
public Checkbox createCheckbox() { return new LightCheckbox(); }
}
UIFactory f = new DarkFactory(); // 一键切换整套皮肤
f.createButton().render(); // 输出:深色按钮
f.createCheckbox().render(); // 输出:深色复选框
UIFactory f2 = new LightFactory(); // 换皮肤只换工厂,调用代码不用改
f2.createButton().render(); // 输出:浅色按钮
为什么叫"抽象"工厂? 它把"工厂"本身也抽象成接口,客户端面对的是 UIFactory 接口而不是具体工厂------换皮肤 = 换工厂实现。真实场景 :Spring 的 BeanFactory 就是工厂接口,具体创建逻辑由 XmlBeanFactory、AnnotationConfigApplicationContext 等实现类各自负责。
四、建造者模式:一步一步"搭"出对象
建造者模式(Builder)适用于构造参数很多、且有很多可选参数的对象:把"构造过程"拆成一步步的方法调用,最后 build() 一次性产出对象。类比:组装电脑------主板、CPU、内存、显卡一步步选,最后装机师傅(build)一次性装好。
1. 你早就用过它:StringBuilder
java
// StringBuilder 就是 Builder 的经典例子:一步步 append,最后 toString
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World"); // 链式调用:每个 append 返回 this
System.out.println(sb.toString()); // 输出:Hello World
// 链式调用的秘密:append 返回 this,所以能一直 .append().append()
StringBuilder sb2 = new StringBuilder().append("A").append("B").append("C");
System.out.println(sb2); // 输出:ABC
为什么 StringBuilder 是 Builder? 它没有一次性传入所有内容,而是分步拼接,最后统一产出 String------正是"一步步搭、最后 build"的思想。
2. 自己写一个 PersonBuilder
java
public class Person {
private final String name; // 必填
private final int age; // 必填
private final String phone; // 选填
private final String email; // 选填
private Person(Builder b) { // 构造私有,只能通过 Builder 创建
this.name = b.name; this.age = b.age;
this.phone = b.phone; this.email = b.email;
}
// 静态内部类 Builder:字段与 Person 一一对应
public static class Builder {
private final String name; // 必填字段放构造器(不填根本编译不过)
private final int age;
private String phone = "无"; // 选填字段给默认值
private String email = "无";
public Builder(String name, int age) { this.name = name; this.age = age; }
public Builder phone(String phone) { this.phone = phone; return this; } // 返回 this 才能链式
public Builder email(String email) { this.email = email; return this; }
public Person build() {
// 在 build() 里做参数校验:不满足就抛异常,而不是造出"半残"对象
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("姓名不能为空");
}
return new Person(this);
}
}
public String toString() { return name + ", " + age + ", " + phone + ", " + email; }
}
Person p = new Person.Builder("小明", 20).phone("13800000000").build();
System.out.println(p); // 输出:小明, 20, 13800000000, 无(email 没设,用默认值"无")
// 边界:必填校验在 build 时触发
try {
new Person.Builder("", 20).build();
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 输出:姓名不能为空
}
// 边界:链式顺序随意,可读性好
Person p2 = new Person.Builder("小红", 18).email("hong@test.com").phone("13900000000").build();
System.out.println(p2); // 输出:小红, 18, 13900000000, hong@test.com
为什么用 Builder 而不是构造器? 参数一多,直接写构造器就要写一大堆重载组合(构造器重载爆炸):2 个必填 + 2 个选填就有 2² 种组合要写重载,更别说 10 个字段。Builder 让你"想设哪个就调用哪个方法",可读性好,还能在 build() 里做参数校验。何时别用:只有 2、3 个参数时用 Builder 反而啰嗦,直接构造器最合适。
五、代理模式:找个"经纪人"替你办事
代理模式(Proxy):不直接访问目标对象,而是通过一个"代理"间接访问。代理可以在调用前后附加额外逻辑------日志、权限校验、性能统计------目标类完全不用改。类比:明星不直接接商演,经纪人(代理)负责谈价格、签合同(附加逻辑),明星只管上台(核心业务)。
1. 静态代理:手动写一个代理类
java
interface UserService { void addUser(String name); }
// 目标类:只关心业务
class UserServiceImpl implements UserService {
public void addUser(String name) { System.out.println("添加用户: " + name); }
}
// 静态代理:和目标实现同一个接口,包一层
class UserServiceProxy implements UserService {
private final UserService target;
public UserServiceProxy(UserService target) { this.target = target; }
public void addUser(String name) {
System.out.println("[日志] 开始添加用户"); // 附加逻辑
target.addUser(name); // 调用真实业务
System.out.println("[日志] 添加完成"); // 附加逻辑
}
}
UserService proxy = new UserServiceProxy(new UserServiceImpl());
proxy.addUser("小明");
// 输出:[日志] 开始添加用户 / 添加用户: 小明 / [日志] 添加完成
为什么用代理? 日志直接写进业务类会每个方法重复;代理把"日志"和"业务"分开------业务类保持纯净,附加逻辑集中到代理。静态代理缺点:每个接口都要手写代理类,接口多了很累。
2. JDK 动态代理:运行时自动生成代理(回顾第 11 章反射)
第 11 章学过反射,动态代理正是反射的典型应用:不用手写代理类,程序运行时自动"造"一个。
java
import java.lang.reflect.*;
UserService target = new UserServiceImpl();
// 三个参数:类加载器、接口数组、调用处理器(InvocationHandler)
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{UserService.class},
(p, method, args) -> {
System.out.println("[权限] 校验通过"); // 附加逻辑:权限
Object r = method.invoke(target, args); // 反射调用真实方法
System.out.println("[日志] 调用完成: " + method.getName());
return r;
});
proxy.addUser("小红");
// 输出:[权限] 校验通过 / 添加用户: 小红 / [日志] 调用完成: addUser
动态代理的原理 :Proxy.newProxyInstance 在运行时生成 一个实现 UserService 接口的代理类(一个临时的 .class 字节码),代理类所有方法调用都会转发给 InvocationHandler 的 invoke 方法------我们在 invoke 里统一加逻辑,加什么、加多少都由我们说了算。一个 InvocationHandler 能代理任意接口,这就是"动态"二字的威力。
3. 动态代理的限制与 CGLIB
JDK 动态代理有个硬限制:目标必须有接口------它生成的代理类只能"实现接口",不能"继承类"。
java
// 没有接口的类:JDK 动态代理无能为力
class PlainService { // 注意:没有实现任何接口
public void doWork() { System.out.println("干活"); }
}
// Proxy.newProxyInstance(loader, new Class[]{PlainService.class}, ...)
// -> 抛 IllegalArgumentException: PlainService is not an interface
怎么办? 用 CGLIB(第三方库):它在运行时生成目标类的子类 ,通过继承实现代理,不需要接口。Spring 的 AOP 就是"有接口用 JDK 动态代理,没接口用 CGLIB"。面试常问:"JDK 动态代理和 CGLIB 的区别?"------JDK 代理要求接口、基于接口实现;CGLIB 基于继承、不要求接口、不能代理 final 类。
静态代理 vs 动态代理:静态代理"写死",一个接口一个类;动态代理靠反射在运行时生成,一个 InvocationHandler 能代理任意接口。Spring 的 AOP(面向切面编程)底层就是这个机制。
六、观察者模式:一喊多应(概念)
观察者模式(Observer):一个对象(被观察者)状态变化时,自动通知所有"关注它"的对象(观察者)。典型例子:按钮的点击监听器------你点了按钮,所有注册的监听器都会被回调。类比:主播开播(事件发生),所有关注他的粉丝(观察者)都收到通知。
java
import java.util.ArrayList;
import java.util.List;
interface Listener { void onEvent(String event); } // 观察者接口:监听器都实现它
// 被观察者:维护监听器列表,事件发生时挨个通知
class Button {
private final List<Listener> listeners = new ArrayList<>();
public void addListener(Listener l) { listeners.add(l); } // 注册监听
public void click() {
System.out.println("按钮被点击");
for (Listener l : listeners) l.onEvent("click"); // 通知所有观察者
}
}
Button button = new Button();
button.addListener(e -> System.out.println("监听器A: 记录日志"));
button.addListener(e -> System.out.println("监听器B: 弹出提示"));
button.click();
// 输出:按钮被点击 / 监听器A: 记录日志 / 监听器B: 弹出提示
为什么用观察者? Button 不需要知道"谁在听",只管喊一嗓子;新增监听器不用改 Button 代码。GUI 里的 addMouseListener、Spring 的事件监听都是这个思想。适用场景:一个变化要触发多个动作,且动作列表会变。
推模型 vs 拉模型 :上面的例子是"推"------被观察者主动把事件内容推给观察者;"拉"是被观察者只通知"我变了",观察者自己来取数据。推模型简单直接(本章用推),拉模型解耦更彻底。注意线程安全 :真实项目里监听器列表常用 CopyOnWriteArrayList 而不是 ArrayList,防止"遍历通知的同时有人注册监听器"导致并发修改异常(易错点有专门条目)。
扩展知识
1. 设计模式的六大原则(SOLID)
模式背后是一组原则。SOLID 五个缩写 + 迪米特法则:单一职责(一个类只干一件事)、开闭原则(对扩展开放、对修改关闭)、里氏替换(子类能替换父类)、接口隔离(接口别太胖)、依赖倒置(依赖抽象不依赖具体)、迪米特法则(少和陌生人说话)。面试被问"为什么这么设计"时往原则上靠,比背模式名更显功底。开闭原则用代码看最直观:
java
// 违反开闭原则:加一种支付方式就要改"支付处理"类
class BadPaymentProcessor {
void pay(String type) {
if ("alipay".equals(type)) { System.out.println("支付宝支付"); }
else if ("wechat".equals(type)) { System.out.println("微信支付"); }
// 加"银行卡支付"?又来这里加一个 else if------对修改开放了
}
}
// 遵守开闭原则:定义接口,新增支付方式 = 新增一个类,旧代码一行不改
interface Pay { void pay(); }
class Alipay implements Pay { public void pay() { System.out.println("支付宝支付"); } }
class WechatPay implements Pay { public void pay() { System.out.println("微信支付"); } }
// 新增银行卡支付:再写一个 BankCardPay implements Pay 即可------这就是"对扩展开放、对修改关闭"
2. 单例的破坏与防御:反射、序列化
普通单例(饿汉/懒汉/双重检查/静态内部类)有一个致命弱点:反射可以绕过私有构造方法 ,强行再 new 一个实例;反序列化也会创建新实例。枚举单例天生免疫这两招。
java
import java.lang.reflect.Constructor;
// 反射破坏双重检查单例:绕过私有构造,造出第二个实例
DoubleCheckSingleton s1 = DoubleCheckSingleton.getInstance();
Constructor<DoubleCheckSingleton> c = DoubleCheckSingleton.class.getDeclaredConstructor();
c.setAccessible(true); // 私有构造也能调
DoubleCheckSingleton s2 = c.newInstance(); // 需要在外层方法声明 throws Exception
System.out.println(s1 == s2); // 输出:false(两个不同实例!单例被破坏)
java
// 枚举单例免疫反射:反射创建枚举实例直接抛异常
Constructor<EnumSingleton> ec = EnumSingleton.class.getDeclaredConstructors()[0];
ec.setAccessible(true);
try {
ec.newInstance("x", 0); // 枚举构造器带 (String, int) 两个隐藏参数
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
// 输出:Cannot reflectively create enum objects(JVM 直接拒绝)
}
// 非枚举单例的序列化防御:加 readResolve 方法,反序列化时返回同一个实例
// protected Object readResolve() { return INSTANCE; } // 加到单例类里即可
面试常问:"单例怎么防反射/序列化破坏?"------答:用枚举,或者加 readResolve + 在构造器里做"已存在实例就抛异常"的检查。
3. 装饰器模式与代理模式的区别(别混淆)
两者都是"包一层",但目的完全不同:代理控制"访问权" (能不能调、调用前后加逻辑);装饰器增强"功能"(加能力)。装饰器可以层层包裹,比如咖啡加牛奶、再加糖:
java
interface Coffee {
double cost();
String desc();
}
class PlainCoffee implements Coffee {
public double cost() { return 8; }
public String desc() { return "原味咖啡"; }
}
// 装饰器:包一层,加料(持有"被装饰者",自己也是 Coffee)
class MilkDecorator implements Coffee {
private final Coffee coffee;
public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
public double cost() { return coffee.cost() + 2; }
public String desc() { return coffee.desc() + "+牛奶"; }
}
class SugarDecorator implements Coffee {
private final Coffee coffee;
public SugarDecorator(Coffee coffee) { this.coffee = coffee; }
public double cost() { return coffee.cost() + 1; }
public String desc() { return coffee.desc() + "+糖"; }
}
// 层层包裹:原味 -> 加牛奶 -> 再加糖
Coffee c = new SugarDecorator(new MilkDecorator(new PlainCoffee()));
System.out.println(c.desc() + " " + c.cost()); // 输出:原味咖啡+牛奶+糖 11.0
记忆口诀 :代理是"管门的"(管你能不能进),装饰器是"加料的"(给你加能力);Java IO 的 BufferedInputStream(new FileInputStream(...)) 就是装饰器经典案例。
4. 观察者模式进阶:事件三件套(EventObject / EventListener)
JDK 的事件体系(AWT/Swing、Spring 事件)由三件套组成:事件对象 (携带信息)、事件监听器接口 (观察者)、事件源(被观察者,负责广播)。模仿 JDK 风格写一遍:
java
import java.util.EventObject;
import java.util.EventListener;
import java.util.ArrayList;
import java.util.List;
// 1) 事件对象:携带事件信息(继承 EventObject,source 是"谁发的")
class OrderEvent extends EventObject {
private final String orderId;
public OrderEvent(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
// 2) 监听器接口:继承 EventListener 只是标记"我是监听器"
interface OrderListener extends EventListener {
void onOrderCreated(OrderEvent e);
}
// 3) 事件源:创建订单后广播给所有监听器
class OrderService {
private final List<OrderListener> listeners = new ArrayList<>();
public void addListener(OrderListener l) { listeners.add(l); }
public void createOrder(String orderId) {
System.out.println("创建订单: " + orderId);
OrderEvent event = new OrderEvent(this, orderId);
for (OrderListener l : listeners) l.onOrderCreated(event); // 广播
}
}
OrderService service = new OrderService();
service.addListener(e -> System.out.println("[短信] 下单成功: " + e.getOrderId()));
service.addListener(e -> System.out.println("[日志] 记录订单: " + e.getOrderId()));
service.createOrder("ORD-1001");
// 输出:创建订单: ORD-1001 / [短信] 下单成功: ORD-1001 / [日志] 记录订单: ORD-1001
为什么要学这套? Spring 的 ApplicationEvent / ApplicationListener、Swing 的 ActionEvent / ActionListener 全是这个套路。看懂三件套,读框架事件源码就有了地图。
5. 策略模式:把"算法"也变成可替换的
策略模式(Strategy):把一组"可互换的算法"抽成接口,运行时自由切换------和观察者一样属于行为型模式。最典型的例子是促销折扣:不同用户不同折扣规则,收银台"插什么策略按什么算"。
java
// 策略接口:一种折扣规则 = 一个策略
interface DiscountStrategy {
double calc(double price);
}
class NoDiscount implements DiscountStrategy {
public double calc(double price) { return price; }
}
class TenPercentOff implements DiscountStrategy {
public double calc(double price) { return price * 0.9; } // 9 折
}
class FullReduce implements DiscountStrategy {
public double calc(double price) { return price >= 100 ? price - 20 : price; } // 满 100 减 20
}
// 收银台:持有策略,可随时换
class Cashier {
private DiscountStrategy strategy;
public Cashier(DiscountStrategy strategy) { this.strategy = strategy; }
public void setStrategy(DiscountStrategy strategy) { this.strategy = strategy; } // 运行时换策略
public double checkout(double price) { return strategy.calc(price); }
}
Cashier cashier = new Cashier(new NoDiscount());
System.out.println(cashier.checkout(120)); // 输出:120.0(无优惠)
cashier.setStrategy(new FullReduce());
System.out.println(cashier.checkout(120)); // 输出:100.0(满 100 减 20)
cashier.setStrategy(new TenPercentOff());
System.out.println(cashier.checkout(120)); // 输出:108.0(9 折)
// 新增"会员 8 折":加一个 VipDiscount 类即可,Cashier 一行不用改(又是开闭原则)
为什么用策略模式? 不用策略,折扣逻辑就是一个巨大的 if-else 或者 switch(满减、打折、无优惠、叠加......越加越长);用了策略,每种规则一个类,新增规则不动旧代码。策略模式 vs 状态模式:策略是"主动换算法",状态是"状态变了行为自动变"------面试答出这层区别就加分。
6. 不要过度设计:模式是工具,不是 KPI
模式是工具不是目的,小项目硬套 5 种模式只会更难读。判断标准:这个模式是否解决了当下的真实问题------只有一个形状时工厂就是多余的。先写简单代码,出现重复或扩展痛点时再引入模式。
java
// 只有一个实现类时,工厂就是多余的
class PdfReport {
public void generate() { System.out.println("生成 PDF 报告"); }
}
// 简单直接(推荐):
PdfReport report = new PdfReport();
report.generate(); // 输出:生成 PDF 报告
// 过度设计(只有一个实现类,别这么写):
// 接口 + PdfReportFactory + 工厂方法,三层包装就为了 new 一个对象
// PdfReport r = PdfReportFactory.create(); // 没有任何收益,只有阅读成本
什么时候该上模式? ① 出现了重复代码(工厂/策略能消重);② 需求明确会变(开闭原则才有意义);③ 团队里大家都懂模式(否则沟通成本大于收益)。记住:先能跑,再优雅------过度设计比没有设计更糟。
易错点
❌ 错误 :懒汉单例不加锁,多线程下创建多个实例(单例失效)。
✅ 正确:用双重检查锁定 + volatile,或用静态内部类、枚举------三者都兼顾线程安全。面试手写推荐静态内部类,最不容易写错。
❌ 错误 :忘记把构造方法设为 private,外部随便 new,单例变"多例"。
✅ 正确:单例构造方法必须 private,只能通过 getInstance() 拿实例。
❌ 错误 :以为枚举不能当单例、或者"枚举单例没业务能力"。
✅ 正确:枚举实例天然全局唯一、防反射防序列化,还能写方法、带字段,是最安全的单例写法之一。
❌ 错误 :工厂类里堆所有创建逻辑,if-else 越来越长(职责膨胀)。
✅ 正确:按开闭原则拆分,新增类型时新增工厂/类(工厂方法/抽象工厂),而不是继续往一个工厂加分支。
❌ 错误 :写了 Builder 却忘了调用 build(),得到 null;或漏了必填字段校验。
✅ 正确:build() 才真正组装对象;必填字段放 Builder 构造器(不填编译不过),业务校验放 build() 里抛异常。
❌ 错误 :把代理模式和装饰器模式(Decorator)混为一谈。
✅ 正确:代理控制"访问权"(能不能调、调用前后加逻辑);装饰器增强"功能"(加能力)。代理由调用方间接使用,装饰器可层层包裹(咖啡加牛奶加糖)。
❌ 错误 :以为 JDK 动态代理能代理任何类。它只能代理有接口 的类。
✅ 正确:没有接口的类要用 CGLIB(基于继承生成子类);Spring AOP 的策略就是"有接口用 JDK 动态代理,没接口用 CGLIB"。
❌ 错误 :观察者的监听器列表用普通 ArrayList,遍历通知的同时有人注册监听器,抛 ConcurrentModificationException。
✅ 正确 :用 CopyOnWriteArrayList(写时复制,遍历时改也安全)或加锁。
❌ 错误 :以为"单例 = 线程安全"。单例只保证"一个实例",实例里的共享可变字段被多线程改照样出问题。
✅ 正确:单例 + 无状态(或不可变字段)才安全;有可变状态要加锁或用原子类。
❌ 错误 :以为饿汉/懒汉/双重检查/静态内部类单例"绝对安全"------反射、反序列化都能再造一个实例。
✅ 正确:要绝对安全用枚举单例;或加 readResolve + 构造器防反射检查。
JDK 8 / 11 / 17 版本调用方式
设计模式是代码组织思想,与 JDK 版本无关:本章所有模式在 JDK 8、JDK 11、JDK 17 下写法完全一致、都能运行。枚举单例、静态内部类、volatile、synchronized、反射动态代理,三个版本全部支持。版本差异只在一处:JDK 17 新增的 record 可以简化 Builder 里的数据类。
| 写法 | JDK 8 | JDK 11 | JDK 17 |
|---|---|---|---|
| 五种单例写法(饿汉/懒汉/双重检查/静态内部类/枚举) | 全部可用 | 全部可用 | 全部可用 |
| 工厂(简单工厂/工厂方法/抽象工厂)、Builder(普通类)、静态代理、观察者 | 全部可用 | 全部可用 | 全部可用 |
| JDK 动态代理(java.lang.reflect.Proxy + lambda) | 可用 | 可用 | 可用 |
| record(简化 Builder 中的数据类) | 无 | 无 | 新增,需 JDK 17 才能运行 |
java
// JDK 17 及以上:record 一键生成不可变数据类,替代手写 getter/toString
public record Address(String city, String street) {}
Address a = new Address("北京", "中关村");
System.out.println(a.city() + " " + a.street()); // 输出:北京 中关村(需 JDK 17 才能运行)
// record 自动生成:构造器、city()/street() 访问器、equals/hashCode/toString------不用手写
JDK 17 的一个注意点 :JDK 17 默认强封装(strong encapsulation),反射访问 JDK 内部类 会抛 InaccessibleObjectException(需加 --add-opens 才放行);但本章反射示例访问的是我们自己写的类,三个版本下都正常,不受影响。
迁移建议:三个版本下设计模式写法零差异,无需迁移。升级 JDK 17 后可以用 record 少写样板代码(比如 Person 的姓名/年龄数据用 record 承载),但模式本身的套路不变。
小结

- 设计模式是"特定场景下被验证的解决方案",不是语法,是代码组织经验;23 种模式分创建型、结构型、行为型三大类。
- 单例保证全局唯一:饿汉简单但可能浪费内存、懒汉有线程坑、双重检查要 volatile、静态内部类最优雅、枚举最安全(还防反射/序列化)。
- 工厂把"创建对象"集中管理:简单工厂一个类搞定但会膨胀;工厂方法扩展时不动旧代码;抽象工厂生产"一族"产品(成套皮肤)。
- Builder 适合参数多的对象,StringBuilder 就是现成例子;自己写 Builder 要记得最后 build(),必填字段放构造器、业务校验放 build()。
- 代理在调用前后附加逻辑:静态代理手写、动态代理靠反射运行时生成(Spring AOP 的根基);JDK 动态代理只能代理有接口的类,没接口用 CGLIB。
- 观察者"一喊多应",事件监听器就是它的应用;监听器列表注意线程安全(CopyOnWriteArrayList)。
- 扩展知识:SOLID 六大原则、单例的破坏与防御(反射/序列化/枚举免疫)、装饰器 vs 代理(管门的 vs 加料的)、事件三件套、策略模式(算法可替换)、不要过度设计。
- 模式是工具不是 KPI:先写简单代码,出现重复或扩展痛点时再引入模式;模式要服务于真实问题,别为模式而模式。
下一篇:第15章 日期时间与常用工具类