尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java设计模式学习导航:23种模式详解与面试实战

Java设计模式学习导航:23种模式详解与面试实战 23种设计模式学习导航Java完整版说实话“设计模式”这四个字几乎每一个Java开发者在简历上都写过面试前也都背过几轮。但等真正翻开《设计模式》那本GoF经典或者点开收藏夹里的各种“Java设计模式详解”时很多人第一个感觉还是懵单例模式倒是简单但抽象工厂和工厂方法到底差在哪策略模式和状态模式怎么长得一模一样装饰器模式和代理模式不都是包了一层吗这篇文章就是从这个痛点出发的一份完整导航。不堆概念不画花架子图我直接用Java代码和实际场景把23种设计模式拆开讲清楚再补上面试中被追问得最多的高频考点以及一套我实际验证过的学习方法。不管是正在准备校招实习、Java面试八股文还是被课程设计设计模式大作业困扰的同学这份导航都值得你边读边收藏边对照着写。1. 学设计模式之前先想清楚它在解决什么问题1.1 为什么背了八股文写代码时还是不会用先泼一盆冷水如果你把23种设计模式全部背下来却还是不会写代码这是很正常的事。因为设计模式的核心从来不是“某种固定写法”而是一种针对反复出现的业务场景的成熟解决方案。举个例子单例模式的写法你背得滚瓜烂熟——饿汉式、懒汉式、双重检查锁。可如果你不知道构造函数私有化的意义是“禁止外部随意new”不知道全局唯一实例在什么场景下才需要那这几个写法背了也白背。Java面试题里最常见的一种追问方式就是你说你用了单例那如果一个系统里所有类都写成单例会有什么后果显然不是所有情况都适合单例。学习设计模式有两种典型的学习误区只背概念和UML图从没有自己动手敲过代码。工作或作业里所有的类都硬套模式哪怕一个只有三行逻辑的小方法也非要整出个接口实现。这两种我都干过。最早做课程设计时我用了十几个类去套模式最后被自己绕晕后来真正在公司做项目却发现框架底层、业务代码里到处都是设计模式而且都是“没用名字但用了思想”的模式。所以今天的导航不讨论怎么背得快而是帮你建立“看到需求就能找到对应模式”的直觉。1.2 一张总览图三大类到底各管什么事23种GoF设计模式按目的分成创建型、结构型、行为型三大类每类回答的问题完全不一样创建型Creational解决“对象怎么创建”的问题核心思想是把创建逻辑和使用逻辑解耦。常见的五种单例Singleton、工厂方法Factory Method、抽象工厂Abstract Factory、建造者Builder、原型Prototype另外简单工厂虽然不是GoF钦定的23种但在面试和学习中通常和工厂三兄弟放在一起讲。结构型Structural解决“类和对象怎么组合”的问题目标是让组合出来的结构更灵活、复用性更好。一共七种代理Proxy、装饰器Decorator、适配器Adapter、外观Facade、桥接Bridge、组合Composite、享元Flyweight。行为型Behavioral解决“对象之间怎么协作、职责怎么分配”的问题是三类里数量最多的一共十一种策略Strategy、模板方法Template Method、观察者Observer、责任链Chain of Responsibility、状态State、命令Command、迭代器Iterator、中介者Mediator、备忘录Memento、解释器Interpreter、访问者Visitor。记忆方法很简单创建型管出生结构型管关系行为型管协作。前面这个地图心里有数了后面学起来就不会乱。2. 创建型模式把new这个动作管起来2.1 单例模式面试最高频、没有之一的写法单例的概念一句话就能说清一个类在整个JVM生命周期内只允许存在一个实例。但面试里远不止问概念这么简单。我用完整代码演示一下标准写法// 懒汉式线程安全 双重检查锁 public class Singleton { // volatile防止指令重排保证可见性 private static volatile Singleton instance; private Singleton() { // 构造函数私有化 } public static Singleton getInstance() { if (instance null) { // 第一次检查避免无谓加锁 synchronized (Singleton.class) { if (instance null) { // 第二次检查防止重复创建 instance new Singleton(); } } } return instance; } }很多人不理解为什么双重检查更不理解这里为什么必须加volatile。其实关键在于new Singleton()这行代码在JVM层面不是原子的会拆成分配内存、初始化对象、把引用指向内存这三个步骤。如果没有volatile线程A执行到一半线程B进来发现instance不为null直接拿去用可能就拿到了一个半初始化的半成品。这里每次面试都有倒人一定记住。还有一种在实际生产环境更推荐的写法枚举单例。它天然防反射攻击、防序列化破坏Effective Java里Joshua Bloch专门推荐过public enum SingletonEnum { INSTANCE; // 业务方法... }2.2 简单工厂与工厂方法别在业务代码里到处new如果你写过这样的代码那你已经在用简单工厂了public static PayService createPayService(String channel) { if (wechat.equals(channel)) { return new WechatPayService(); } else if (alipay.equals(channel)) { return new AlipayPayService(); } throw new IllegalArgumentException(unknown channel); }简单工厂的精髓是把“根据条件创建对象”的逻辑集中到一个地方业务调用方只传一个参数就行。缺点是每加一个新的支付类型就得改这个工厂类的if-else违反开闭原则。于是有了工厂方法模式把工厂抽象出一个接口每种产品对应一个具体工厂子类新加产品时不动老代码只新增一个工厂类。写业务代码时我总结的经验是产品类型变化不频繁、也不多时简单工厂足够产品会持续扩展、且不同产品之间有不同创建逻辑时上工厂方法。工厂方法模式里接口、抽象类、具体类这三层结构要理清楚很多Java面试题就是让你手写这个关系。2.3 抽象工厂产品族的配对问题抽象工厂最典型的例子是界面UI组件假设你开发一套跨平台工具需要考虑Windows风格和Mac风格每种风格下又有一组配套产品——按钮、输入框、菜单。如果只用工厂方法会面临“不小心把Windows风格的按钮和Mac风格的菜单搭在一起”的问题。抽象工厂的核心是产品族一个工厂负责创建一整套风格统一的产品而不是单个产品。拿一套代码骨架来说public interface UIFactory { Button createButton(); TextField createTextField(); } public class MacUIFactory implements UIFactory { public Button createButton() { return new MacButton(); } public TextField createTextField() { return new MacTextField(); } }这样做的好处是替换整套UI风格时只需要替换一个工厂对象。坏处也很明显产品族里新增一种产品比如滑动条所有具体工厂都要改。所以抽象工厂适合“产品族稳定、产品维度少”的场景。2.4 建造者模式和原型模式参数多、对象重该怎么办建造者模式在Java开发里出镜率极高你平时用的Lombok注解Builder、StringBuilder、Stream.Builder都是它的体现。本质是解决“构造函数参数太多、类型还容易写拧巴”的问题User user User.builder() .name(张三) .age(25) .email(zhangsanexample.com) .build();和JavaBean的setter方案相比建造者模式的优点是不可变性和链式调用体验更好缺点是类会多出一大段Builder代码。所以如果参数小于四个老老实实写构造函数或静态工厂就行不要滥用Builder。原型模式则针对“创建对象成本很高、想直接拷贝一份”的场景Java里就是实现Cloneable接口并重写clone()。这里有一个大坑默认的Object.clone()是浅拷贝如果你拷贝的对象里有引用类型的成员变量比如一个List拷贝出来的新对象和原对象会指向同一个List。改一个全变。所以实际使用时要么自己实现深拷贝逻辑要么直接用序列化或JSON的方式深拷贝。设计师模式里原型模式出场率不高但面试喜欢问浅拷贝和深拷贝的区别。3. 结构型模式把类和对象组合得更合理3.1 代理模式不改变原代码却能增强功能代理模式在Java里的地位不用多说Spring AOP底层就是动态代理。核心思想是为一个对象提供一个替身或占位符通过这个替身访问原对象在调用前后做一些原对象没有的能力日志、权限、事务、缓存。静态代理最简单代理类和目标类实现同一个接口但一个接口一个代理类代码膨胀很快。于是有了JDK动态代理用反射和目标接口生成代理对象public class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(before method.getName()); Object result method.invoke(target, args); System.out.println(after method.getName()); return result; } } // 使用 UserService proxyInstance (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new LogInvocationHandler(new UserServiceImpl()) );这里必背的一个细节JDK动态代理要求目标对象必须有接口因为生成的代理类已经继承了Proxy类Java单继承导致它只能通过实现接口来扩展。如果目标类没有接口就只能用CGLIB通过生成目标类的子类来实现代理。Spring里就是根据有没有接口自动选择JDK代理还是CGLIB的。3.2 装饰器模式给对象动态叠加功能装饰器模式是个容易和代理模式混淆的兄弟。一句话区分代理控制访问装饰器增强功能。代理模式的重心是“限制或管理你对原对象的访问”装饰器的重心是“给原对象一层层加功能”。Java IO流是最经典的课堂级例子BufferedReader reader new BufferedReader(new InputStreamReader(new FileInputStream(data.txt)));FileInputStream负责读文件InputStreamReader给字节流加了一个转字符流的功能BufferedReader再加一个缓冲的功能。每一层包上去都增强了能力。这就是典型的装饰器模式。所以面试被问“IO流用了什么设计模式”时一定要答出装饰器模式并且顺带解释BufferedReader到底装饰了谁。3.3 剩下的结构型模式适配器、外观、组合、享元、桥接这几个模式在面试中出现频率略低但源码里到处都是。适配器模式是“转换接口”的比如手机充电头把220V交流电转换成5V直流电。Java里InputStreamReader就是字节流到字符流的适配器Spring MVC里的HandlerAdapter也是把不同类型的处理器统一适配成HandlerExecutionChain来调用。外观模式是“提供简单入口”的比如你开了一家茶饮店顾客不需要知道后厨怎么做奶茶只需要在前台点单。代码里就是用一个Facade类封装一系列复杂子系统的调用。MyBatis的SqlSession就是外观门面。组合模式处理“树形结构”的比如文件系统、部门架构、菜单系统。叶子节点和树枝节点用同一个接口Component这个接口让外部调用时一视同仁。菜单递归渲染是组合模式最经典的应用。享元模式是“共享复用”的目的是减少重复对象的创建。Java里的Integer.valueOf()为什么能缓存-128到127的值String常量池的思路都和享元有关。面试问“两个Integer用比较为什么有时候相等有时候不相等”就是在考享元模式对应的缓存机制。桥接模式分离抽象和实现两个维度独立变化。最经典的例子是画笔颜色是一个维度红、蓝粗细是一个维度细笔、粗笔如果用继承去组合会很爆炸用桥接模式可以把这两个维度分开。JDBC的DriverManager就是典型的桥接——同样的Connection接口底层可以是MySQL驱动也可以是Oracle驱动。这些结构型模式你不需要每一个都背一遍完整代码但至少要做到例子和源码场景脱口而出。4. 行为型模式对象之间怎么协作4.1 策略模式与模板方法算法的灵活切换与固定骨架策略模式是最好上手、也是大家最容易理解的行为型模式。核心思想是定义一族算法把它们各自封装起来并且可以互相替换。最经典的例子是支付方式选择。我把代码骨架写出来public interface PayStrategy { void pay(BigDecimal amount); } public class WechatPayStrategy implements PayStrategy { public void pay(BigDecimal amount) { System.out.println(微信支付 amount); } } public class AlipayPayStrategy implements PayStrategy { public void pay(BigDecimal amount) { System.out.println(支付宝支付 amount); } }再写一个Context类持有策略对象调用方可以随时更换策略。这样比写一长串switch-case的好处是每一种支付逻辑独立成一个类互不干扰也好单独测试。而且新加支付方式时不用去改支付入口的老代码只要新加策略类即可。模板方法模式和策略模式正好是反着想的。策略是把整个算法都丢给外部。模板方法是父类把骨架流程写死但某几个具体步骤让子类去实现。最生活化的类比是做奶茶的流程永远是“煮茶、加料、封装”但加什么料珍珠、椰果、红豆由子类决定。我举一个实际开发最常见的模板方法场景public abstract class AbstractDataParser { public final void parse(String filePath) { // 骨架固定 checkFile(filePath); readData(filePath); processData(); writeResult(); } protected abstract void readData(String filePath); protected abstract void processData(); protected final void checkFile(String filePath) { // 通用逻辑... } protected final void writeResult() { // 通用逻辑... } }Spring中的JdbcTemplate执行SQL时确实把“获取连接、处理异常、关闭连接”这些固定步骤封装好了只让回调方法去决定怎么映射结果集这种做法本质上就带着模板方法加策略的影子。4.2 观察者模式与责任链模式事件驱动与请求流转观察者模式解决的是“一对多通知”的问题。主题状态变化以后所有观察者都能收到通知。Java里最朴素的实现就是定义一个观察者接口和主题类public interface Observer { void update(String event); } public class OrderSubject { private final ListObserver observers new ArrayList(); private String status; public void attach(Observer observer) { observers.add(observer); } public void changeStatus(String newStatus) { this.status newStatus; // 通知所有观察者 for (Observer observer : observers) { observer.update(newStatus); } } }如果你用过Spring的事件机制会发现ApplicationEvent和EventListener本质上就是观察者模式的应用消息队列MQ里的发布订阅模型也把它放大了无数倍。面试被问观察者模式时把“发布订阅”这四个字和订单状态变更这种场景串起来讲基本就稳了。责任链模式则是把多个处理器串成一条链请求从头到尾依次经过每一个处理器直到有处理器决定“我来处理”或者链走完。最典型的例子是Java Web里的Filter过滤器和Spring MVC的拦截器。写权限、日志、校验这类横切逻辑时我自己的习惯是优先想责任链。比如一个下单请求参数校验——用户鉴权——风控检查——创建订单。每一步做成一个Handler串起来public abstract class AbstractHandler { protected AbstractHandler next; public void setNext(AbstractHandler next) { this.next next; } public abstract void handle(OrderRequest request); }这种结构最大的好处是后续要新增一个“库存检查”环节不用去改前面任何一个Handler代码只要新写一个类挂到链上即可。这点和策略模式、模板方法模式的“开闭原则”导向是共通的。4.3 剩下的行为型模式状态、迭代器、命令、备忘录、中介者、解释器、访问者剩的这七个学习中有一个加分的点能说出它们在标准库和常见框架里出现在哪里。状态模式把对象在不同状态下的行为封装成独立类用状态对象取代大段的if-else判断。和策略模式长得极像。区别在于策略由调用方主动选择状态是由对象内部状态变化自动切换。举例子订单状态的待支付、已支付、已发货、已完成每个状态能执行的行为不同就是状态模式的典型场景。状态模式还会配合状态机一起玩。迭代器模式让外部可以顺序访问集合内部元素却不用暴露底层表示。Java的Iterator接口就是标准实现HashMap之所以能通过entrySet遍历本质上都是迭代器在干活。命令模式把请求封装成对象支持排队、日志、撤销操作。Java里Runnable就是一个命令对象多个线程执行同一个Runnable其实就是把“要做的事”封装好传给执行者。编辑器里CtrlZ撤销功能的实现思路通常是备忘录模式加命令模式。备忘录模式保存对象状态以便回滚恢复。游戏存档、文档快照都是这个概念。要理解好“打破封装”的权衡保存状态要么直接暴露内部结构要么用备忘录对象做特殊通道。中介者模式多个对象互相通信时全部改成和中介者通信从而减少对象之间的网状依赖。聊天室、GUI里的对话框控件、MQ的Broker都是中介者思想的体现。解释器模式定义一种语言并解释执行。正则表达式的解析器、SQL解析、数学表达式引擎都在这个范畴。工作中用得极少面试只需要知道概念和适用场景即可。访问者模式在不修改对象结构的前提下给对象增加新的操作。难点是理解“双分派”。比如报表统计系统里数据结构员工、部门相对稳定但操作薪资统计、考勤统计经常变化就可以用访问者模式把操作分离出去。实际业务中比较少见除非确实是算法频繁变、数据结构的变动极少的情况。5. 从“听得懂”到“写得出”一套可落地的学习方案5.1 三步走先找场景、再看源码、最后动手改常有人问我设计模式到底怎么学才不白学我给的建议是三步走。第一步把一个模式对应到一个生活或业务场景用一段手写的最简Java代码跑通。比如学策略模式写一个商场打折系统VIP、普通会员、新用户各自不同折扣学观察者写一个气象站实时推送天气变化。不要照抄教材代码要有自己的业务例子写完再打印出运行结果。第二步去常见的Java框架源码里找到它的身影。Spring、MyBatis、JDK自带的类库都是活教材。这里我列一个对照表读源码的时候可以对照着找设计模式源码中的典型体现工厂方法Spring的BeanFactory、MyBatis的SqlSessionFactory抽象工厂Spring的ProxyFactoryAOP相关建造者StringBuilder、Lombok Builder、MyBatis的SqlSessionFactoryBuilder代理Spring AOP动态代理、MyBatis Mapper接口的JDK代理适配器Spring AOP的MethodBeforeAdviceAdapter、HandlerAdapter装饰器Java IO流里的BufferedReader / FilterInputStream组合MyBatis的SqlNode动态SQL的树形结构享元Integer.valueOf()缓存、String常量池、线程池复用责任链Spring MVC拦截器链、MyBatis的Interceptor插件观察者Spring事件监听机制、Java Swing监听器模板方法JdbcTemplate、Spring的AbstractApplicationContext.refresh()第三步挑一个现有作业或项目去“重构”一段代码。这是从“懂”到“会用”的临门一脚也是最少人做的一步。比如你以前写过几个if-else判断不同类型试着用策略模式重写一遍看看代码的结构发生了什么变化。重写完以后再问自己一个问题新写的代码是真的变好了还是仅仅变复杂了如果变复杂了说明这个场景并不适合套这个模式。这一点在面试时也经常被追问你是怎么判断一个模式该不该用的这种基于真实重构的经历可以直接做面试答题的素材。5.2 高频面试题速查区别类问题是重灾区针对Java面试八股文和设计模式期末我把最高频的“对比型”题目整理成了一份速查表对比问题一句话总结简单工厂 vs 工厂方法简单工厂一个类管到底违反开闭原则工厂方法每个产品有独立工厂满足开闭原则工厂方法 vs 抽象工厂工厂方法生产单个产品抽象工厂生产一整套产品族懒汉式 vs 饿汉式单例懒汉式延迟加载但synchronized有性能问题饿汉式类加载时创建没有线程安全问题但可能浪费资源代理模式 vs 装饰器模式代理控制访问装饰器增强功能适配器模式 vs 装饰器模式适配器改变接口装饰器保持接口并增强行为策略模式 vs 状态模式策略由外部主动换算法状态由内部状态变化自动触发模板方法 vs 策略模板方法父类定骨架、子类补步骤策略整个算法可以替换观察者模式 vs 责任链模式观察者是广播一对多责任链是顺序传递直到有人处理静态代理 vs 动态代理静态代理编译期写好代理类动态代理运行时生成代理类JDK代理 vs CGLIBJDK代理基于接口CGLIB基于继承目标类没有接口也能代理表格中的每个问题都值得写一段五十行左右的代码去验证一遍别只看答案。面试时如果能主动举个例子、说一段自己踩过的坑比干巴巴背概念强太多。6. 新手避坑总结用错误换来的三条经验最后分享三个自己踩过的坑也是新手进门最容易犯的错误。第一个坑是**“贪全”**。刚开始学设计模式的时候总觉得23种必须一次性全部掌握于是每天抄一种模式的例子。结果学到后一半时前面已经忘干净了。后来学到一个合理路线把模式按“使用频率”分成三批。第一批是单例、工厂方法、抽象工厂、代理、策略、模板方法、观察者、装饰器、适配器这九个最常用必须达到熟练手写代码的程度第二批是建造者、组合、外观、责任链、状态、命令、迭代器理解了核心思想、能说清案例即可第三批是桥接、享元、备忘录、中介者、解释器、访问者、原型做到概念清楚、能识别出源码场景。分批学完以后整体收效特别快。第二个坑是**“重结构、轻语义”**。以前画UML图特别好看类、接口、关系清清楚楚但一写业务代码就露馅因为根本不知道这套结构到底要解决什么问题。后来被带我的师傅点醒学模式首先要抓住“它解决的是什么问题”其次才是结构。比如装饰器模式如果你只看到一层套一层却不知道这是为了不修改原有类就扩展功能那代码写了等于白写。第三个坑是**“过度设计”**。新学一个模式很容易上头恨不得把每个类都套上xxxFactory、xxxStrategy。结果类越拆越多别人看不懂自己也维护困难。后来我给自己定了个原则一段代码出现第三次重复时再考虑抽象出现第二个可变的维度时再考虑设计模式。设计模式是让代码更好维护的不是用来表演的。这点在项目代码评审里非常重要——比起炫技可读性永远是第一位的。14年的开发经验浓缩到这里其实就一句话设计模式是前人对“变化”的应对方案。你只需要站在前人的肩膀上把这些方案消化成自己的直觉然后在真正的代码里灵活运用。希望这份Java完整版导航能成为你吃透设计模式的第一块垫脚石。
返回列表