
最近在组里面试Java工程师有一道题我几乎每场都会问你熟不熟悉设计模式说说你实际用过的有哪些。十个候选人里有七八个都能背出23种设计模式的分类——创建型、结构型、行为型讲起单例的线程安全、工厂方法的开闭原则也是头头是道。但一旦往下追问你们项目里哪个地方用了策略模式为什么要用不用行不行大多数人就卡住了。这不是个别现象。背概念和会使用是两码事网上一搜java设计模式跳出来的全是定义、UML图、优缺点列表却很少有人讲清楚一个最核心的问题设计模式到底解决的是什么。这篇文章我想换个角度结合Java代码和真实业务场景把面试常问、项目常用的那些模式拆开揉碎讲给你听。不管你是准备java面试题还是被设计模式大作业折磨这套思路应该都能直接用上。1. 设计模式的本源面试官究竟在考察什么1.1 设计模式是应对变化的方案手册GoF在《设计模式》里给的定义是在特定场景下为解决特定问题而总结出来的可复用方案。注意关键词是特定场景和特定问题。设计模式不是语法规范也不是代码风格它是一本前人踩坑之后浓缩的避坑手册。面向对象编程最核心的一句话是封装变化。设计模式本质上也只干这一件事把代码里容易变化的部分和相对稳定的部分拆开让稳定的部分不会因为变化的部分跟着一起改。你去看23种模式往根上追全都是这个目的。策略模式把一组算法封装起来互相替换工厂模式把创建对象的逻辑集中起来观察者模式把事件发布和事件处理解耦——都是在隔离变化点。理解了这一点你就不会犯为了用模式而用模式的毛病。看到一段代码先问自己这里会不会变如果不会变就别折腾如果会变再想用哪种模式把这些变数圈起来。1.2 六大设计原则比模式本身更重要很多教程把六大设计原则当成背诵点单一职责、开闭原则、里氏替换、接口隔离、依赖倒置、迪米特法则。但原则真正的价值不是背名字而是用来判断模式该不该用。我举个例子。我以前做一个报表导出功能刚开始只支持Excel所有代码都写在Service里。后来产品提需求要支持PDF和CSV我就在Service里加了一个又一个else if导出方法越写越长改一次要动一大片。这明显违反了开闭原则——对扩展不开放对修改倒是相当开放。后来用工厂加策略重构新增一种格式只需要写一个新类老代码一行不动。里氏替换原则说白了就是父类能用得动的地方换成子类也不能出问题否则你的继承体系就是错的。依赖倒置更直白上层模块别直接new一个底层实现让Spring帮你注入接口就行。这些原则其实比模式更重要模式只是原则的具体化表达而已。面试时你要是能把原则和模式串着讲比单纯背定义高出一个档次。1.3 23种模式的分类是一张地图23种模式分成三类创建型5种、结构型7种、行为型11种。这个分类不是摆设它帮你快速定位问题创建型回答对象怎么来结构型回答对象之间怎么组合行为型回答对象之间怎么协作、行为怎么分配给谁。面试被要求举例时也建议按这个逻辑作答。创建型举单例和工厂结构型举装饰器和代理行为型举策略和模板方法。按业务场景去蒙上一层分类图来展开会比按字母表把23个名字过一遍显得你对这套体系有真实理解。你自己学习阶段也可以拿着这张地图一个一个场景往里填而不是死记硬背。2. 创建型模式对象不是new出来就完事了2.1 单例模式五种写法里的线程安全博弈先问个问题单例模式解决什么问题很多人说全局只要一个实例。对但更准确的说法是有些资源很重、或者需要全局保持一致不能随便new。比如配置中心整个应用只允许一份配置比如数据库连接池和线程池你new一百个线程池出来系统直接完蛋。Spring里的Bean默认就是单例的框架已经帮你挡掉大部分场景但自己写工具库或者中间件时还是得自己动手。最古老的写法是饿汉式类加载时就new好简单、线程安全但如果实例创建很重比如要连接数据库而应用启动后根本用不到它就白白浪费了启动时间。懒汉式给它加个synchronized解决了延迟加载但每次getInstance都要抢锁高并发下性能不好。性能好点的是双重检查锁也就是DCL这是面试名场面public class ConfigCenter { private static volatile ConfigCenter instance; private ConfigCenter() {} public static ConfigCenter getInstance() { if (instance null) { // 第一次检查避免无谓加锁 synchronized (ConfigCenter.class) { // 只有需要创建时才加锁 if (instance null) { // 第二次检查防止重复创建 instance new ConfigCenter(); } } } return instance; } }这里有个细节我面试必问instance为什么要加volatile因为new ConfigCenter()不是原子操作它分三步——分配内存、调用构造方法初始化、把引用指向内存。如果不加volatileJVM可能发生指令重排先赋值引用再调用构造方法。另一个线程第一次检查时看到instance不为null直接return拿到的就是个没初始化完成的半成品对象。volatile禁用了这个重排序保证别人拿到的一定是完整实例。还有两种更优雅的。第一种是静态内部类方式public class ConfigCenter { private ConfigCenter() {} private static class Holder { private static final ConfigCenter INSTANCE new ConfigCenter(); } public static ConfigCenter getInstance() { return Holder.INSTANCE; } }利用类加载机制Holder只有在真正调用getInstance时才被加载既懒加载又线程安全。第二种是Effective Java推荐的枚举单例天然防反射、防序列化破坏但项目里用的人少大家还是不习惯。我的经验是面试题里DCL要会写实际项目优先用静态内部类枚举单例可以在聊天时当亮点提一嘴。2.2 工厂模式把new的权利从业务代码收走工厂模式解决的核心痛点是new对象的逻辑太分散。拿支付渠道举例对接支付宝、微信、银联每个渠道的初始化参数完全不同。业务代码里写一长段if-else去new具体实现以后每加一个渠道就得改一段老代码又违反开闭原则。最简单的写法是简单工厂一个Factory类持有Map或者switch根据type返回不同的支付实现。这能解决一部分问题但工厂自身还是会越改越长。再往上走就是工厂方法模式定义创建接口让子类决定实例化哪个类——每次加渠道加一个工厂类老代码不动。如果这些渠道需要成组出现既要创建支付对象又要创建对账单解析对象那就是抽象工厂模式。实际项目中我见得最多的是工厂Spring容器结合。把策略实现类注册成Bean注入一个List按类型自动归类Service public class PayChannelFactory { private final MapString, PayChannel channelMap; public PayChannelFactory(ListPayChannel channels) { channelMap channels.stream() .collect(Collectors.toMap( c - c.getClass().getAnnotation(PayType.class).value(), Function.identity())); } public PayChannel getChannel(String type) { return channelMap.get(type); } }以后新增渠道加一个实现类加一个注解什么都不用改。这也是为什么Spring项目里策略和工厂总会一起出现——接口负责定义能力Map负责运行时查找开闭原则自然就满足了。2.3 建造者和原型参数太长与复制太贵时的解法建造者模式什么时候用构造器参数超过四五个、还有很多可选参数的时候。我见过一个老系统创建订单的构造函数有12个参数调用的人根本分不清第8个参数传的到底是什么稍不留神就传错位置。用Builder之后代码变成这样Order order Order.builder() .orderNo(NO20250101) .userId(10086L) .amount(new BigDecimal(99.00)) .remark(春节活动订单) .build();Lombok的Builder注解就是干这个的。有人会问这和setter有啥区别setter的问题是对象存在中间状态——new完还没set完另一个线程就可能读到半个对象Builder则在build()那一刻产出完整对象还能顺手加参数校验保证不可变对象的构造质量。原型模式则是用现有对象复制新对象Java的Cloneable接口支持浅拷贝深拷贝得自己处理引用类型。业务代码里用得不频繁Spring的bean scopeprototype有点原型的味道。我不建议新手一上来就在项目里硬套原型浅拷贝的坑改一个对象另一个跟着变往往比收益更大等真遇到创建对象太贵、又需要大量相似实例的场景再回头用它不迟。3. 结构型模式对象之间怎么搭架子3.1 适配器、装饰器、代理长得像但心思完全不同这三个模式结构上都像是包一层Java面试题里经常被放一起问。区分它们有个口诀适配器解决接口不兼容装饰器解决功能增强代理解决访问控制。表格化之后一目了然模式核心目的典型场景关键特征适配器把A接口转成B接口对接第三方SDK、老接口改造接口变了能力不变装饰器给对象增加新功能IO流加缓冲、加压缩接口不变层层包装增强代理控制访问、加横切逻辑日志、鉴权、AOP原对象被包住客户端无感知适配器的例子最常见于老系统改造。项目里有一个老的Logger接口只有log(String msg)新标准是Logback风格需要logger.info(...)。不必重写老代码写一个Adapter转一下就行public class LoggerAdapter implements NewLogger { private final OldLogger oldLogger; public LoggerAdapter(OldLogger oldLogger) { this.oldLogger oldLogger; } Override public void info(String msg) { oldLogger.log([INFO] msg); } }装饰器模式你其实天天在用。InputStream是一个抽象组件BufferedInputStream包一个FileInputStream加缓冲能力这就是装饰器。你自己也可以写一个带压缩、带日志的装饰器把增强功能拆成一个个小类自由组合避免一个类背上所有能力。代理模式最经典的应用是AOP。静态代理很简单手工给UserService写一个代理类在调用前后加日志或权限判断。动态代理更进一步Spring里如果有接口就用JDK动态代理没有接口就用CGLIB生成子类。那为什么JDK动态代理必须有接口因为代理对象是通过Proxy.newProxyInstance创建的代理类要implements目标接口才能冒充原对象CGLIB不走接口它靠字节码生成目标类的子类所以对没有接口的类也有效。这个区别是面试题里的高频考点值得记牢。3.2 组合模式用一棵树统一单个和整体权限菜单、组织架构、文件目录都是天然的树形结构。如果用if-else去区分叶子节点和树枝节点树一深代码就完全没法看。组合模式让叶子节点和树枝节点实现同一个抽象接口树枝节点内部持有ListComponent客户端统一调用不用关心到底拿到的是叶子还是容器。写个最简单的菜单例子。MenuItem定义一个render()方法叶子菜单直接打印名称有子菜单的容器先打印自己再递归遍历子菜单。树的遍历逻辑被封装在容器内部新加一层菜单根本不用改客户端代码。Java的java.awt.Component和Container就是教科书级实现HashMap的TreeNode也是类似思想的产物。理解组合模式的关键是敢用抽象去统一单个和整体。3.3 门面模式让复杂子系统只露一张脸门面模式Facade也叫外观模式。意图是当一个子系统内部的协作关系很复杂时对外只暴露一个统一入口。我做老系统改造时体会特别深老代码里有十几个Service来回调用新同事接手根本不知道下单需要调哪几个方法、顺序是什么、异常怎么处理。后来我写了一个OrderFacade把参数校验、扣库存、算价、生成订单、发消息串起来外部只调一个createOrder()。好处有两个一是解耦外部模块完全不需要知道内部胡子拉碴的依赖二是隔离变化如果某个环节要替换比如把库存接口换成新的只需要改门面内部外部毫无感知。门面模式不算高深但它在系统分层、老系统改造、第三包封装里出镜率极高是最实用的整容手段之一。4. 行为型模式把行为的选择权交给对象4.1 策略模式干掉if-else最顺手的武器策略模式是我在Java项目里用得最多的行为型模式。简单说把一组可以互相替换的算法封装成策略接口运行时选择使用哪个。典型业务是营销折扣满减、打折、优惠券。不用策略时if (discountType FULL_REDUCTION) { amount amount.subtract(...); } else if (discountType DISCOUNT) { amount amount.multiply(...); } else if (discountType COUPON) { // 再来一段优惠券逻辑 }每加一种活动都要去改老if-else两个活动叠加时排列组合直接爆炸。用策略模式重构后public interface DiscountStrategy { BigDecimal apply(BigDecimal amount); String type(); } Service public class FullReductionStrategy implements DiscountStrategy { Override public BigDecimal apply(BigDecimal amount) { return amount.compareTo(BigDecimal.valueOf(100)) 0 ? amount.subtract(BigDecimal.valueOf(20)) : amount; } }再配合前面说的工厂加Spring注入Map前端传一个type后端直接strategyMap.get(type).apply(amount)。新增活动等于新增一个类老代码一行不动开闭原则完美落地。顺便提一嘴策略模式和状态模式不要混策略是调用方主动选择同级算法状态是对象内部状态驱动行为自动流转。很多人博客里把这两个扯在一起说面试很容易露馅。4.2 模板方法模式骨架写死钩子放开模板方法的思想定义一个流程骨架把其中会变化的步骤延迟到子类实现。Java里最常见的场景是业务编排。比如订单处理流程public abstract class AbstractOrderProcessor { public final void process(Order order) { checkParam(order); handleStatus(order); // 子类自己实现 save(order); sendNotify(order); // 默认实现子类可选重写 } protected abstract void handleStatus(Order order); protected void sendNotify(Order order) { // 默认发送短信的逻辑 } }稳定的步骤写死在父类变化的步骤留成抽象方法可选的步骤做成钩子方法。Java并发包里的AQSAbstractQueuedSynchronizer是模板方法最典型的例子把锁的获取和释放骨架定好tryAcquire、tryRelease留给子类实现ReentrantLock就是它的子类。JdbcTemplate同理连接管理、异常转换这些脏活它全包了把SQL执行留给你。你只要理解骨架加钩子这两个词读这类源码会轻松一半。4.3 观察者模式下单成功后的广播机制观察者模式解决的是一个事件发生后多个模块要跟着响应的问题。比如订单支付成功要发短信、减库存、送积分、通知财务。如果在下单代码里一行行调用耦合度直接拉满每加一个响应方就要改一行业务代码。观察者把事件发布方和订阅方彻底解耦Spring里用起来相当顺Component public class OrderEventListener { EventListener public void onOrderPaid(OrderPaidEvent event) { // 发短信、通知仓库、记账... } }发布方只需要applicationEventPublisher.publishEvent(new OrderPaidEvent(orderId));发布方完全不知道谁会响应反正事件发出去就行。这里有一个常见坑Spring默认事件监听是同步执行的如果监听器里做重活比如同时调短信服务、推送服务会拖慢主流程。要异步可以用Async配合线程池但要注意事务边界和消息丢失问题真要求可靠投递还是得交给消息队列而不是依赖JVM内事件。4.4 状态模式和责任链两个高频考点状态模式适合状态流转清晰的系统比如订单待支付、已支付、已发货、已完成、已取消。朴素写法是流程方法里写if (status 已支付 action 发货)状态一多全是分支谁也改不动。状态模式把每个状态做成一个类状态类里定义在当前状态下能做什么动作流转逻辑交还给状态对象。审批流、设备状态机这类业务特别适合面试考它本质是考察你有没有状态机抽象意识。责任链模式也值得了解。Servlet的Filter、Spring Security的过滤器链、Netty的Handler链全是责任链的应用。好处是请求依次经过多个Handler每个Handler只处理自己关心的部分链条顺序还能动态调整。如果你的系统里有告警分级、多级限流、风控拦截责任链比一长串硬编码if-else好维护得多。4.5 剩下的行为型模式怎么快速记牢行为型有11个不是每个都要深入实践。常见的可以这么记模式一句话定位实际使用频率命令模式把请求封装成对象支持撤销、重做、队列低迭代器模式让集合遍历与底层结构解耦极高天天在用中介者模式多对象交互集中到一个中介类低备忘录模式保存对象快照支持回滚低访问者模式把对对象结构的操作分离出来很低解释器模式定义一门语言的解释规则极低老实说访问者和解释器在普通业务代码里我几乎没手写过面试能把概念讲清楚就够。但迭代器不一样你天天在用却可能没意识到——Java集合框架的Iterator就是标准迭代器模式for-each语法底层就是在调它。学习设计模式没必要追求23个全用一遍把高频的那六七个用熟比泛泛地了解23个强得多。5. 真实Java项目里的落地点JDK、Spring与别乱用5.1 你其实每天都在用JDK源码里的模式图谱很多人觉得设计模式只在框架源码里觉得离自己很远。其实JDK里就藏着一大堆Runtime.getRuntime()是单例模式。Integer.valueOf()对-128到127做缓存这是享元模式。Collections.unmodifiableList()返回一个只读包装装饰器模式。InputStreamReader把字节流转成字符流适配器模式。Iterator接口是迭代器模式。AQS大量使用模板方法模式。线程池的ThreadFactory是工厂模式。面试如果被问JDK哪里用到了设计模式能一口气说出这六个印象分会好很多。这些例子还说明一件事设计模式不是高不可攀的东西就是Java标准库作者们在真实工程里的日常选择。5.2 Spring框架就是一场大型设计模式展览Spring框架本身就是个巨大设计模式案例库。BeanFactory是工厂模式容器默认管理单例BeanAOP底层是动态代理有接口用JDK代理没有接口就用CGLIBJdbcTemplate是模板方法模式ApplicationEvent是观察者模式HandlerAdapter是适配器模式MyBatis的MapperProxy本质是工厂加代理的组合。更妙的是这些不是牵强附会Spring最初设计时就大量参考了这些模式。你每天用Spring却不懂设计模式就像天天开车但对发动机工作原理一无所知——不影响开但出了诡异问题你只能干瞪眼。反过来你在Spring里看到这么多模式实例也说明设计模式不是学院派玩具而是工业级框架验证过的工程套路。5.3 过度设计是比不懂模式更常见的问题前面一直在讲怎么用但这篇里我特别想讲一个反向经验什么时候别用。我见过一个大一学生的期末项目做一个图书管理系统给每个实体都配了接口、抽象工厂、抽象类十几个文件最后自己都维护不动改一个字段要翻五个文件。这就是典型的过度设计。判断一个模式该不该用标准很简单看它管理的业务有没有真正的变化。如果三五年不变、只有一种实现、调用方就你一个人直接用类就完了别硬抽象。设计模式引入的是复杂度换回来的是灵活度这笔账必须划算。我现在的习惯是先写简单版本等真的出现第二个分支、第二个实现、第二处调用时再重构引入模式。记住模式是重构出来的不是一开始就套上去的。能说清楚我什么时候不用设计模式比背熟23种定义更能证明你真懂。5.4 面试答题框架与期末大作业的正确姿势最后聊点应试干货。被问你常用哪些设计模式时建议按场景→痛点→方案→效果四步走。比如我之前做支付渠道对接每新增一个渠道都要改Service里的if-else场景痛点所以用工厂加策略把渠道实现做成Bean按类型映射成Map新增渠道只需加一个实现类方案之后渠道上线基本不动老代码效果。不要只说我用了工厂模式要说你是怎么判断它适合这个场景的。面试官想听的是你的决策过程不是名词本身。至于设计模式大作业想拿高分不必把23种全塞进去选四五个真正契合业务场景的就好。比如点餐系统单例管理全局菜单配置工厂加策略处理不同支付方式模板方法定义订单处理流程观察者处理下单后的通知业务。写完再在报告里讲清楚不使用会怎样、用了解决了什么比罗列23个模式名高级得多。设计模式期末考的核心从来不是数量是你有没有建立用模式解决问题的直觉。写到最后我得交代一下自己踩过的坑。刚工作头两年我特别喜欢到处套模式给一个永远只有一个渠道的支付类写了抽象工厂加模板方法三个月后自己接手都想骂街——想改个字段得翻五个文件。后来带团队面试反而越来越喜欢问你什么时候不用设计模式。能把这个判断说明白的人设计模式才算真正入了门。这套先看变化、再选模式、最后落地的思路希望对正在准备java面试或者被期末作业折磨的朋友有点帮助。