☰
Java设计模式实战总结:面试高频与源码落地全解析
2026/9/30 1:22:42 网站建设 项目流程

设计模式在Java圈子里就是个老生常谈但又绕不开的话题。你翻招聘JD,十有八九写着“熟悉常见设计模式”;你刷面试题,单例、工厂、代理几乎是必问;你读Spring、MyBatis源码,满眼都是各种模式的影子。但说实话,很多人学设计模式的方式有问题,要么死记硬背UML类图,要么对着Demo抄了一遍转头就忘,真正到了写业务代码的时候,脑子里还是一团浆糊。

这篇总结我不想搞成教科书式的罗列,而是站在实际开发的角度,把Java里最常见、面试最高频、源码最爱用的那些模式重新梳理一遍。每个模式我都会讲清楚三个问题:它是为了解决什么痛点而生的,它在Java生态里最经典的落地场景是什么,以及你自己写代码的时候应该怎么判断“这里该不该用”。适合准备面试的Java开发,也适合想提升代码质量但一直没找到正确姿势的初学者。

1. 设计模式到底在解决什么问题

1.1 别把设计模式当成银弹

很多人学设计模式之前会有一个误解,觉得用了设计模式代码就高级了,不用就显得low。这是完全错误的观念。设计模式本质上是一套“在特定场景下被反复验证过的解决方案模板”,它的核心价值是让你的代码具备可扩展性和可维护性,而不是让代码看起来更复杂。

举个很直白的例子:你的系统里只有一个if-else分支,判断用户类型然后返回不同折扣,这时候硬套一个策略模式,搞出一堆接口和实现类,属于典型的过度设计。但当折扣规则增加到十几种,而且每种规则还在不断变化的时候,if-else就会变成一座屎山,这时候策略模式才体现出真正的价值。

我的判断标准很简单:当代码的复杂度开始让你觉得修改一个需求要小心翼翼、生怕碰坏其他地方的时候,就是该引入设计模式的时候了。设计模式不是用来炫技的,它是用来给你“兜底”的。

1.2 六大原则是比模式本身更重要的东西

Gof的23种设计模式,背后其实只讲了六条设计原则。你要是把原则吃透了,很多模式你甚至能自己推出来。这也是为什么有些大牛在面试的时候会说“设计模式不重要,思想才重要”,本质上说的就是这些原则。

原则核心思想反面案例
单一职责一个类只负责一件事一个Service里既做订单处理又做用户校验
开闭原则对扩展开放,对修改关闭新增功能需要改原有代码而非新增代码
里氏替换子类必须能替换父类且行为正确继承后重写方法改变了原方法的预期行为
依赖倒置依赖抽象而非具体实现上层直接依赖底层具体类而非接口
接口隔离接口要小而专,不要大而全一个接口塞了十多个不相关的方法
迪米特法则一个对象应尽量少了解其他对象一个类直接调用了另一类的内部属性

你会发现,后面要讲的所有模式,都是对这几条原则的具体落地。比如单例模式就是控制实例数量保证全局唯一,策略模式就是开闭原则的最典型实现,代理模式则体现了对原有对象的松耦合增强。所以别再死记硬背UML图了,把原则记住了,模式就是顺水推舟的事。

2. 创建型模式:管好对象的“出生证”

2.1 单例模式:饿汉、懒汉和双检锁

单例模式是面试登场率最高的模式,没有之一。它的核心诉求很简单:一个类在整个JVM生命周期内只存在一个实例,并且提供一个全局访问点。典型场景包括配置管理类、线程池、数据库连接池、日志对象——这些对象如果被反复创建,资源开销大且状态难以统一管理。

Java里实现单例的方式太多了,我给你梳理几个最常见的写法,以及它们各自的坑。

饿汉式是最简单的一种,类加载时就初始化实例。好处是线程安全,没有并发问题;坏处是类加载时就创建对象,如果这个类一直没被用到,就白白浪费了内存。适合实例本身比较轻量、启动时创建无所谓的场景。

