跳到正文
Java 入门到面试指南
返回

07-设计模式

阶段:入门篇 | 建议时长: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()、Servlet HttpServlet.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())、SpringMVC HandlerInterceptor(pre/post/after 三个时机,返回 false 就断链)、Netty ChannelPipeline 的 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 / @AsyncJDK 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