设计模式是Java进阶的核心知识点,更是面试高频考点(初/中级开发必问)、工作中解耦提效的“利器”。很多人学设计模式只记理论,遇到实际场景就懵,导致“学了不用、用不对”。
今天整理了Java最常用的8种设计模式,跳过冗余理论,重点讲「核心思想+实战场景+极简代码示例」,每个模式都贴合真实开发场景(如Spring、MyBatis源码中常见),新手能看懂、老手能复盘,收藏起来,面试和工作直接套用!
一、设计模式核心认知(先搞懂,再学具体模式)
很多人觉得设计模式复杂,其实核心就一个:“封装变化、解耦依赖”,用固定的套路解决重复出现的代码问题,让代码更灵活、易维护、可复用。
补充2个面试必答知识点(避免被问懵):
•设计模式三大分类:创建型(负责对象创建)、结构型(负责对象/类的组合)、行为型(负责对象间的交互);
•设计模式六大原则:单一职责、开闭原则(核心)、里氏替换、依赖倒置、接口隔离、迪米特法则(最少知道),后续每个模式都会贴合对应原则。
不用死记六大原则,结合下面的模式实例,自然就能理解其应用场景~
二、Java常用设计模式(8种,实战+面试双覆盖)
(一)单例模式(创建型|高频面试+实战必备)
核心思想:一个类在JVM中只有一个实例,提供全局唯一的访问入口,避免重复创建对象造成资源浪费(如工具类、连接池)。
实战场景:Spring中的Bean默认是单例模式、数据库连接池(DataSource)、日志工具类(Logger)、配置文件工具类。
关键注意:线程安全、防止反射破坏、防止序列化破坏(面试常问“如何保证单例安全”)。
极简实战代码(推荐2种,工作首选):
1. 静态内部类式(推荐,线程安全+延迟加载,无锁高效)
// 静态内部类式单例(工作首选)
public class Singleton {
// 私有构造方法,防止外部new
private Singleton() {
}
// 静态内部类,懒加载(类加载时不初始化)
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 全局访问入口,线程安全(JVM保证类加载原子性)
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2. 双重检查锁(DCL,适合多线程场景,延迟加载)
// 双重检查锁(DCL)单例
public class SingletonDCL {
// volatile修饰,防止指令重排(面试必问volatile的作用)
private static volatile SingletonDCL instance;
private SingletonDCL() {
}
public static SingletonDCL getInstance() {
// 第一次检查,避免频繁加锁
if (instance == null) {
synchronized (SingletonDCL.class) {
// 第二次检查,防止多线程并发创建
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
}
面试追问应答:为什么用volatile?—— 防止instance = new Singleton()指令重排(分配内存→初始化对象→赋值引用),避免多线程获取到“未初始化的对象”。
(二)工厂模式(创建型|高频面试+实战必备)
工厂模式分为3种:简单工厂(入门)、工厂方法(常用)、抽象工厂(复杂场景),重点掌握「工厂方法」,贴合实战。
核心思想:将对象的创建逻辑封装起来,不直接在代码中new对象,而是通过工厂类创建,降低耦合(符合“依赖倒置原则”)。
实战场景:Spring中的BeanFactory(创建Bean)、MyBatis中的SqlSessionFactory(创建SqlSession)、不同类型的支付方式创建(微信支付、支付宝支付)。
1. 简单工厂(入门,适合产品种类少的场景)
// 1. 产品接口(统一规范)
public interface Pay {
void pay(); // 支付方法
}
// 2. 具体产品(微信支付)
public class WeChatPay implements Pay {
@Override
public void pay() {
System.out.println("微信支付,扣款成功");
}
}
// 3. 具体产品(支付宝支付)
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("支付宝支付,扣款成功");
}
}
// 4. 简单工厂类(创建对象的逻辑封装在这里)
public class PayFactory {
// 根据类型创建对应支付对象
public static Pay createPay(String type) {
if("wechat".equals(type)) {
return new WeChatPay();
} else if("ali".equals(type)) {
return new AliPay();
} else {
throw new IllegalArgumentException("不支持该支付方式");
}
}
}
// 5. 实战调用(不用直接new,通过工厂获取)
public class Test {
public static void main(String[] args) {
Pay weChatPay =PayFactory.createPay("wechat");
weChatPay.pay(); // 输出:微信支付,扣款成功
}
}
2. 工厂方法(常用,解决简单工厂“新增产品需修改工厂”的问题)
// 1. 产品接口(不变)
public interface Pay {
void pay();
}
// 2. 具体产品(不变,新增产品只需新增此类)
public class WeChatPay implements Pay {
@Override
public void pay() {
System.out.println("微信支付,扣款成功");
}
}
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("支付宝支付,扣款成功");
}
}
// 3. 工厂接口(统一工厂规范)
public interface PayFactory {
Pay createPay(); // 工厂方法,由具体工厂实现
}
// 4. 具体工厂(一个产品对应一个工厂,新增产品只需新增工厂)
public class WeChatPayFactory implements PayFactory {
@Override
public Pay createPay() {
return new WeChatPay();
}
}
public class AliPayFactory implements PayFactory {
@Override
public Pay createPay() {
return new AliPay();
}
}
// 5. 实战调用(符合开闭原则,新增产品不修改原有代码)
public class Test {
public static void main(String[] args) {
// 微信支付
PayFactory weChatFactory = new WeChatPayFactory();
Pay weChatPay =weChatFactory.createPay();
weChatPay.pay();
// 支付宝支付
PayFactory aliFactory = new AliPayFactory();
Pay aliPay = aliFactory.createPay();
aliPay.pay();
}
}
面试重点:简单工厂vs工厂方法的区别?—— 简单工厂违背开闭原则(新增产品需修改工厂),工厂方法遵循开闭原则(新增产品只需新增产品类和工厂类)。
(三)代理模式(结构型|高频面试+实战必备)
代理模式分为3种:静态代理(入门)、动态代理(JDK动态代理+CGLIB动态代理,实战核心),重点掌握动态代理(Spring AOP的核心原理)。
核心思想:为目标对象创建一个代理对象,通过代理对象访问目标对象,在不修改目标对象代码的前提下,增强目标对象的功能(如日志、权限校验、事务控制)。
实战场景:Spring AOP(日志、事务、权限)、MyBatis的Mapper代理、接口的权限控制。
1. JDK动态代理(常用,基于接口,无需依赖第三方jar)
// 1. 目标接口(代理的前提,JDK动态代理必须基于接口)
public interface UserService {
void addUser(String username); // 目标方法
}
// 2. 目标对象(具体实现,无需修改代码)
public class UserServiceImpl implements UserService {
@Override
public void addUser(String username){
System.out.println("新增用户:" + username);
}
}
// 3. 动态代理处理器(实现InvocationHandler,增强逻辑在这里)
public class MyInvocationHandler implements InvocationHandler {
// 目标对象(被代理的对象)
private Object target;
// 构造方法,传入目标对象
public MyInvocationHandler(Object target) {
this.target = target;
}
// 代理方法(核心,所有代理对象的方法调用都会走到这里)
@Override
public Object invoke(Object proxy,Method method, Object[] args) throws Throwable {
// 增强逻辑1:方法执行前(如日志、权限校验)
System.out.println("日志:开始调用addUser方法,参数:" + args[0]);
// 调用目标对象的方法(核心,执行真实业务逻辑)
Object result = method.invoke(target, args);
// 增强逻辑2:方法执行后(如事务提交、日志记录)
System.out.println("日志:addUser方法调用完成");
return result;
}
}
// 4. 实战调用(获取动态代理对象,执行增强后的方法)
public class Test {
public static void main(String[] args) {
// 1. 创建目标对象
UserService userService = new UserServiceImpl();
// 2. 创建代理处理器,传入目标对象
MyInvocationHandler handler = new MyInvocationHandler(userService);
// 3. 获取动态代理对象(JDK提供Proxy类创建)
UserService proxy = (UserService)
Proxy.newProxyInstance(
userService.getClass().getClassLoader(), // 类加载器
userService.getClass().getInterfaces(), // 目标对象实现的接口
handler // 代理处理器
);
// 4. 调用代理对象的方法(实际执行的是invoke方法,包含增强逻辑)
proxy.addUser("张三");
}
}
2. 面试重点(必背)
•JDK动态代理 vs CGLIB动态代理:
•JDK动态代理:基于接口,无需第三方依赖,效率高(JDK原生支持);
•CGLIB动态代理:基于继承(无需实现接口),依赖cglib.jar,可代理无接口的类,效率略低;
•Spring AOP默认用哪种?—— 目标对象有接口用JDK动态代理,无接口用CGLIB。
(四)装饰器模式(结构型|实战必备)
核心思想:动态地给目标对象添加额外功能,不改变目标对象的结构,也不影响其他同类型对象(类似“给手机贴钢化膜、戴手机壳,不改变手机本身,增加保护功能”)。
实战场景:Java IO流(核心应用!如BufferedReader装饰FileReader,增加缓冲功能)、Spring中的BeanWrapper(装饰Bean,增加属性设置功能)。
极简实战代码(模拟IO流缓冲功能):
// 1. 目标接口(统一规范,如IO流中的Reader)
public interface Reader {
void read(); // 核心方法:读取数据
}
// 2. 目标对象(具体实现,如FileReader)
public class FileReader implements Reader {
@Override
public void read() {
System.out.println("读取文件中的原始数据(无缓冲,效率低)");
}
}
// 3. 装饰器抽象类(实现Reader接口,持有目标对象,统一装饰规范)
public abstract class ReaderDecorator implements Reader {
// 持有目标对象(被装饰的对象)
protected Reader reader;
// 构造方法,传入目标对象
public ReaderDecorator(Reader reader){
this.reader = reader;
}
// 重写read方法,子类实现具体的装饰逻辑
@Override
public abstract void read();
}
// 4. 具体装饰器(缓冲装饰,增加缓冲功能,如BufferedReader)
public class BufferedReader extends ReaderDecorator {
public BufferedReader(Reader reader){
super(reader);
}
// 装饰逻辑:在目标方法基础上,增加缓冲功能
@Override
public void read() {
System.out.println("增强:添加缓冲功能,提高读取效率");
reader.read(); // 调用目标对象的原始方法
}
}
// 5. 实战调用(动态添加功能,可嵌套装饰)
public class Test {
public static void main(String[] args) {
// 原始读取(无缓冲)
Reader fileReader = new FileReader();
fileReader.read();
System.out.println("----------------");
// 装饰后读取(增加缓冲功能)
Reader bufferedReader = new BufferedReader(fileReader);
bufferedReader.read();
}
}
关键区分:装饰器模式vs代理模式?—— 装饰器侧重“动态增加功能”,代理模式侧重“控制对象访问”(如权限校验、日志记录)。
(五)观察者模式(行为型|高频面试+实战必备)
核心思想:定义对象间的一对多依赖关系,当一个对象(被观察者)的状态发生变化时,所有依赖它的对象(观察者)都会自动收到通知并更新(类似“公众号订阅:作者发文(被观察者变化),所有订阅者(观察者)收到推送”)。
实战场景:Spring中的事件驱动模型(如ContextRefreshedEvent)、消息通知系统、订单状态变更通知(订单支付后,通知库存、物流、积分系统)。
极简实战代码(订单状态通知):
// 1. 观察者接口(订阅者,统一更新方法)
public interface Observer {
// 接收通知,更新自身状态(参数为被观察者的状态)
void update(String orderStatus);
}
// 2. 具体观察者1(库存系统,接收订单状态通知)
public class StockObserver implements Observer {
@Override
public void update(String orderStatus) {
if("paid".equals(orderStatus)) {
System.out.println("库存系统:订单已支付,减少库存");
}
}
}
// 3. 具体观察者2(物流系统,接收订单状态通知)
public class LogisticsObserver implements Observer {
@Override
public void update(String orderStatus){
if("paid".equals(orderStatus)) {
System.out.println("物流系统:订单已支付,创建物流单");
}
}
}
// 4. 被观察者接口(公众号/订单,统一订阅、取消订阅、通知方法)
public interface Observable {
void addObserver(Observer observer);
// 新增观察者(订阅)
void removeObserver(Observer observer); // 移除观察者(取消订阅)
void notifyObservers(String status);
// 通知所有观察者(推送)
}
// 5. 具体被观察者(订单对象,状态变化时通知观察者)
public class OrderObservable implements Observable {
// 存储所有观察者(订阅者列表)
private List<Observer> observers = new ArrayList<>();
@Override
public void addObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String status) {
// 遍历所有观察者,发送通知
for (Observer observer :observers) {
observer.update(status);
}
}
// 订单状态变更方法(触发通知的入口)
public void changeStatus(String status) {
System.out.println("订单状态变更为:" + status);
notifyObservers(status); // 状态变更后,通知所有观察者
}
}
// 6. 实战调用(订单支付后,自动通知库存、物流系统)
public class Test {
public static void main(String[] args) {
// 1. 创建被观察者(订单)
OrderObservable order = new OrderObservable();
// 2. 创建观察者(库存、物流)
Observer stockObserver = new StockObserver();
Observer logisticsObserver = new LogisticsObserver();
// 3. 订阅(观察者注册到被观察者)
order.addObserver(stockObserver);
order.addObserver(logisticsObserver);
// 4. 变更订单状态(触发通知)
order.changeStatus("paid"); // 订单支付,通知所有观察者
}
}
(六)策略模式(行为型|实战必备)
核心思想:定义一系列算法(策略),将每个算法封装起来,使它们可以相互替换,且算法的变化不影响使用算法的客户端(类似“出门选择交通方式:公交、地铁、打车,算法不同,但目的都是到达目的地,可灵活切换”)。
实战场景:电商平台的折扣策略(新用户折扣、会员折扣、节日折扣)、排序算法切换、支付方式切换(和工厂模式结合使用)。
极简实战代码(电商折扣策略):
// 1. 策略接口(统一算法规范,折扣策略)
public interface DiscountStrategy {
// 计算折扣后价格(参数:原价)
double calculateDiscount(double originalPrice);
}
// 2. 具体策略1(新用户折扣:9折)
public class NewUserDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
System.out.println("新用户折扣:9折");
return originalPrice * 0.9;
}
}
// 3. 具体策略2(会员折扣:8折)
public class MemberDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
System.out.println("会员折扣:8折");
return originalPrice * 0.8;
}
}
// 4. 具体策略3(节日折扣:7折)
public class FestivalDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
System.out.println("节日折扣:7折");
return originalPrice * 0.7;
}
}
// 5. 上下文(客户端,持有策略对象,统一调用入口)
public class DiscountContext {
// 持有策略对象(可灵活切换)
private DiscountStrategy strategy;
// 构造方法,传入策略对象
public DiscountContext(DiscountStrategy strategy) {
this.strategy = strategy;
}
// 切换策略(动态更换算法)
public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}
// 统一调用策略方法,计算最终价格
public double getFinalPrice(double originalPrice) {
return strategy.calculateDiscount(originalPrice);
}
}
// 6. 实战调用(灵活切换折扣策略,无需修改客户端代码)
public class Test {
public static void main(String[] args) {
double originalPrice = 100.0; // 商品原价
// 1. 新用户购买,使用新用户策略
DiscountContext context = new DiscountContext(new NewUserDiscount());
System.out.println("新用户最终价格:" + context.getFinalPrice(originalPrice));
// 2. 切换为会员策略(动态切换,无需修改上下文代码)
context.setStrategy(new MemberDiscount());
System.out.println("会员最终价格:" + context.getFinalPrice(originalPrice));
// 3. 切换为节日策略
context.setStrategy(new FestivalDiscount());
System.out.println("节日最终价格:" + context.getFinalPrice(originalPrice));
}
}
(七)适配器模式(结构型|实战常用)
核心思想:将一个类的接口转换成客户端期望的另一个接口,使原本接口不兼容的类可以一起工作(类似“充电器适配器:手机需要5V电压,插座输出220V,适配器转换电压,使两者兼容”)。
实战场景:Java中的InputStreamReader(将字节流InputStream适配为字符流Reader)、SpringMVC中的HandlerAdapter(适配不同的处理器Handler)、旧系统接口改造(新接口适配旧接口,无需修改旧代码)。
极简实战代码(旧接口适配新接口):
// 1. 新接口(客户端期望的接口,如新版支付接口)
public interface NewPayService {
void payOnline(double money); // 在线支付方法(参数:金额)
}
// 2. 旧接口(已存在的类,接口不兼容,如旧版支付接口)
public class OldPayService {
// 旧接口方法(参数:金额,返回支付结果)
public String pay(double money) {
return "旧版支付:支付金额" + money + "元,支付成功";
}
}
// 3. 适配器类(将旧接口适配为新接口,核心)
public class PayAdapter implements NewPayService {
// 持有旧接口对象(被适配的对象)
private OldPayService oldPayService;
public PayAdapter(OldPayService oldPayService) {
this.oldPayService = oldPayService;
}
// 实现新接口方法,内部调用旧接口方法(适配逻辑)
@Override
public void payOnline(double money) {
String result =oldPayService.pay(money);
System.out.println(result); // 适配新接口的输出格式
}
}
// 4. 实战调用(客户端调用新接口,底层使用旧接口实现)
public class Test {
public static void main(String[] args) {
// 旧接口对象(已存在,无需修改)
OldPayService oldPayService = new OldPayService();
// 适配器(将旧接口适配为新接口)
NewPayService newPayService = new PayAdapter(oldPayService);
// 客户端调用新接口(无需关心底层是旧接口实现)
newPayService.payOnline(100.0);
}
}
(八)模板方法模式(行为型|高频面试)
核心思想:定义一个算法的骨架(模板方法),将算法中的某些步骤延迟到子类中实现,使子类可以在不改变算法骨架的前提下,重新定义算法的某些步骤(类似“做饭模板:先准备食材→烹饪→装盘,烹饪步骤可延迟到子类(炒、煮、蒸)”)。
实战场景:Spring中的JdbcTemplate(模板方法定义JDBC操作骨架,子类实现具体的查询/更新逻辑)、Servlet的doGet/doPost方法(模板方法定义请求处理流程,子类实现具体逻辑)。
极简实战代码(做饭模板):
// 1. 抽象模板类(定义算法骨架,模板方法+抽象步骤)
public abstract class CookTemplate {
// 模板方法(算法骨架,定义做饭的流程,不可被子类修改)
public final void cook() {
prepareIngredients(); // 步骤1:准备食材(固定步骤,父类实现)
cookFood(); // 步骤2:烹饪(延迟到子类实现,可变步骤)
serveFood(); // 步骤3:装盘(固定步骤,父类实现)
}
// 固定步骤1:准备食材(父类实现,所有子类共用)
private void prepareIngredients() {
System.out.println("准备食材:清洗、切配");
}
// 抽象步骤2:烹饪(延迟到子类实现,不同子类有不同实现)
protected abstract void cookFood();
// 固定步骤3:装盘(父类实现,所有子类共用)
private void serveFood() {
System.out.println("装盘完成,可食用");
}
}
// 2. 具体子类1(炒青菜,实现烹饪步骤)
public class StirFryVegetable extends CookTemplate {
@Override
protected void cookFood() {
System.out.println("烹饪:热油下锅,放入青菜翻炒,加调料,翻炒出锅");
}
}
// 3. 具体子类2(煮面条,实现烹饪步骤)
public class BoilNoodle extends CookTemplate {
@Override
protected void cookFood() {
System.out.println("烹饪:加水煮沸,放入面条,煮5分钟,加调料出锅");
}
}
// 4. 实战调用(调用模板方法,子类自动执行完整流程)
public class Test {
public static void main(String[] args) {
// 炒青菜
CookTemplate stirFry = new StirFryVegetable();
System.out.println("开始做炒青菜:");
stirFry.cook();
System.out.println("----------------");
// 煮面条
CookTemplate boilNoodle = new BoilNoodle();
System.out.println("开始煮面条:");
boilNoodle.cook();
}
}
面试重点:模板方法为什么用final修饰?—— 防止子类修改算法骨架,保证算法流程的一致性。
三、实战+面试小贴士(必看)
1. 不要为了用设计模式而用设计模式!简单场景(如一个工具类)直接写,复杂场景(多对象、多逻辑切换、需要扩展)再考虑设计模式;
2. 8种模式优先级:单例、工厂、代理、观察者(这4种最常用,先吃透)→ 装饰器、策略、适配器 → 模板方法;
3. 面试应答技巧:被问设计模式,先讲「核心思想」,再讲「实战场景」(结合Spring/MyBatis源码),最后说「注意事项」(如单例的线程安全、代理的两种实现),加分项;
4. 实战技巧:很多场景是“多种模式结合”(如工厂模式+策略模式实现支付方式切换,代理模式+装饰器模式实现功能增强);
5. 避坑点:不要过度设计(如简单业务用抽象工厂,反而增加复杂度),设计模式的核心是“简化代码”,不是“增加复杂度”。
最后,设计模式的掌握离不开实战,建议结合自己的项目,尝试用上面的模式优化代码(如用单例优化工具类、用策略模式优化多逻辑判断),多练、多复盘,才能真正吃透~
祝大家面试时能从容应答设计模式问题,工作中能用设计模式写出更优雅、易维护的代码,进阶高级Java工程师💪
留言区互动:你工作中最常用哪种设计模式?遇到过哪些设计模式的坑?评论区交流讨论~

2052

被折叠的 条评论
为什么被折叠?