public class HungrySingleton { private static final HungrySingleton INSTANCE = new HungrySingleton(); private HungrySingleton() {} public static HungrySingleton getInstance() { return INSTANCE; } }

懒汉式就复杂得多了。最基础的版本就是判断为null才创建,但在多线程环境下会出大问题:两个线程同时判断null,然后各自new了一个实例,单例就被破坏了。于是就有了加锁版本,但直接用synchronized修饰方法的话,所有线程每次获取实例都要抢锁,性能确实有影响。

**双检锁(DCL)**是性能和安全兼顾的方案,也是我平时最推荐面试时手写的答案:

public class DoubleCheckSingleton { 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; } }

这里有个关键细节必须懂:instance必须用volatile修饰。因为new DoubleCheckSingleton()在JVM层面不是原子操作,它分为分配内存、初始化对象、将引用指向内存地址三步。如果没有volatile,CPU指令重排序后可能先执行第三步再执行第二步,另一个线程这时看到instance != null就直接返回了,拿到的却是一个还没初始化完成的对象。这个考点面试官特别喜欢追问。

2.2 工厂模式:从简单工厂到抽象工厂

工厂模式的核心思想是把创建对象的使用权和实现权分离。调用方只需要告诉工厂“我要什么”,不需要关心这个对象是怎么new出来的、内部依赖了哪些东西。

简单工厂严格来说不算Gof23里的模式,但它最容易被理解。比如你有一个FruitFactory,根据传入的字符串类型决定返回苹果、香蕉还是橘子。缺点是每新增一种水果,要改工厂里的if-else或switch代码,违反了开闭原则。

工厂方法模式把简单工厂的“分支判断”拆掉了,由工厂子类来决定生产什么产品。每个产品对应一个具体工厂类,新增产品只需新增工厂类,不需要改原有代码,这就满足了开闭原则。在Spring里,各种FactoryBean就是工厂方法模式的思想——容器里注册的是FactoryBean,等到要用的时候才通过它生成真正的目标Bean。

抽象工厂模式进一步处理“产品族”的场景。比如一个UI框架既要支持浅色主题又要支持深色主题,每个主题下面又包含按钮、输入框、对话框等一套产品。抽象工厂就是定义“创建一套主题所有产品”的接口,具体主题工厂负责生产该主题下的完整产品族。它能保证产品之间风格的兼容性,代价是类数量会明显膨胀。

面试的时候面试官喜欢问“简单工厂、工厂方法、抽象工厂有什么区别”,你只要抓住一个主线就行:简单工厂在方法内做分支,工厂方法把分支上移到了子类,抽象工厂管理的是产品族而非单个产品。

2.3 建造者模式:解决参数过多的构造函数问题

当一个对象有七八个甚至十几个参数的时候,构造函数的重载组合会爆炸,阅读和调用都极度痛苦。建造者模式就是解决这个问题的。

我之前遇到过一个真实的案例:一个短信通知对象有手机号、模板ID、签名、占位参数、定时发送时间、重试次数、优先级等十几个字段。如果全走构造函数,调用方根本记不住参数顺序,极易传错。用建造者模式之后,代码的可读性提升了一个档次:

SmsRequest request = SmsRequest.builder() .phone("13800138000") .templateId("TM0001") .sign("某某平台") .putParam("name", "张三") .retryCount(3) .build();

Java生态里建造者模式的典型代表就是Lombok的@Builder注解,这几乎是企业级项目的标配了。另外Spring的BeanDefinitionBuilder、MyBatis的SqlSessionFactoryBuilder也都是建造者模式在源码层面的落地。本质上,只要对象的构建过程是“多步骤、多可选参数、并且未来可能增加配置项”的,就适合用建造者模式。

3. 结构型模式:让类与对象组合出更强的结构

3.1 代理模式:Java动态代理是面试重灾区

代理模式是我认为Java开发必须吃透的模式,因为它跟Spring AOP直接绑定,而且面试几乎必问。

