Java 中的 23 种设计模式详解
设计模式是软件开发中的最佳实践,帮助开发者在面对复杂设计问题时提供有效的解决方案。GoF(Gang of Four)在其经典著作《设计模式:可复用面向对象软件的基础》中定义了 23 种设计模式。。本文将详细介绍23种经典设计模式,包括创建型模式、结构型模式和行为型模式,提供每种模式的定义、原理、优点、Java示例代码以及详细注释。
目的 | 范围 | 名称 | 描述 |
---|---|---|---|
创建型(Creational) | 对象 | 单例模式(Singleton) | 保证一个类仅有一个实例:并提供一个访问它的全局访问点。 |
原型模式(Prototype) | 用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。 | ||
建造者模式(Builder) | 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 | ||
抽象工厂模式(Abstract Factory) | 提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。 | ||
类 | 工厂方法模式(Factory Method) | 定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使个类的实例化延迟到其子类。 | |
结构型(Structural) | 对象 | 适配器模式(Adapter) | 将一个类的接口转换成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 |
桥接模式(Bridge) | 将抽象部分与它的实现部分分离,使它们都可以独立地变化。 | ||
组合模式(Composite) | 将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得客户对单个对象和复合对象的使用具有一致性。 | ||
装饰模式(Decorator) | 动态地给一个对象添加一些额外的职责。就扩展功能而言,Decorator模式比生成子类方式更为灵活。 | ||
外观模式(Facade) | 运用共享技术有效地支持大量细粒度的对象。 | ||
享元模式(Flyweight) | 为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 | ||
代理模式(Proxy) | 为其他对象提供一个代理以控制对这个对象的访问。 | ||
类 | 适配器模式(Adapter) | 将一个类的接口转换成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 | |
行为型(Behavioral) | 对象 | 责任链模式(Chain of Responsibility) | 为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 |
命令模式(Command) | 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。 | ||
迭代器模式(lterator) | 提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示。 | ||
中介者模式(Mediator) | 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 | ||
备志录模式(Memento) | 在不破坏封装性的前提下,捕获一个对象的内部状态。 | ||
观察者模式(Observer) | 定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。 | ||
状态模式(State) | 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。 | ||
策略模式(Strategy) | 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。 | ||
访问者模式(Visitor) | 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 | ||
类 | 解释器模式(Interpreter) | 给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 | |
模板方法模式(Template Method) | 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Iethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步。 |
创建型模式(Creational Patterns)
创建型模式关注于对象的创建,提供了更灵活的对象创建方式。
创建型模式分为单例模式(Singleton Pattern)、原型模式(Prototype Pattern)、建造者模式(Builder Pattern)、工厂方法模式(Factory Method Pattern)、抽象工厂模式(Abstract Factory Pattern)
单例模式(Singleton Pattern)
问题:
在某些情况下,需要确保一个类只有一个实例,并且需要一个全局访问点来访问这个实例。例如,在一个应用程序中,一个配置管理器类需要保持一致的配置数据,以避免不同部分之间的配置冲突。
解决方案:
单例模式通过确保一个类只能创建一个实例,并提供一个静态方法或静态属性来访问这个实例。通常,单例类会将自己的构造函数声明为私有,以防止外部代码直接new创建实例。通过一个静态方法,单例类可以控制在运行时只能获得同一个实例。在需要协调访问共享资源(例如数据库连接)的场景中,单例能有效控制并发访问,防止产生冲突。
效果:
单例模式的应用可以确保在整个应用程序中只有一个实例存在,从而节省了资源和内存。它也可以提供一个全局的访问点,使得代码中的各个部分都可以方便地获取这个实例。然而,过度使用单例模式可能导致全局状态的难以控制,以及模块之间的紧耦合。在多线程环境下需要小心处理,以确保线程安全。
总之,单例模式是一种常用的设计模式,适用于需要全局唯一实例的场景。它的核心思想在于通过限制类的实例化来控制对象的数量,从而保证全局唯一性。
常见的单例模式实现方式有:饿汉式(静态常量)、饿汉式(静态代码块)、懒汉式(线程不安全)、懒汉式(线程安全——同步方法)、双重检查锁定(Double-Check Locking)、静态内部类、枚举实现等。
饿汉式(静态常量)
- 原理:在类加载时就创建好单例实例,这样保证了线程安全,因为 Java 类的加载是线程安全的。
- 优点:
- 实现简单;
- 线程安全,无需额外的同步控制。
- 缺点:
- 没有延迟加载效果(即使应用中可能永远不会使用这个实例,也会在类加载时创建)。
Java示例
public class Singleton {
// 类加载时创建实例,保证线程安全
private static final Singleton INSTANCE = new Singleton();
// 私有构造方法,防止外部实例化
private Singleton() {
// 防止通过反射调用私有构造方法创建多个实例
if (INSTANCE != null) {
throw new IllegalStateException("实例已经存在!");
}
}
// 全局访问点
public static Singleton getInstance() {
return INSTANCE;
}
}
饿汉式(静态代码块)
- 原理:利用静态代码块在类加载时创建实例,与静态常量方式类似。
- 优点:
- 与静态常量方式相同,线程安全、实现简单。
- 缺点:
- 同样没有实现延迟加载,类加载时就完成了实例化。
Java示例
public class Singleton {
private static final Singleton INSTANCE;
// 静态代码块,在类加载时执行
static {
INSTANCE = new Singleton();
}
private Singleton() {
if (INSTANCE != null) {
throw new IllegalStateException("实例已经存在!");
}
}
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉式(线程不安全)
- 原理:在第一次调用
getInstance()
时创建实例,实现延迟加载。但在多线程环境下存在竞争条件,可能会创建多个实例。 - 优点:
- 实现了延迟加载
- 缺点:
- 在多线程场景下,两个线程可能同时判断
instance == null
,从而各自创建一个实例,违背了单例原则。
- 在多线程场景下,两个线程可能同时判断
Java示例
public class Singleton {
// 延迟加载实例
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 多线程环境下可能出现问题
instance = new Singleton();
}
return instance;
}
}
懒汉式(线程安全——同步方法)【推荐使用】
- 原理:通过在
getInstance()
方法上加synchronized
关键字,保证同一时刻只有一个线程进入该方法,从而确保只创建一个实例。 - 优点:
- 实现简单,能够保证线程安全。
- 缺点:
- 整个方法加锁,每次调用都需要同步,降低了性能(尽管实例只创建一次,但每次访问都涉及同步开销)。
Java示例
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 方法同步,保证线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查锁定(Double-Check Locking)【推荐使用】
- 原理:在进入同步块前先进行一次非同步检查,只有当实例为
null
时才进入同步块;在同步块内部再进行一次检查,确保只创建一次实例。 - 注意:必须将实例声明为
volatile
,防止由于指令重排序造成线程安全问题。 - 优点:
- 线程安全、实现延迟加载;
- 同步代码块只在第一次初始化时执行,提高了效率。
- 缺点:
- 实现相对复杂,需要正确使用
volatile
和双重检查机制。
- 实现相对复杂,需要正确使用
- 深入分析:
- 为什么需要双重检查?
第一次检查是为了避免每次都进入同步块,提高效率;第二次检查是为了防止多个线程在同步块内同时创建实例。
- volatile 的作用
在 Java 中,实例化对象(instance = new Singleton();)实际上可以分为以下几个步骤:
分配内存空间;
- 初始化对象;
- 将内存地址赋值给 instance 变量。
- 由于指令重排序,步骤 2 和 3 可能会调换,如果没有 volatile 修饰,另一个线程可能会看到一个未完全初始化的对象。
Java示例
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;
}
}
静态内部类【推荐使用】
- 原理:利用 JVM 类加载机制实现延迟加载和线程安全。静态内部类只有在外部类调用
getInstance()
时才会被加载,从而实现延迟加载效果,同时 JVM 保证类加载时的线程安全性。 - 优点:
- 实现延迟加载;
- JVM 在加载类时保证线程安全,无需显式同步;
- 代码简洁易懂。
- 缺点:
- 静态内部类的机制对初学者可能不够直观,需要理解类加载的原理。
Java示例
public class Singleton {
private Singleton() {}
// 静态内部类,负责持有 Singleton 实例
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 当调用 getInstance() 时,SingletonHolder 会被加载并初始化 INSTANCE
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举实现【推荐使用】
- 原理:
- 使用枚举类型实现单例,利用 Java 枚举的特性保证单例的唯一性和线程安全性。
- 枚举实现不仅天然防止了反射攻击,还能防止反序列化重新创建实例,因为 Java 保证了每个枚举常量在 JVM 中都是唯一的。
- 优点:
- 实现简单、代码精炼;
- 天然线程安全;
- 防止反射和反序列化破坏单例(反射很难创建枚举实例)。
- 缺点:
- 如果需要继承其他类或实现某种接口,枚举的局限性可能会带来一些限制;
- 在某些场景下,枚举的语法风格可能不符合团队的编码规范。
- 不支持延迟加载,因为枚举常量在类加载时就已经被实例化了,需要延迟加载请使用双重检查锁定或静态内部类
Java示例
public enum Singleton {
INSTANCE; // 枚举中的唯一实例
// 可以添加其他方法
public void someMethod() {
// 实现具体逻辑
}
}
public class TestSingleton {
public static void main(String[] args) {
// 获取枚举单例实例
Singleton singleton = Singleton.INSTANCE;
singleton.someMethod();
}
}
单例模式实现方式对比
实现方式 | 延迟加载 | 线程安全 | 实现复杂度 | 性能影响 | 反射/反序列化防护 |
---|---|---|---|---|---|
饿汉式(静态常量/代码块) | 否 | 是 | 低 | 较好(无同步开销) | 需额外判断防护 |
懒汉式(同步方法) | 是 | 是 | 低 | 每次调用均有同步开销 | 需额外判断防护 |
双重检查锁定 | 是 | 是 | 中 | 初次同步,后续无同步 | 需额外判断防护 |
静态内部类 | 是 | 是 | 中 | JVM机制,无额外同步 | 需额外判断防护 |
枚举 | 是 | 是 | 最低 | 最优(JVM保证) | 天然防护 |
推荐使用场景
- 双重检查锁定:适用于需要延迟加载且对性能要求较高的多线程环境,但需要确保理解 volatile 的作用。
- 静态内部类:实现简洁、延迟加载且线程安全,适合大部分业务场景。
- 枚举实现:是实现单例的最简洁和安全的方式,推荐在 JDK1.5 及以上版本中使用,特别是在需要防止反射和反序列化攻击的场合