常见设计模式详解
单例模式
单例模式是创建型设计模式的核心模式之一。主要目标是:确保一个类在程序生命周期中仅存在一个实例,并提供唯一的访问点来获取改实例。
核心特性:
- 唯一实例:类的构造方法必须被私有化,禁止外部通过New创建实例。从根源避免多实例。
- 提供唯一访问点。
单例模式 创建实例时,需要注意创建时机、并发控制和性能、防止通过特殊手段创建多实例。
常见实现方式
1、饿汉模式:在类加载时,创建对象(使用JVM加载机制,加载静态变量)线程安全保证单例。
public class Test {
// 1.私有构造方式private Test() {}
// 2。静态变量初始化对象public static final Test TEST= new Test();
//3.提供唯一访问点public static Test getTest() {return TEST;}
}
2、枚举单例:利用Java枚举单例特性,在类加载时创建且单例。
枚举类:构造方法被隐式私有化,禁止外部创建。同时天然支持序列化/反序化安全。
public enum Test{TEST;// 枚举类可以添加自定义方法public void doSomething() {。。。}
}
使用时 Test.TEST
3、懒汉式:(线程安全版本)需要使用锁保证线程安全
在获取实例的方法上添加锁,限制并发。
public class Test {private Test() {}// 声明唯一实例private static Test test;// 包装创建实例对象方法public static synchrionized Test getTest() {if (test == null) {test = new Test();}return test;}
}
4、双重锁检查机制:在 懒加载 基础上,优化锁粒度。仅在实例化初始化代码块加锁,而非整个方法。同时通过两次检查 实例对象是否为空确保线程安全。
注意:需要在变量上使用Volatile修饰,禁止指令重排,否则对线程下可能获取到”半初始化的对象“
public class Test {private Test() {}
// 声明唯一实例 private statis Volatile Test test;
// 提供唯一访问点public static Test getTest() {// 检查实例是否初始化if (test == null) {// 加锁创建对象synchronized(Test.class) {// 二次检查,防止多线程等待锁时,重复创建实例if (test==null) {test = new Test();// 不加volatile时,JVM可能重排序为:1.分配内存 → 2.返回实例 → 3.初始化实例// 导致其他线程获取到“未初始化完成”的实例}}}return test;
}
5、静态内部类:相较于饿汉模式(使用静态变量),同样JVM加载 外部类时,静态内部类不会加载,只有调用时才会加载并初始化静态变量。天然的懒加载和线程安全
public class test {private Test() {}// 定义静态方法,初始化单例对象private static Test SingleTest() {// 静态内部类中初始化,静态实例private static final Test TEST = new Test();|// 提供唯一访问点public static Test getTest() {return SingleTest.TEST;}
}
注意事项
1、反射破坏与防护(枚举单例 天然防护反射)
通过Java反射机制,可强制调用私有化构造方法,创建新实例。
//反射破坏饿汉式单例
public class Reflact{public static void main(String[] args) {// 获取单例类的构造方法Constructor<Test> testConstructor = Test.class.getDeclareConstructor();//取消访问检查(强制访问私有方法)testConstructor.setAccessible(true);// 通过反射创建实例,*(此时有2个对象)Test test1 = Test.getTest();// 获取单例对象Test test2 = testConstructor.newInstance(); // 反射获取对象System.out.printf(test1==test2); // 输出false}
}
防护措施:在私有构造方法中添加”实例已存在“的判断,若已存在则抛出异常,阻止反射创建新实例。
public class Test{private static final Test TEST = new Test();private Test() {// 反射防护,若实例已存在,则报错if (TEST!=null) {throw new IllegalStateException("单例类禁止通过反射创建实例")}}public static Test getTest() {return TEST;}
}
2、序列化/反序列 破坏与防护
破坏原理:
若单例类实现了Serializable接口,遇到序列化将实例写入文件,在反序列化读取时,会创建一个新实例(破坏单例)。
// 破坏单例模式
public class SerializableTest {public static void main(String[] args) {// 序列化单实例到文件。Test test1 = Test.getTest(); // 获取单例对象// 序列化到文件ObjectOutPutStream oos = new ObjectOutPutStream(new FileOUtPutStrean("singleton.txt"));oos.writeObject(test1);// 反序列化ObjectInPutStream ois = new ObjectInPutStream(new FileInPutStream("singleton.txt"));Test test02 = (Test)ois.readObject();System.out.printf(test1 == test02); 输出false}
}
防护措施:
在单例类中 添加readResolve()方法,反序列化时会优先调用该方法 返回已拥有的实例,而非创建新实例:
public class Test Implements Seralizable {private Test() {} private static volatile Test test;public static Test getTest() {if(test==null) {synchronized(Test.class) {if(test==null) {test = new Test();}}}return test} public Object readResolve() {return getTest();}
总结:
- 不要过度使用单例:不是所有工具类都单例,对于无状态的(如StringUtils)无需单例,直接使用静态方法即可。
- 注意线程安全:
- 不要滥用全局访问;单例的“全局访问”可能导致代码 耦合。后续难以重构。
适用场景
资源复用和状态统一
- 资源密集型对象:数据库连接池、线程池、Redis连接池等,避免频繁创建/销毁。
- 全局状态管理:应用配置类(如application.properties的配置),全局日志器(logger)、用户会话管理器(多模块共享状态)
- 工具类/服务类:Spring中的Bean(默认单例),分布式Id生成器,全局计数器等。确保逻辑一致。
场景匹配
无参数、无需懒加载 → 枚举单例(最简单安全);
无参数、需懒加载 → 静态内部类(比 DCL 简洁);
需动态参数、需懒加载 → 双重检查锁(DCL)(需加 volatile);
单线程 / 低频场景 → 饿汉式 / 同步方法懒汉式(不推荐生产使用)。
工厂模式
是创建型设计模式的核心代表,核心思想:将对象的创建与使用分离。
通过 工厂类,负责对象实例化,解决直接new对象导致的代码耦合,扩展性差的问题。
分为:简单工厂模式、工厂方法模式、抽象工厂模式。
为了解决:通常使用直接new 对象,调用方法。
存在弊端:**耦合高,扩展性差、职责不单一。**即创建对象和使用对象绑定,使用对象的类即负责创建由负责使用。
工厂模式引入工厂角色,将对象的创建逻辑集中管理。
常见实现方式
1、简单工厂模式
“入门版”通过单一工厂类根据输入参数动态决定创建哪种实例。
适合;产品种类少,变化不频繁的场景。解决直接new的场景。
角色:
- 产品接口:定义产品统一行为。
- 具体产品:实现产品具体类。
- 简单工厂:核心,提供静态方法根据输入创建对应的具体产品实例。
// 产品接口
public imterface Fruit {void eat();// 统一行为,吃水果
}
// 具体产品1;苹果
public class Apple Impaement Fruit {@Overridepublic void eat() {。。。}
}
// 具体产品2
public void Banana implements Furit {@Overridepublic void eat() {...}
}// 简单工厂类 核心
public class FuritFactory{//静态方法,根据参数创建产品public static Fruit createFruit(String type){switch(type.toLowCase()){case "apple":return new Apply();case "banana":return new Banana();default:throw new IllegalAgumentException("未知水果类型");}}
}
使用
Fruit apple = Fruit。createFruit("apple"); apple.eat();
解耦、集中管理,便于维护。
违反开闭原则,产品过多时工厂臃肿。
2、工厂方法模式:
针对简单工厂“违反开闭原则”问题,工厂方法模式:将单一工厂拆分""多个工厂+多个工厂实现类。每个工厂负责单一产品。
角色:
- 产品接口:统一所有产品的行为。
- 具体产品:实现产品的具体类。
- 工厂定义:定义共产的统一行为。提供产品的抽象方法。
- 具体工厂:实现工厂对应的具体产品。
优缺点:符合开闭原则,单一职责。类数量冗余,复杂度提升。
3、抽象工厂模式
当产品是“产品簇”一组相关链的产品,而非单个产品。工厂方法模式无法高效管理,此时抽象工厂可以很好解决,它能创建一整套 产品族。
角色:
- 抽象产品接口:按照产品等级定义多个产品(如水果、果汁)。
- 具体产品:针对多个抽象产品定义对应实现(如:苹果、苹果汁)。
- 抽象工厂接口:定义创建 一个产品族所有方法(如创建水果,创建果汁)。
- 具体工厂实现:实现抽象工厂的每个抽象方法。
小结:简单工厂:工厂直接创建所有产品。工厂方法:将工厂专一化,一个工厂产一个产品。抽象工厂:定义不同的产品,相关的产品生产交给一个抽象工厂生产(即结合简单工厂和工厂方法。一个工厂生产只一类产品)。
适用场景:
简单场景:产品固定(如仅 3-5 种)、几乎不扩展 → 选简单工厂(代码少、易维护)。
单产品等级 + 频繁扩展:如仅需创建 “水果”,但水果种类不断加 → 选工厂方法。
多产品等级 + 产品族关联:如需要同时创建 “水果 + 果汁”“按钮 + 文本框” → 选抽象工厂。
应用案例:
JDK中的工厂模式:Collection 的iterator():每个集合都有自己的迭代器工厂。
SPring中的Bean Factory(抽象工厂)负责创建Spring容器中的Bean,ApplicationContext是具体实现。
Mybatis的SqlSessionFactory抽象工厂。创建SqlSession产品族核心对象。DefaultSQL SessionFactory是具体实现。
观察者模式
迭代模式
装饰器模式
是结构型设计模式的一种,核心是动态的给对象添加额外的功能,同时不改变对象本身结构。
该模式 比继承更灵活,能够在不创建大量子类的情况下,实现功能复用和组合。
解决问题:不使用 装饰器模式时,为对象添加新功能,只能通过 修改源码或者继承子类。
特点:
通过组合而非继承的 方式,解决了上述问题。
- 可以动态、灵活地给对象添加功能。
- 可以叠加多个功能
- 新增功能无序修改原有代码。
角色:
- 组件接口(Component):定义被装饰 对象和装饰器的共同行为。所有对象的顶层抽象。
- 具体组件(ConcreteComponent): 被装饰的原始对象,实现组件接口。
- 装饰器抽象类(Decorator):实现组件接口,同时持有组件接口的引用(通过构造函数传入),是所有具体装饰器的父类。
- 具体装饰器(ConcreteDecorator):继承装饰器抽象类,负责给组件添加具体功能,
代码示例:
// 抽象组件接口
interface Component{public void operation(); // 声明组件基础功能
}
// 具体组件角色
class ConcreteComponent implements Component{public ConcreteComponent() {System.out.println("创建具体组件角色");}public void operation () {System.out.println("组件角色功能");}
}// 抽象装饰角色
class Decorator implements Component {private Component component; // 装饰器中提供 原始组件的调用public Decorator(Component component) { // 通过构造器传入this.component = component;}// 装饰器中调用 原始组件的功能public void operation() {component.operation();}
}
// 具体装饰器对象。
class ConcreteDecorator extends Decorator{// 初始化 装饰器对象public ConcreteDecorator(Component component) {super(component); }public void operation() {super.operation();addBehavior(); // 新增的装饰方法}public void addBehavior() {System.out.println("为具体组件新增额外功能);}
}// 使用时:
public static void main(String[] args) {Component component = new ConcreteComponent(); //原始 组件对象component.operation();System.out.println("------");Component decorator = new ConcreteDecorator(component);decorator.operation();
}
优缺点:
- 比继承更灵活:可以给对象添加功能,且可以灵活组合多个功能。
- 符合开闭原则:新增功能只需要添加新的装饰类,无序修改原有代码。
- 单一职责:每个装饰类只负责一个功能。
- 可重复使用:装饰器可以在不同场景中被 重复使用。
- 多层装饰后 调试困难:多个装饰器装饰后,排查问题需要逐层分析装饰器逻辑。
- 必须基于接口编程:装饰器和被装饰对象必须实现同一个接口,否则无法组合。
- 客户端需要了解装饰器的顺序,有时装饰器的顺序 会影响结果。
应用案例
- java IO流:使用装饰器模式:
- InputStream是组件接口
- FileInputStream:是具体组件
- FilterInput是装饰器抽象类
- BufferInputStream(缓冲功能)、DataInputStream(数据转换功能)是具体装饰器对象。
- Spring框架
- TransacationAwateDataSourceProxy:给数据源添加事务管理功能。
- HttpServletRequestWrapper:对HTTP请求进行包装扩展。
与装饰器相关模式的区别:
- 与继承:
- 继承是静态的,编译时就确定功能,
- 装饰器是动态的,运行时可以灵活的添加和组合功能。
- 与适配器模式的区别:
- 装饰器模式:不改变接口,只扩展功能
- 适配器模式:改变接口,使不兼容的接口可以一起工作。
- 与代理模式:
- 装饰器模式:主要目的是添加功能,通常透明的扩展对象。
- 代理模式:主要目的是 控制访问,通常包含对对象的某种约束。
原型模式
代理模式
是结构型设计模式的一种,核心思想通过一个代理对象, 间接访问 ”目标对象“,在不修改目标对象代码的前提下,为其提供额外功能(如增强、控制访问、隐藏细节等)。模式在生活中随即可见。
针对场景:
直接访问对象时,可能会遇到以下问题:
- 目标对象创建成本高(如数据库连接,大文件加载),需要延迟初始化(懒加载)。
- 目标对象在远程服务器,直接访问需要处理网络通信细节(如RPC调用)。
- 需要给目标对象的方法添加额外逻辑(如日志记录、权限校验、事务管理),但不能修改目标对象代码。
- 需要限制对目标对象的访问(如某些方法只允许管理员调用)。
代理模式:通过引入 "代理对象”作为 中间层,完美解决了这些问题,实现了功能增强和“核心业务的耦合。
核心角色
- 目标接口(Subject):定义目标对象和代理对象的共同行为,是两者的统一接口。
- 目标对象(Real Subject):真正执行业务逻辑的对象,实现目标接口。
- 代理对象(Proxy):实现目标接口,持有目标对象的引用,在调用目标方法前后可以添加额外逻辑。
实现方式
- 动态代理:
指代理类在运行时期动态生成,无需手动编写代理类代码,Java中常用的动态代理有两种方式- 基于JDK动态代理:基于接口实现,只能代理接口
- 基于CGLIB动态代理:基于继承实现,可以代理类。字节码生成
JDK动态代理示例:
// 实现Invocationhandler接口,定义增强逻辑
import java.Lang.reflect.invocationHandler;
import java.Lang.reflect.Method;public class LogInvocationHandler implements invocationHandler {// 目标对象private Object target;public LogInvocationHandler(Object target) {this.target = target;}//当调用代理对象的方法时,会自动代用该方法。@Overriedpublic Object invoke(Object proxy,Method method,Object[] args) throws Throw able {// 方法调用前增强System.out.println("【日志】准备执行方法:" + method.getName());// 调用目标对象的方法Object result = method.invoke(target, args);// 方法调用后增强System.out.println("【日志】方法" + method.getName() + "执行完成");return result;}
}
- 静态代理:
指代理类在编译期就已确定,需要手动编写代理类代码。
代码示例:
// 抽象接口
public interface UserService {void login(String username,String passward); // 定义目标方法void logout();
}// 目标对象
public class UserServiceImple implements UserService {@Overriedpublic void loging(String username,String password) {System.out.println("用户“【"+username+"]登陆成功");}@Overriedpublic void logout(String username,String password) {System.out.println("退出")}
}// 静态代理,通过硬编码 指定代理逻辑
public class UserServiceProxy implements UserService {// 持有目标对象的引用private UserService userService;// 通过构造函数传入目标对象。public UserServiceProxy(UserService userService) {this.userService = userService;}// 实现抽象接口的方法,调用目标对象的方法 进行代理。@Overriedpublic void login(String username,String password) {// 前置增强System.out.println("[日志]准备登陆操作");// 目标对象方法user Service。login(username,password);//后置方法System.out.println("[日志]登陆完成");} @Overridepublic void logout() {System.out.println("前置增强");userService.logout();System.out.println("后置增强");}
}
代理的类型
- 远程代理(Remote Proxy):
- 为远程对象(另一台服务器上的对象) 创建本地代理,隐藏网络通信细节。
- 如RPC框架中,本地代理对象负责 与远程服务通信。
- 虚拟代理(Virtual Proxy):
- 用于创建开销大的对象,实现懒加载(只有在需要时才创建目标对象)。
- 例如:图片加载框架中,先用占位符代替真实图片,当用户滚动到该图片时在加载。
- 保护代理(Protection Proxy):
- 控制对 目标对象的访问,实现权限控制。
- 例如:某些方法只允许管理员访问,代理对象会先检查访问者用户权限。
- 日志记录代理(Logging Proxy):
- 在方法调用前后记录日志 用于调试或审计。
- 例如:SpringAOP中日志切面就是 基于代理模式实现的。
- 缓存代理:
- 缓存方法调用结果,避免重复计算或请求。
- 例如:Mybatis中的二级缓存,Redis缓存代理。
优缺点
优点:
- 解耦:代理对象与目标对象分离,增强逻辑(如日志、权限)与核心逻辑分离。
- 扩展性好:可以在不修改目标对象的情况下,通过代理对象零或添加功能。
- 保护目标对象:隐藏对目标对象的实现细节,控制访问权限。
- 灵活性高:动态代理可以在运行时为不同目标对象生成代理。
缺点:
- 增加系统复杂性:引入代理层,增加了代码理解和调试难度。
- 性能损耗:代理对象会额外处理增强逻辑,可能代理轻微的性能开销。
- 静态代理的局限性:需要为每个目标对象编写代理类,代码冗余;接口变更时,代理类也需要更改。
与装饰器模式
代理模式:控制访问、隐藏细节、增强功能。通常指导目标对象的类型。通常增强逻辑时固定的(日志、权限)。客户端通常无法察觉不到代理对象。
装饰器模式:动态添加功能,不需要知道目标对象的类型。增强逻辑可以灵活组合,客户端知道并主动组合装饰器。
策略模式
是行为型设计模式的一种,核心解决同一种行为在不同场景下有多种实现方式,且需要灵活切换的场景。
针对问题
- 避免大量If-else或switch判断逻辑(如不同支付方式,不同排序算法)。
- 解决”硬编码“问题,当需要新增实现方式时,无需修改原先代码,只需扩展新策略。
- 实现行为与使用行为的代码分离,使策略可以独立 进行完善。
角色
- 策略接口(Strategy):定义所有支持的 算法/行为的公共接口(如paymentStrategy定义的pay()方法)。
- 具体策略(ConcreteStrategy):实现策略接口,包含具体的算法逻辑。
- 上下文(Context):持有策略接口的引用,负责调用具体的策略。
代码示例
// 订单支付系统为例,不同方式支付作为策略
// 策略接口
public interface PaymentStrategy{void pay(double amount);
}
// 具体实现 支付宝
public class AlipayStrategy implements PayStrategy {@Overridepublic void pay(double amount) {System.out.println("使用支付宝支付");}
}
// 具体实现 微信
public class WechatpayStrategy implements PaymentStrategy {@Overridepublic void pay(double amount) {System.out.println("使用weixin支付");}
}// 上下文;订单
public class Order {private PaymentStratrgy strategy;public Order(PaymentStrategy strategy) {this.strategy = strategy;}// 调用策略执行支付public void pay(double amount) {strategy.pay(amount);}
}
// 5. 客户端使用
public class Client {public static void main(String[] args) {// 选择支付宝策略Order order1 = new Order(new AlipayStrategy());order1.pay(100); // 输出:使用支付宝支付:100.0元// 切换为微信策略Order order2 = new Order(new WechatPayStrategy());order2.pay(200); // 输出:使用微信支付:200.0元}
}
优点:
- 消除大量条件判断:用策略选择代替if-else,代码更清晰,易维护。
- 符合开闭原则:新增只需要实现接口,无需修改上下文或其他策略。
- 策略可复用:同一策略可在不同场景中被多个上下文复用。
- 灵活性高:运行时可动态切换策略。
缺点
- 增加类数量:每个策略都需要对应一个类,策略过多时会导致类膨胀。
- 客户端需了解策略:客户端必须所有策略的存在,才能选择合适的策略。
- 策略间无法共享状态:若策略需要共享数据,需要额外设计(如上下传递)。
与相关模式区别:
- 与状态模式:
- 策略模式:策略的选择由客户端决定,策略间无依赖。
- 状态模式:状态的切换由上下文根据内部状态自动触发,状态间可能有依赖。
- 与模板方法模式:
- 策略模式:通过组合实现算法替换,更灵活(运行时切换)。
- 模板方法模式:通过继承实现算法骨架固定,步骤可变,更稳定(编译时确定)。