代理的核心思路是:不直接操作目标对象,而是通过一个代理对象间接访问,在代理里可以额外附加逻辑。比如给目标方法调用前后加日志、做权限校验、开事务、做缓存等——这些附加逻辑如果写进业务代码本身,会让业务代码变得很臃肿;通过代理就能把这些横切逻辑和核心业务逻辑解耦。

静态代理是手写一个代理类,跟目标类实现同一个接口,代理类里持有目标类的引用,在调用目标方法前后加逻辑。缺点是每个业务类都要手动写一个代理类,类数量急剧膨胀且没法复用。

动态代理就不一样了。JDK动态代理基于接口,只要传入目标对象、接口和InvocationHandler,就能在运行时动态生成一个代理对象,对所有接口方法统一增强。核心代码长这样:

public class LogProxyHandler implements InvocationHandler { private final Object target; public LogProxyHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("调用前记录日志:" + method.getName()); Object result = method.invoke(target, args); System.out.println("调用后记录日志:" + method.getName()); return result; } } UserService proxy = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new LogProxyHandler(new UserServiceImpl()) );

有一个高频坑要提醒:JDK动态代理只能代理接口,不能代理没有接口的普通类。如果目标类没有实现接口,就需要用CGLIB——它通过生成目标类的子类来实现代理。Spring的@Transactional、@Async这些注解之所以要求Bean不能是final、要能被继承,本质上就是因为Spring AOP默认会优先用JDK动态代理,类没接口时会切换CGLIB。你在实际项目中如果发现某个类加了事务注解却不生效,十有八九是代理没有成功生成。

3.2 适配器模式:让原本不兼容的接口协同工作

适配器模式在现实中最好的类比就是电源插头转换器。你从国外带回来的电器插头是两孔的,国内插座是三孔的,没办法直接用,那就需要一个转换器把两孔插到三孔插座上。

Java里的典型场景是对接第三方SDK、老系统接口。比如项目中原来统一用的UserService接口,新接入了另一个供应商的SDK,方法名叫queryUserInfo,返回类型也不一致。你不可能去改SDK,也不能让整个项目的调用方都改代码,正确的做法是写一个适配器类,实现项目的UserService接口,内部调用新SDK的方法做一层转换。对外,调用方感知不到任何变化;对内,新逻辑被安全地接入了。

Spring MVC是适配器模式的忠实拥趸。HandlerAdapter就是专门负责“把不同类型的处理器(@Controller方法、HttpRequestHandler等)统一适配成DispatcherServlet可调用的形态”的适配器。你不需要关心外部怎么到达的处理逻辑,适配器帮你统一了口径。

3.3 装饰器模式 vs 代理模式:长得像但目的不同

装饰器模式的经典案例在Java IO里,满满都是:new BufferedInputStream(new FileInputStream(new File("test.txt")))。每个包装类都在不改变原类功能的基础上,附加了缓冲、转换等功能。

装饰器和代理在代码结构上非常像,都是“持有被包装对象引用,并调用其方法”,但它们的意图完全不同:代理模式的核心目的是控制访问——我不让你直接访问目标对象,得先过我这关;装饰器模式的核心目的是增强功能——我原方法还是要的,但在它外面多套了几层能力。

刷题场景下经常有人混用这两个模式,你记住一句话就能破题:如果附加逻辑和原有业务无关(日志、权限、事务),那是代理;如果附加逻辑本身就是同类功能的一部分(比如InputStream加缓冲依旧是读文件),那是装饰器。

适配器、装饰器、代理这三个结构型模式容易被混淆,我建议你用表来区分:

模式目标是否改变接口是否增强功能
适配器让接口匹配是否
装饰器动态增加功能否是
代理控制访问或添加横切逻辑否视情况

4. 行为型模式:理顺对象之间的协作与职责

4.1 策略模式:干掉大颗粒的if-else

没有一种模式能像策略模式这样,在解决“一堆if-else”问题上给人如此直观的爽感。

