阶段:入门篇 | 建议时长:5 天 | 前置:02-面向对象与核心类.md、04-异常-IO-反射-注解.md
学习目标
- 能用一句话说清 23 种 GoF 设计模式里 ⭐15 种常用模式的意图与适用场景。
- 能手写双重检查锁单例、静态内部类单例,并说出各自被反射/序列化破坏的原因。
- 能对比 JDK 动态代理与 CGLIB,并把它接到 Spring AOP 上。
- 能在 JDK 和 Spring 源码里各指出 5 个模式的实际应用(这是面试最高频问法)。
- 会用策略模式重构一段 if-else,并讲出重构收益。
核心知识点
设计模式是什么
一句话:前人反复遇到的问题和对应的解法模板,不是必须照抄的规范。
- GoF《设计模式》23 种,分三类:创建型 5、结构型 7、行为型 11。
- 面试真实考法不是”背 23 种”,而是三问:① 这个模式解决什么问题;② JDK/Spring 里哪里用了;③ 你的项目里怎么用的。
- 使用顺序:先知道”不用它会有什么痛”,再记解法。硬背类图对面试毫无帮助。
SOLID 五大原则
面试官想听:每条能配一个 Java 里的反例或正例,而不是只念缩写。
| 原则 | 一行解释 | Java 例子 |
|---|---|---|
| S 单一职责 | 一个类只有 1 个被修改的理由 | 反例:UserService 又校验、又查库、又发邮件 → 拆成 Validator / Repository / MailSender |
| O 开闭 | 对扩展开放、对修改关闭 | 新增支付方式加 PayStrategy 实现类,不改 PayService 主干 |
| L 里氏替换 | 子类能替换父类且不破坏行为 | 反例:Square extends Rectangle,重写 setWidth 同时改高度,调用方预期崩了 |
| I 接口隔离 | 多个窄接口优于一个胖接口 | JDBC 的 Statement / PreparedStatement / CallableStatement 分层 |
| D 依赖倒置 | 依赖抽象,不依赖具体 | Controller 注入 UserService 接口而非 UserServiceImpl,Spring DI 是它的工程化实现 |
补充两句设计哲学:迪米特法则(只和直接朋友通信,a.getB().getC() 是坏味道)、合成复用原则(能用组合就别用继承)。
创建型模式(5 种)
单例 Singleton
保证一个 Class 只有一个实例,全局访问点。
| 写法 | 线程安全 | 懒加载 | 备注 |
|---|---|---|---|
饿汉 private static final INSTANCE = new X() | ✅ | ❌ 类加载即创建 | 最简单,实例小、无初始化异常时用 |
懒汉(直接 if (null) 加锁) | ✅ 但性能差 | ✅ | 每次调用都进 synchronized,不可取 |
| 双重检查锁 DCL | ✅ | ✅ | ⭐面试标准答案 |
| 静态内部类 | ✅ | ✅ | ⭐推荐,靠 JVM 类加载保证 |
| 枚举 | ✅ | ❌ | ⭐《Effective Java》最佳,天然防反射与序列化 |
public class Singleton {
// volatile 不能去:new 是"分配内存→初始化→指向引用"三步,无 volatile 时可能重排序,
// 别的线程拿到"引用非空但未初始化完"的半成品对象
private static volatile Singleton instance;
private Singleton() {
// 顺便挡一下反射:第二次构造直接抛异常
if (instance != null) throw new IllegalStateException("Already created");
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:无锁快速返回
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton(); // 第二次检查:防止并发重复创建
}
}
return instance;
}
}
静态内部类写法(推荐手写这个):
public class Singleton {
private Singleton() {}
private static class Holder { static final Singleton I = new Singleton(); }
public static Singleton getInstance() { return Holder.I; }
} // 内部类只有被引用时才加载,靠 ClassLoader 的初始化锁保证唯一且延迟
单例如何被破坏(必考追问链):
- 反射:
Constructor c = clazz.getDeclaredConstructor(); c.setAccessible(true); c.newInstance()能强开私有构造 → 构造器里判空抛异常可挡。 - 序列化:每次
readObject都新建实例 → 实现readResolve()返回原实例可挡。 - 多个类加载器:各自加载出一份,这也是 ThreadLocal/容器场景要小心的原因。
- 典型应用:Spring 单例 Bean、线程池、
Runtime、连接池、配置对象。
工厂方法 vs 抽象工厂
- 简单工厂(不算 GoF 模式):一个 static 方法
if/else按参数造对象。新增品类要改工厂 → 违反开闭。 - 工厂方法:每个产品一个工厂子类,新增品类只加类不改老代码。
Connection conn = dataSource.getConnection();——调用方不知道自己是 MySQL 还是 Druid 连接。 - 抽象工厂:一个工厂生产一整族相关对象(如跨数据库的
Connection+Statement+Dialect)。 - 一句话分辨:工厂方法解决”一个产品”的创建,抽象工厂解决”一族产品”的创建。JDK 里
Calendar.getInstance()、NumberFormat.getInstance()是工厂方法。
建造者 Builder
- 解决:参数很多且可选时不要用重叠构造器( telescoping constructor),也别用一堆 setter(对象可变、线程不安全、易漏字段)。
- JDK 代表:
StringBuilder(链式append返回 this)、Stream.Builder。Lombok 的@Builder就是这个模式。 - Lombok 的
@Builder就是这个模式,Spring Boot 项目里对象.builder().a(1).b(2).build()很常见。
原型 Prototype
- 从一个实例克隆新实例,
implements Cloneable+ 重写clone()。 - ⭐面试关联点:浅拷贝只复制引用(
List字段指向同一个对象),深拷贝递归复制。ArrayList.clone()是浅拷贝——这是易错点。 - 工程上更常用序列化/JSON 往返实现深拷贝,或 Spring 的
@Scope("prototype")(注意:Bean 的 prototype 每次getBean新建,但注入进单例后只创建一次)。
结构型模式(7 种)
代理 Proxy —— 面向切面的地基
- 意图:不改目标类,在调用前后插逻辑(日志、事务、权限、缓存)。
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 原理 | Proxy.newProxyInstance + 继承 Proxy 类,反射调用 InvocationHandler.invoke | 生成目标类的子类覆盖方法(字节码,基于 ASM) |
| 要求 | 目标必须实现接口 | 目标类不能是 final,方法不能 private/final/static |
| 默认 | Spring Boot 2.x+ 默认全用 CGLIB(1.x 是 JDK);@EnableAspectJAutoProxy(proxyTargetClass=true) | — |
| 性能 | 每次调用走反射(有优化但仍略慢) | 创建慢、调用快 |
Object proxy = Proxy.newProxyInstance(loader, interfaces,
(p, method, args) -> { // 唯一坑:method.invoke 的第一个参数必须是 target 而不是 proxy
long t = System.currentTimeMillis();
Object r = method.invoke(target, args);
System.out.println(method.getName() + " " + (System.currentTimeMillis() - t) + "ms");
return r;
});
- 一句话:Spring AOP 是代理模式的应用,动态代理是它的实现手段;事务失效的”同类内部方法调用不走代理”也是这个原理(见《Spring核心》)。
- 静态代理:手写
UserServiceProxy implements UserService,每个方法转发 + 加逻辑。缺点是每个接口都要写一份,所以才有动态代理。
装饰器 Decorator
- 意图:动态增强功能,比继承灵活,运行时任意组合。结构是”持有被装饰对象的引用 + 实现同一接口”。
- JDK 最典型:
new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8"))——InputStream套娃,每一层加一种能力(字节→字符→缓冲)。 - 与代理的区别:装饰器关注”加功能”,代理关注”控制访问”;装饰器通常由调用方层层手动包,代理一般由框架生成。
- 其他:
Collections.unmodifiableList(只读装饰)、Stream 的中间操作、Spring 的HttpServletRequestWrapper(XSS 过滤)。
适配器 Adapter
- 意图:接口不兼容时做一次转换,让本来不能合作的类能合作。
- 一句话代表:SpringMVC 的
HandlerAdapter——Controller 有@RequestMapping、HttpRequestHandler、旧Controller接口好几种形态,DispatcherServlet 只认handle()一个方法,中间靠适配器各转一份。 - 生活化例子:电源转换头;代码例子:把
Dog.bark()适配成Robot.beep()。日志门面 SLF4J 底下换 Logback/Log4j2 也是适配器思路。
外观 Facade
- 意图:给复杂子系统提供一个简单统一入口,降低耦合。
- 例子:
Slf4jLogger、Spring 的JdbcTemplate(把Connection/PreparedStatement/ResultSet/异常处理全包成一个update(sql, args))、你写的XxxService聚合多个 DAO + RPC。 - 与适配器区别:外观是自己设计的简化层,适配器是为了兼容已有的不匹配接口。
桥接 Bridge
- 抽象与实现各自独立变化,用组合而不是继承把两者连起来。
- 例子:形状(圆/方)× 颜色(红/蓝),继承会爆炸成 4 个类,桥接是
Shape持有Color接口。JDBC 驱动 API(java.sql抽象 + 各厂商实现)是标准案例。 - 一句话:开闭原则在”两个维度都要扩展”时的落地。
组合 Composite
- 把”单个对象”和”对象树”用同一接口处理,客户端不用区分叶子还是分支。
- 例子:文件系统(
File既能表示文件也能表示目录)、UI 组件树、评论的父子嵌套回复(树形结构菜单/部门表就是组合模式的数据库版)。
享元 Flyweight
- 意图:大量细粒度对象共享同一份来省内存。
- JDK 里你已经学过三个:
Integer缓存 -128~127、字符串常量池、Boolean.valueOf()返回两个固定实例。 - 追问:享元与缓存的区别——缓存是”用完可能淘汰”,享元是”设计上共享不可变实例,由工厂统一管”。
- 反面提醒:缓存的对象必须不可变,否则一个持有者改了全都受影响。
行为型模式(11 种,重点 4 个)
策略 Strategy
- 意图:把可互相替换的算法各自封装成类,客户端只选不用改。
- 用途:干掉成串
if/else或switch。
// 重构前
public BigDecimal fee(String type, BigDecimal amt) {
if ("WECHAT".equals(type)) return amt.multiply(new BigDecimal("0.006"));
else if ("ALIPAY".equals(type)) return amt.multiply(new BigDecimal("0.004"));
else if ("CARD".equals(type)) return amt.add(new BigDecimal("1"));
else throw new IllegalArgumentException(type);
}
// 重构后:策略接口 + 注册表
public interface PayStrategy {
String type();
BigDecimal fee(BigDecimal amt);
}
@Component
public class PayContext {
private final Map<String, PayStrategy> map;
public PayContext(List<PayStrategy> list) { // Spring 自动注入所有实现
this.map = list.stream().collect(Collectors.toMap(PayStrategy::type, s -> s));
}
public BigDecimal fee(String type, BigDecimal amt) {
PayStrategy s = map.get(type);
if (s == null) throw new IllegalArgumentException("unsupported: " + type);
return s.fee(amt);
}
} // 新增渠道只加一个 @Component 类,PayContext 一行不改
- JDK 例子:
Comparator、ThreadPoolExecutor.AbortPolicy等拒绝策略、Arrays.sort(a, cmp)。 - 一句话:策略模式 = 把 if-else 的每个分支变成一个 bean,用 Map 分发。
模板方法 Template Method
- 意图:父类定算法骨架,把某些步骤留给子类实现(钩子方法)。
- 例子:
JdbcTemplate(骨架固定,SQL 和 RowMapper 你给);AbstractQueuedSynchronizer的tryAcquire/tryRelease由ReentrantLock、Semaphore各自实现——AQS 是模板方法 + 钩子的经典(见《01-多线程与并发编程》);InputStream.read()、ServletHttpServlet.service()分发到doGet/doPost。 - 实现手段:抽象类(编译期约定)或接口
default方法(Java 8 后)。 - 与策略区别:模板方法靠继承复用流程(编译期固定),策略靠组合替换算法(运行期可换,更灵活)。
观察者 Observer
- 意图:一对多依赖,一个对象状态变化自动通知一批订阅者,发布者不认识订阅者。
- JDK/Spring:
EventListener体系、Observable(已废弃);Spring 的ApplicationEventPublisher.publishEvent(new OrderCreatedEvent(...))+@EventListener(还有@Async、@TransactionalEventListener)。 - 心智模型延伸:消息队列 MQ 就是跨进程的观察者——生产者只管发消息,消费者自己订阅(见《02-RabbitMQ》)。
- 追问点:同步观察者仍在同一线程(阻塞主流程,靠
@TransactionalEventListener(AFTER_COMMIT)或异步线程池解);观察者注册顺序、内存泄漏(注销订阅)。
责任链 Chain of Responsibility
- 意图:请求沿一串处理者传递,谁能处理谁处理,发送方不需要知道有多少处理者。
- 例子:Servlet
Filter(chain.doFilter())、SpringMVCHandlerInterceptor(pre/post/after 三个时机,返回 false 就断链)、NettyChannelPipeline的Handler链(见《03-IO模型与网络编程》)、Logback 的 Filter、审批流、风控规则链。 - 和策略的区别:策略只命中一个;责任链可能多个依次处理,且链的顺序有语义。
其余 6 种(🟢 了解即可)
| 模式 | 一句话 | 代表 |
|---|---|---|
| 迭代器 | 顺序访问集合又不暴露内部结构 | Iterator、增强 for |
| 命令 | 把请求封装成对象,支持撤销/排队/日志 | Runnable、ThreadPoolExecutor 提交任务、undo 栈 |
| 状态 | 状态变了就换一个状态对象,行为随状态迁移 | 订单状态机、Thread 的 NEW→RUNNABLE→TERMINATED |
| 中介者 | 用一个中心对象拦住同事间的网状引用 | SpringMVC DispatcherServlet、MQ Broker、事件总线 |
| 访问者 | 在不改元素类的前提下给对象结构加新操作 | 编译器 AST 遍历、ASM 的 ClassVisitor |
| 解释器 | 语法规则各建一个类来解释句子 | SQL/正则解析、SpEL(用得极少) |
Spring 全家桶里的设计模式(面试最高频对照表)
| 模式 | Spring 里的落地 | 面试展开点 |
|---|---|---|
| 简单工厂 | BeanFactory.getBean() | 工厂方法版是 FactoryBean.getObject()(SqlSessionFactoryBean) |
| 工厂方法 | FactoryBean 接口 | 造的对象类型和 FactoryBean 自己不同类型,且可返回半成品 |
| 建造者 | BeanDefinitionBuilder、SpEL 解析、UriComponentsBuilder | 对应 Builder:分步构造 BeanDefinition |
| 原型 | @Scope("prototype") | 注入进单例只创建一次,要每次新对象需 ObjectProvider/lookup |
| 单例 | 默认 Bean 就是单例(容器级单例,非 JVM 单例) | 三级缓存解决循环依赖(见《Spring核心》) |
| 代理 | AOP / @Transactional / @Async | JDK vs CGLIB 选择规则、同类调用不走代理 |
| 模板方法 | JdbcTemplate、RedisTemplate、MyBatis 的 SqlSessionTemplate、RedisTemplate.execute() | 骨架管连接与异常,回调(RowMapper)给你扩展 |
| 观察者 | ApplicationEvent + @EventListener | 解耦非核心逻辑;@TransactionalEventListener 控制时机 |
| 责任链 | HandlerInterceptor、Filter、SecurityFilterChain | 顺序由注册顺序/@Order 决定 |
| 适配器 | HandlerAdapter、HandlerExceptionResolver、数据库 Driver | 多种 Controller 形态统一成一个入口 |
| 策略 | 资源加载 Resource(ClassPathResource/FileSystemResource/UrlResource) | 按位置各实现一套,既是策略也是适配器 |
一句话:问”Spring 用了哪些设计模式”,标准答案报 5 个——IoC 工厂、AOP 代理、JdbcTemplate 模板方法、事件观察者、拦截器责任链,每个带一个类名,就赢过大部分候选人。
动手实践
任务 1:手写两种单例并证明它安全
- 要求:写静态内部类单例 + DCL 单例;用 20 个线程并发
getInstance(),用Set<IdentityHashMap>收集所有返回对象。 - 验收标准:并发收集后不同实例数 = 1;再用反射
getDeclaredConstructor().setAccessible(true).newInstance()试图破坏,能说出为什么 DCL 版能被破坏、加了构造器判空后抛什么异常;再说出volatile去掉后可能出现什么问题(指令重排导致半成品对象)。
任务 2:策略模式重构 if-else
- 要求:把上面的
PayContext例子写完整(接口 + 3 个@Component实现 + Context),再加第 4 种支付方式,只新增文件。 - 验收标准:
PayContext里没有if/else/switch;新增渠道后原有代码 diff 为 0;PayContext的 Map 通过构造器注入List<PayStrategy>自动装配;未知 type 抛明确异常而不是返回 null。
任务 3:JDK 动态代理打印耗时
- 要求:给一个
UserService接口生成代理,对每个方法打印方法名 + 耗时,cost > 100ms时额外打印 “slow”。 - 验收标准:不修改
UserServiceImpl一行代码;能解释为什么Proxy生成的类不能同时代理两个已有父类(Java 单继承 → CGLIB 也受此限);说出method.invoke(target, args)传proxy会无限递归的原因。
高频面试题
- Q:单例有哪几种写法?如何保证线程安全?⭐ —— 饿汉/懒汉/双重检查锁 volatile/静态内部类/枚举五种;DCL 靠 synchronized+volatile,推荐静态内部类(JVM 类加载保证)或枚举 | 详见《01-多线程与并发编程》
- Q:单例会被什么手段破坏?怎么防?⭐ —— 反射开私有构造(构造器判空抛异常防)、序列化每次新建对象(实现 readResolve 防)、多类加载器;枚举天然免疫前两种 | 详见《04-异常-IO-反射-注解》
- Q:双重检查锁里 volatile 能去掉吗?为什么?⭐ —— 不能;new 分三步可能被重排序,其他线程第一重检查会拿到”引用非空但没初始化完”的半成品 | 详见《01-多线程与并发编程》
- Q:JDK 动态代理和 CGLIB 的区别?Spring 用哪个?⭐ —— JDK 基于接口+继承 Proxy 类走反射,CGLIB 生成目标类子类走字节码;目标没接口只能用 CGLIB,final 类/CGLIB 搞不了;Spring Boot 默认 CGLIB | 详见《Spring核心》
- Q:工厂方法和抽象工厂的区别?⭐ —— 工厂方法一个工厂造一个产品(新增加类不改老代码),抽象工厂一个工厂造一整族相关产品;DataSource.getConnection() 是工厂方法 | 详见《07-设计模式》
- Q:装饰器和代理有什么区别?⭐ —— 装饰器给对象叠加功能、由调用方层层包(InputStream 套娃);代理控制对目标的访问、通常由框架生成(AOP);结构一样、意图不同 | 详见《04-异常-IO-反射-注解》
- Q:策略模式和状态模式的区别?⭐ —— 策略是算法之间互不相识、由客户端选一个;状态是状态间有迁移规则、由各状态自己决定下一个;策略模式更轻、状态模式带状态机 | 详见《07-设计模式》
- Q:策略和模板方法怎么选?⭐ —— 流程步骤固定只有局部变化用模板方法(继承,编译期);整个算法可运行期互换用策略(组合,更符合开闭);策略可用 Map+Spring 注入落地 | 详见《07-设计模式》
- Q:Spring 里用了哪些设计模式?举例说明。⭐ —— BeanFactory=工厂、BeanDefinitionBuilder=建造者、@Scope prototype=原型、AOP/事务=代理、JdbcTemplate=模板方法、ApplicationEvent=观察者、HandlerInterceptor=责任链、HandlerAdapter=适配器 | 详见《Spring核心》
- Q:责任链模式在 Java Web 里哪里用?Filter 和 Interceptor 什么区别?⭐ —— Filter(Servlet 规范,容器管,拦 URL)、HandlerInterceptor(SpringMVC 管,能拿到 handler 和模型)、Netty Pipeline;区别在归属层、可用 API、能否注入 Spring Bean | 详见《SpringBoot实战》
- Q:你在项目里用过什么设计模式?(场景题)⭐ —— 答题口径:讲一个重构故事,不说模式清单。结构=原来的 if-else 长什么样 → 什么痛点(新增要改老代码、测试难)→ 用了什么模式 → 落地方式(接口 + Map + Spring 注入)→ 收益(新增零改动、单测能各测各的) | 详见《04-简历与求职指南》
- Q:SOLID 里的开闭原则怎么落地?⭐ —— 抽象 + 扩展:把变化点抽成接口,新增行为只加实现类;策略模式和观察者模式是它的两种主要手段;Spring 靠注入 List<接口> 自动收集实现 | 详见《Spring核心》
- Q:Integer 缓存和字符串常量池是什么模式?⭐ —— 享元;共享不可变实例省内存,Integer 缓存 -128~127、字符串常量池复用字面量;也解释了为什么超出范围 == 为 false | 详见《02-面向对象与核心类》
推荐资料
| 资料 | 用来干什么 | 链接 |
|---|---|---|
| JavaGuide 设计模式详解 | 23 种模式图解 + Java 代码,按”意图/结构/场景/优缺点”展开 | https://javaguide.cn/other/how-to-learn-design-pattern.html |
| pdai 设计模式 | 系统梳理 + 与 Spring 源码结合的完整版 | https://pdai.tech/md/dev-spec/pattern-all.html |
| 廖雪峰 Java 面向对象 | 回补 SOLID 的地基:继承/多态/接口 | https://liaoxuefeng.com/books/java/oom/index.html |
| 慕课 设计模式体系课 | 跟着敲一遍单例/工厂/代理/策略的手写实现 | https://www.imooc.com/course/list?c=designpattern |
| 码工具 设计模式在线演示 | UML 类图与结构对照,快速看图回忆模式形状 | https://www.matools.com/design-pattern |