设计模式按功能可分为创建型、结构型、行为型三大类。
一、创建型模式(负责对象创建,解耦 “对象创建” 与 “使用”) 创建型模式核心是控制对象创建流程,避免硬编码依赖,常见包括单例、工厂方法、抽象工厂、建造者、原型。
单例模式(Singleton)
核心原理:保证一个类仅有一个实例,并提供全局唯一访问点。 关键实现要点:私有构造器(禁止外部 new)、静态实例(存储唯一对象)、静态方法(返回实例),需考虑线程安全(如双重检查锁、静态内部类)。 常见实现方式对比: 实现方式 线程安全 懒加载 优缺点 饿汉式 是 否 简单,类加载时创建实例,可能浪费内存 懒汉式(加锁) 是 是 线程安全,每次获取实例都加锁,性能损耗 双重检查锁 是 是 仅首次创建加锁,兼顾线程安全与性能 静态内部类 是 是 依赖 JVM 类加载机制,无锁,推荐使用
class Singleton {
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;
}
}
面试题 1:单例模式的双重检查锁(DCL)为什么要加volatile?答:避免 “指令重排” 导致的空指针。未加volatile时,instance = new Singleton()可能被拆分为 “分配内存→初始化对象→赋值引用” 三步,指令重排可能导致 “赋值引用” 先执行(此时对象未初始化),其他线程获取到未初始化的instance,调用方法时抛出空指针。volatile可禁止指令重排,确保对象初始化完成后再赋值。 面试题 2:单例模式的破坏方式有哪些?如何防止?答:破坏方式包括反射(调用私有构造器)、序列化 / 反序列化(生成新实例)。防止方案:1. 反射防护:构造器中判断实例是否已存在,存在则抛异常;2. 序列化防护:重写readResolve()方法,返回已有的单例实例而非新创建的实例。 应用场景:全局配置类、线程池、日志工厂等需唯一实例的场景。
工厂方法模式(Factory Method)
核心原理:定义一个创建对象的接口(工厂接口),让子类决定实例化哪个类,将对象创建延迟到子类。 对比简单工厂:简单工厂是 “一个工厂类创建所有对象”,违反开闭原则;工厂方法是 “一个产品对应一个工厂子类”,新增产品只需新增工厂,符合开闭原则。 面试题:工厂方法模式和简单工厂模式的区别是什么?答:1. 职责不同:简单工厂由一个类承担所有产品的创建,工厂方法将创建职责分散到多个工厂子类;2. 开闭原则:简单工厂新增产品需修改工厂类代码(违反开闭),工厂方法新增产品只需新增工厂子类(符合开闭);3. 复杂度:简单工厂实现简单,适合产品少的场景;工厂方法结构更灵活,适合产品多且易扩展的场景。 应用场景:日志框架(如SLF4J的LoggerFactory)、数据库连接(不同数据库对应不同连接工厂)。
建造者模式(Builder)
核心原理:将复杂对象的构建过程与表示分离,让同一构建过程可创建不同表示(如 “组装电脑”,相同步骤可组装出游戏本、办公本)。 关键角色:产品(复杂对象)、建造者(定义构建步骤)、指挥者(控制构建顺序,调用建造者步骤)。
面试题:建造者模式和工厂模式的区别是什么?答:1. 目标不同:工厂模式专注 “创建对象”,不关心对象的内部组成;建造者模式专注 “构建复杂对象的步骤”,需控制对象的各个部件组装顺序;2. 适用场景:工厂模式适合创建简单或中等复杂度对象(如User);建造者模式适合创建多部件、有组装顺序的复杂对象(如HttpResponse、StringBuilder)。 应用场景:StringBuilder(append方法构建字符串)、MyBatis的SqlSessionFactoryBuilder、 Lombok 的@Builder注解。
二、结构型模式(处理类 / 对象的组合,优化结构灵活性)结构型模式核心是通过组合类或对象,实现更灵活的功能扩展,常见包括代理、装饰器、适配器、外观、组合。
代理模式(Proxy)
核心原理:为目标对象提供一个代理对象,由代理对象控制对目标对象的访问(如 “中介” 代理房东处理租房流程)。
常见类型:
静态代理:编译期生成代理类,代理类与目标类实现同一接口,硬编码代理逻辑。
动态代理:运行期动态生成代理类,无需手动编写代理类(如 JDK 动态代理、CGLIB 动态代理)。
面试题 1:JDK 动态代理和 CGLIB 动态代理的区别是什么 |----------------|---------------------------|-----------------------------| | 对比维度 | JDK 动态代理 | CGLIB 动态代理 |
| 底层原理 | 基于接口生成代理类(实现目标接口) | 基于继承生成代理类(继承目标类) | | 目标类要求 | 必须实现接口 | 可无接口(不能是 final 类 / 方法) | | 性能 | 生成代理快,调用慢 | 生成代理慢,调用快(基于 ASM 字节码) | | 依赖 | JDK 自带,无需额外依赖 | 需引入 CGLIB 依赖 || 对比维度 | JDK 动态代理 | CGLIB 动态代理 |
面试题 2:代理模式的应用场景有哪些?答:1. 权限控制(如 Spring Security 的权限代理)2. 日志记录(代理目标方法,打印调用前后日志)3. 缓存(代理数据库查询,缓存查询结果);4. 远程调用(如 Dubbo 的服务代理,隐藏远程调用细节)。
装饰器模式(Decorator)
核心原理:动态给对象添加额外功能,不改变原对象结构 关键角色:抽象组件(定义核心功能接口)、具体组件(实现核心功能)、装饰器(持有组件引用,实现抽象组件,添加额外功能)。 面试题 1:装饰器模式和继承的区别是什么?答:1. 灵活性:继承是静态扩展(编译期确定功能),新增功能需新增子类,可能导致类爆炸;装饰器是动态扩展(运行期添加功能),可组合多个装饰器,灵活组合功能;2. 耦合度:继承耦合度高(子类依赖父类);装饰器耦合度低(装饰器与组件通过接口关联)。 面试题 2:JDK 中哪些类使用了装饰器模式?答:IO流(如BufferedReader装饰FileReader,添加缓冲功能;DataInputStream装饰InputStream,添加数据类型读取功能)、Collections的同步集合(如Collections.synchronizedList装饰List,添加线程安全功能)。
适配器模式(Adapter)
核心原理:将一个类的接口转换成客户端期望的另一个接口,解决 “接口不兼容” 问题 常见类型: 类适配器:通过继承目标类 + 实现源接口,实现适配(Java 单继承,灵活性低)。 对象适配器:通过持有源对象引用 + 实现目标接口,实现适配(推荐,符合合成复用原则)。 面试题:适配器模式和装饰器模式的区别是什么?答:1. 目的不同:适配器模式是 “解决接口不兼容”,让原本不能一起工作的类协同工作;装饰器模式是 “给对象动态添加功能”,不改变接口;2. 对原对象的依赖:适配器模式持有源对象,不扩展其功能;装饰器模式持有组件对象,扩展其功能;3. 客户端感知:适配器模式客户端面对的是新接口;装饰器模式客户端面对的是原接口。 应用场景:SpringMVC的HandlerAdapter(适配不同类型的处理器,统一调用handle方法)、Java的Arrays.asList()(将数组适配为List)。
三、行为型模式(处理对象间的交互,优化通信与职责分配) 行为型模式核心是定义对象间的交互规则,减少耦合,常见包括观察者、策略、模板方法、迭代器、责任链。
观察者模式(Observer)
核心原理:定义对象间的 “一对多” 依赖关系,当主题对象状态变化时,所有依赖它的观察者对象会自动收到通知并更新(如 “公众号订阅”,公众号(主题)更新文章,订阅者(观察者)收到推送)。 关键角色:主题(Subject,管理观察者,通知状态变化)、观察者(Observer,定义更新接口)、具体主题 / 具体观察者(实现对应接口)。
面试题 1:观察者模式的 “推模型” 和 “拉模型” 有什么区别?答:1. 推模型:主题主动将所有更新数据推送给观察者,观察者无需主动获取;优点是观察者简单,缺点是数据可能冗余(观察者不需要的信息也会推送)。2. 拉模型:主题仅通知观察者 “状态已变”,观察者主动从主题拉取需要的数据;优点是数据按需获取,缺点是观察者需知道主题结构,耦合略高。
面试题 2:JDK 的Observable类和Observer接口有什么缺点?答:1. Observable是类而非接口,限制了复用(子类只能继承Observable,无法继承其他类);2. setChanged()方法是protected,仅子类可调用,外部无法触发状态变化;3. 通知观察者时是同步调用,若某个观察者处理耗时,会阻塞其他观察者。 应用场景:Spring的ApplicationEvent(事件驱动模型,如ContextRefreshedEvent)、GUI开发(按钮点击事件的监听)。
策略模式(Strategy)
核心原理:定义一系列算法,将每个算法封装起来,使它们可互相替换,且算法的变化不影响使用算法的客户端(如 “支付方式”,支付宝、微信支付是不同策略,可切换且不改变订单支付逻辑)。 关键角色:策略接口(定义算法方法)、具体策略(实现算法)、上下文(持有策略引用,调用策略方法,不关心算法细节)。
面试题 1:策略模式和简单工厂模式的结合使用场景是什么?答:当客户端需要选择不同策略时,可通过简单工厂在上下文内部创建策略对象,客户端只需传入策略类型(如 “支付上下文” 接收 “支付宝”“微信” 类型,工厂创建对应策略),进一步降低客户端与策略的耦合(客户端无需知道具体策略类名)。
面试题 2:策略模式和状态模式的区别是什么?答:1. 目的不同:策略模式是 “封装不同算法,让算法可替换”,算法间无依赖;状态模式是 “封装对象的不同状态,状态间有切换逻辑”(如订单的 “待支付→已支付→已发货” 状态流转);2. 上下文角色:策略模式中上下文主动选择策略;状态模式中上下文的状态由状态对象控制,状态变化时自动切换策略。 应用场景:Spring的Resource(不同资源加载策略,如ClassPathResource、UrlResource)、排序算法(冒泡、快排等策略可切换)。
模板方法模式(Template Method)
核心原理:定义一个操作的算法骨架,将算法的某些步骤延迟到子类中实现(如 “做咖啡”,骨架是 “煮水→冲泡→倒杯→加调料”,“冲泡” 和 “加调料” 步骤延迟到子类(咖啡、茶)实现)。 关键要点:父类定义模板方法(骨架,用final修饰防止子类重写),子类重写 “钩子方法”(可选步骤,如 “加调料” 可重写或不重写)。
面试题 1:模板方法模式中的 “钩子方法” 有什么作用?答:钩子方法是父类中定义的默认实现(或空实现)的方法,子类可选择重写或不重写,用于 “控制算法步骤是否执行” 或 “补充算法细节”。例如:父类模板方法中,在 “加调料” 步骤前判断钩子方法isAddCondiment()的返回值,子类重写isAddCondiment()返回false,则跳过 “加调料” 步骤。
面试题 2:模板方法模式在框架中的应用有哪些?答:1. Spring的AbstractApplicationContext(refresh()方法是模板方法,定义容器初始化骨架,onRefresh()等步骤延迟到子类实现);2. JUnit的TestCase(setUp()、tearDown()是钩子方法,子类重写用于测试前后的初始化和清理)。 应用场景:框架骨架设计(如 ORM 框架的 CRUD 模板)、重复流程的标准化(如接口调用的 “参数校验→发送请求→结果解析” 骨架)。
四、高频综合面试题
什么是 “开闭原则”?哪些设计模式符合开闭原则?
答:开闭原则是 “对扩展开放,对修改关闭”,即新增功能时不修改原有代码,通过扩展实现。符合的模式包括:工厂方法(新增产品→新增工厂)、装饰器(新增功能→新增装饰器)、策略(新增算法→新增策略)、观察者(新增观察者→新增观察者类)。
面试题:在实际项目中,你如何选择设计模式?
答:1. 先明确问题场景:若需控制对象创建→选创建型模式(如单例、工厂);若需优化类组合→选结构型模式(如代理、装饰器);若需优化对象交互→选行为型模式(如观察者、策略);2. 避免过度设计:简单场景优先用简单方案(如产品少用简单工厂,不用抽象工厂);3. 结合框架特性:如Spring项目中,用动态代理而非静态代理,用@Builder而非手动写建造者。
面试题:单例模式在并发场景下的安全问题如何解决?
答:1. 饿汉式:类加载时创建实例,依赖 JVM 类加载的线程安全性(同一类仅加载一次),无并发问题;2. 双重检查锁:加volatile禁止指令重排,首次创建时加锁,后续无锁,兼顾安全与性能;3. 静态内部类:依赖 JVM 静态内部类的加载机制(仅在调用时加载,且线程安全),是推荐的线程安全实现方式。