策略模式的定义是“定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换替换替换”。核心组成三件套:策略接口、具体策略实现、上下文(持有策略引用并调用)。

举个例子,你的系统有会员折扣、节假日折扣、满减优惠三种计价逻辑。用if-else写就是三层嵌套,以后每加一种活动,就要改核心计价方法,代码越改越乱。用策略模式的话,先把折扣抽象成一个接口:

public interface DiscountStrategy { BigDecimal calculate(BigDecimal amount); } @Component public class MemberDiscountStrategy implements DiscountStrategy { @Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(new BigDecimal("0.9")); } } @Component public class HolidayDiscountStrategy implements DiscountStrategy { @Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(new BigDecimal("0.8")); } }

然后在上下文里注入所有的DiscountStrategy,通过一个类型标识来路由选择哪个策略执行。配合Spring的ApplicationContext甚至能做成“策略工厂”,这也是企业中很常见的设计。

需要注意:策略模式只消除了创建和使用的耦合,并没有完全消灭if-else——你仍然需要某种方式来确定“现在该用哪个策略”。这块通常用Map做策略名到策略Bean的映射来优化,让选择逻辑变成查表,彻底消灭判断分支。

4.2 观察者模式:事件驱动的基石

观察者模式解决的核心问题是“一对多的依赖关系”:一个对象状态变了,所有依赖它的对象都要收到通知。它让被观察者和观察者们解耦,彼此都不用知道对方的细节。

最经典的Java原生实现就是Observable和Observer,但因为Java 9开始这俩被废弃了,平时用得更多的反而是Spring里的ApplicationEvent和@EventListener。发布方只管发布事件,监听方只关心自己感兴趣的事件类型,两者通过事件类型解耦。这是典型的事件驱动架构,也是观察者模式在Java生态里的“官方标准落地”。

举一个现实例子:用户下单成功后,需要发短信、发邮件、扣库存、更新积分。如果这些逻辑全写在下单Service里,就是一大坨硬编码依赖,每加一个后续动作都要改核心方法。改成观察者模式后,下单Service只负责发布“订单创建成功”事件,短信监听器、邮件监听器、积分监听器各自处理各自的逻辑,互不干扰。

面试时观察者模式还会被问到“推模型和拉模型的区别”。推模型是事件对象把完整的数据推给所有观察者,缺点是观察者可能只关心其中一小部分数据,但全都收到了;拉模型是观察者收到通知后,自己再去拿想要的数据,优点是灵活,缺点是观察者需要额外关心数据来源。实际项目里推模型用得多一些,比如Spring的事件对象把订单ID传过去,各监听器再按需查库。

4.3 模板方法模式:固定流程,可变细节

模板方法模式很好理解,就是“父类定流程骨架,子类实现具体步骤”。核心价值在于把不变的流程固化,把变化的细节交给子类,避免子类各自实现导致流程不一致。

举个我们日常业务里经常遇到的场景:要对接多个渠道上传对账文件,流程都是固定的——校验文件、发送请求、解析响应、记录日志、更新对账状态。这些步骤的顺序和整体逻辑完全一样,唯一不同的是每个渠道的API地址、请求格式、解析规则。这时候如果每个渠道都单独写一套完整逻辑,就会复制出大量重复代码,而且将来改流程(比如加一步“加密”),得改好几个地方。

模板方法模式的做法是:父类定义processFile()方法,里面按顺序调用validateFile()、sendRequest()、parseResponse()、updateStatus()这些方法,其中validateFile()和parseResponse()是抽象方法,由子类实现。子类只需要关心“我这个渠道独特的部分”,公共流程父类全部包办。

Spring里的JdbcTemplate也是模板方法模式的体现,它把“获取连接、创建Statement、处理ResultSet、释放资源”这套固定流程封装好,你只需要传入要执行的SQL和结果集处理逻辑。

4.4 责任链模式:让每个处理器只关心自己该做的事

责任链模式处理的是“请求需要多个处理器逐个处理,但每个处理器的处理条件和方式各不相同”的场景。

最典型的Java落地是Filter——Servlet规范里的FilterChain就是一条责任链。每个Filter决定自己要不要处理这个请求,处理完后就调chain.doFilter()把请求传给下一个Filter。这种机制跟观察者模式的事件监听器不同,责任链强调的是“链式传递”,每个环节决定继续往上传还是中断。

我自己的项目里用责任链做过审批流:请假请求先经过部门主管审批,同意后转给HR,HR同意后再转给财务。每个审批节点就是一个Handler,Handler里维护一个next指针。代码结构可以很简洁,比如用抽象类和链表模式组合:

public abstract class Approver { protected Approver next; public void setNext(Approver next) { this.next = next; } public abstract void process(LeaveRequest request); } public class ManagerApprover extends Approver { @Override public void process(LeaveRequest request) { if (request.getDays() <= 3) { System.out.println("主管审批通过"); } else if (next != null) { next.process(request); } } }

责任链模式最大的好处是请求的发起者完全不用知道链条上有谁、每一步怎么处理。新增一个审批节点就是新增一个Handler并把它接到链尾,旧代码一行都不用改。代价就是请求在链上流转的排查难度会增加,所以健壮的链路日志是必须的。

5. 模式不是背出来的,是“练”出来的

5.1 一个真实项目里的模式组合拳

讲完单体模式,我想用一个真实场景把这些东西串起来。假设你要开发一个支付模块,支持支付宝、微信、银行卡三种支付方式,每种支付方式有各自的参数校验、下单、回调验签逻辑。你该怎么做?

第一层,用策略模式:定义一个PaymentStrategy接口,支付宝、微信、银行卡各自是一个实现类,通过PaymentType枚举映射到具体策略Bean。这样支付Service的核心逻辑就变成查表取策略,再调用策略的pay()方法,简洁干净。

第二层,用模板方法模式:定义AbstractPaymentStrategy实现PaymentStrategy接口,在pay()方法里固定流程——参数校验、金额计算、调用第三方、记录流水、返回结果,其中doPay()是抽象方法由子类实现。这个流程是任何支付渠道都通用的。

第三层,用工厂模式 + 观察者模式:策略的获取交给一个PaymentStrategyFactory,传入类型返回对应策略;支付成功后又发布一个PaymentSuccessEvent,让积分服务、通知服务、订单服务分别监听。

你看,一个支付模块就把策略、模板方法、工厂、观察者全用上了。所有模式协同工作,没有一层是硬凑的,每一种都有它要解决的明确问题。

5.2 怎么判断“该用哪种模式”

很多人看完所有模式后会陷入选择困难。我的建议是不要试图记忆“XX场景用XX模式”,而是从你要解决的问题倒推。

  • 如果你发现代码里有大段重复的算法、流程,只有部分细节不同,考虑模板方法模式或策略模式。
  • 如果代码里的if-else分支根据某些条件执行不同算法且条件经常变,考虑策略模式。
  • 如果某个对象状态一变,其他一堆对象都要跟着做事情,考虑观察者模式。
  • 如果有个流程是“多个人依次决定是否处理”,比如审批、过滤,考虑责任链模式。
  • 如果创建对象的过程很复杂,或者需要控制实例数量和生命周期,考虑单例模式或工厂模式。
  • 如果不想让代码直接操作具体类、还要在不改原代码的情况下加能力,考虑代理模式或装饰器模式。

5.3 面试复盘:高频设计模式题怎么答才出彩

面试题九成集中在“写一个线程安全的单例”“说一下Spring里用到了哪些设计模式”“你的项目里用过哪些设计模式、是怎么用的”。

“写单例”这种手写题没什么好多说的,重点是把双检锁和volatile的底层原理讲清楚,能顺带提一句“如果用枚举还可以防反射攻击和序列化破坏”,这就属于加分项。

“Spring里有哪些设计模式”是个人人都知道答案但很少有人讲出深度的题。你可以挑三四个有代表性的展开:AOP相关的动态代理模式、BeanFactory相关的是工厂模式、Web MVC里是适配器模式、RestTemplate里可以理解为模板方法模式、ApplicationEvent是观察者模式、@Conditional结合策略选择是策略模式。每个都别只报名字,最好能举出源码里的一个具体方法或类,例如AbstractAutowireCapableBeanFactory的createBeanInstance()方法里如何用策略选择InstantiationStrategy。

最怕的就是你回答“项目里用过策略模式”,面试官问“具体怎么用的”,你说“就定义了一个接口有多个实现”。这种回答等同于没说。面试官想听的是:你遇到了什么问题(if-else膨胀)、为什么选择策略模式(开闭原则、可维护性)、模式怎么落地的(Spring注入、Map路由、枚举映射)、有没有代价(类数量增加、选择逻辑仍需维护)。把这条叙事线讲清楚,这道题基本就拿下了。

6. 几个实战中必须避开的坑

设计模式用错了地方,比不用还难受。我在code review里见过太多“为了模式而模式”的代码,这里总结几个高频坑,给大家提个醒。

第一,过度抽象导致代码可读性暴跌。我再强调一遍,设计模式的价值是让代码更容易理解和维护,如果一个简单的需求被你整出了接口+抽象类+工厂+策略四层结构,连项目里最资深的同事都得花半天才能看懂,那就是本末倒置了。模式是手段,不是目的。什么时候引入模式,应该让代码的复杂度来说话,而不是让设计文档来说话。

第二,单例模式里的状态管理失控。很多人把非线程安全的成员变量放进单例类里,比如一个计数器的int字段,多个线程同时操作就会产生脏数据。记住:单例意味着全局唯一,全局唯一意味着所有访问共享同一份状态,共享状态就必须考虑并发安全。单例类的成员变量尽量设计成不可变的,有可变状态时要明确加锁或者用原子类。

第三,策略模式里的策略类无限膨胀。我之前见有一个项目,策略模式的类超过四十个,全是某些微小差异的变体。这时候把策略类的粒度放大一点,或者把一些相对固定的逻辑直接提升到模板方法里,反而更合理。模式不是越多越好,要控制在团队脑容量能覆盖的范围内。

第四,动态代理加事务时的自调用陷阱。Spring的事务是基于代理实现的,当你在同一个类里写两个方法,A方法内部直接调B方法,而B方法上有@Transactional注解,这时B的事务是失效的——因为this调用没有经过代理对象。这个坑特别隐蔽,解决方案一般是在另一个Bean里调用,或者通过ApplicationContext显式获取代理对象。这种就是典型“懂原理才能排掉的坑”。

第五,观察者模式的事件不要过度长链化。事件A触发事件B,事件B又触发事件C,最后一条链拉出去七八层,一旦出了问题,你从哪个环节排查都不知道。我通常会在事件类型里带上核心的traceId,从发起方开始全链路打印,出现问题一梭子查下来,直观得多。

7. 聊聊我的亲身体会

设计模式这个领域,真正拉开差距的不是背了23个还是25个模式,而是能不能把每个模式背后的封装点找出来。策略模式封装的是算法变化,观察者模式封装的是通知逻辑,模板方法封装的是流程骨架,代理模式封装的是横切关注点——每种模式背后都有一个“什么变、什么不变”的边界问题。你把这个边界找清楚了,用什么模式几乎是不用思考的事。

另外我强烈建议一个学习路径:先看java.io、java.util、JDK动态代理这些JDK自带的模式应用,再看Spring、MyBatis源码里的模式应用,最后回到自己的项目里,找出两三个已有的“臭味代码”用模式重构一遍。这个路径比看十遍《Head First》都管用,因为前者产生的是“肌肉记忆”,后者只是“短期记忆”。等你哪天写代码的时候不再想“这里该用哪个模式”,而是觉得“这段结构天然就该长这样”的时候,设计模式才真正是属于你的了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询