Skip to content

S02-06 面向对象-设计模式基础

[TOC]

设计模式

概述

设计模式(Design Patterns) 是软件工程中的“武功秘籍”或“兵法”。它是针对软件开发中反复出现的常见问题,总结出的一套通用的解决方案。它不是具体的代码,而是一种思想模板

设计模式通常被分为三大类(共 23 种经典 GoF 模式):创建型结构型行为型

以下是这三大类中 Java 开发者最常遇到、必须掌握的核心模式详解。

创建型

创建型模式 (Creational Patterns):优雅地创建对象,隐藏 new 关键字背后的复杂逻辑。试图将对象的创建与使用分离(解耦)。

单例模式

单例模式 (Singleton Pattern)

  • 场景: 保证一个类只有一个实例,并提供一个全局访问点。

  • 现实例子: Windows 的任务管理器(只能打开一个)、数据库连接池、Spring 中的 Bean(默认单例)。

  • 代码示例(饿汉式 - 最简单):

    java
    public class Singleton {
      // 1. 私有化构造方法,防止外部 new
      private Singleton() {}  
    
      // 2. 内部创建好唯一的实例 (static 保证只有一份)
      private static final Singleton INSTANCE = new Singleton();  
    
      // 3. 提供对外获取的方法
      public static Singleton getInstance() {  
        return INSTANCE;
      }
    }

工厂模式

工厂模式 (Factory Pattern):

  • 场景: 想要创建对象,但不想让调用者知道具体的类名或创建细节。

  • 核心: 用一个“工厂类”来代替 new 操作。

  • 现实例子: 你去买车,你只需要告诉工厂“我要一辆 Tesla”,工厂负责把零件组装好给你,你不需要知道怎么造车。

  • 代码示例:

    java
    // 简单工厂
    class CarFactory {
      public static Car getCar(String type) {
        if ("Tesla".equals(type)) {
          return new Tesla();
        } else if ("BMW".equals(type)) {
          return new BMW();
        }
        return null;
      }
    }
    
    // 调用者
    Car myCar = CarFactory.getCar("Tesla");

建造者模式

建造者模式 (Builder Pattern):

  • 场景: 创建一个包含很多参数的复杂对象,且参数组合灵活。

  • 痛点: 避免出现 new User("张三", null, 18, "北京", null, ...) 这种难以阅读的代码。

  • 代码示例(链式调用):

    java
    // 使用 Builder 模式后
    User user = new User.Builder()
      .setName("Gemini")
      .setAge(3)
      .setCity("Google Cloud")
      .build();

注:在 Java 中,通常使用 Lombok 的 @Builder 注解自动生成此模式代码。

结构型

结构型模式 (Structural Patterns):关注类和对象如何组合成更大的结构。

代理模式

代理模式 (Proxy Pattern):

  • 场景: 给某一个对象提供一个代理,用来控制对这个对象的访问。
  • 核心: 中介。我想找明星唱歌,不能直接找明星,要先找经纪人(代理)。经纪人负责谈价钱、安排时间(增强功能),最后让明星唱歌(调用原方法)。
  • 应用: Spring AOP(面向切面编程)的核心。比如在方法执行前自动开启事务,执行后提交事务。

装饰器模式

装饰器模式 (Decorator Pattern):

  • 场景: 动态地给一个对象添加一些额外的职责。

  • 核心: 套娃

  • Java 源码例子: Java IO 流。

    java
    // 这里的 BufferedInputStream 就是一个装饰器,给 FileInputStream 增加了缓冲功能
    InputStream in = new BufferedInputStream(new FileInputStream("test.txt"));

适配器模式

适配器模式 (Adapter Pattern):

  • 场景: 将一个类的接口转换成客户希望的另一个接口。
  • 现实例子: 电源转接头(把两孔插座转为三孔)、USB 转 Type-C。

行为型

行为型模式 (Behavioral Patterns):关注对象之间的通信、职责划分和算法封装。

观察者模式

观察者模式 (Observer Pattern):

  • 场景: 当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

  • 现实例子:

    • 微信公众号: 博主(被观察者)发文章,所有关注者(观察者)都会收到推送。
    • Vue/React: 数据变了,视图自动更新。
  • 代码结构: 被观察者维护一个 List<Observer>,当事件发生时,遍历 List 调用每个观察者的 update() 方法。

策略模式

策略模式 (Strategy Pattern):

  • 场景: 定义一系列算法,把它们封装起来,并且使它们可以相互替换。

  • 现实例子:

    • 地图导航: 从 A 到 B,你可以选择“最快路线”、“不走高速”、“最短路程”。这就是三种不同的策略,但输入输出是一样的。
    • 支付接口: 支付(100 元),可以选支付宝策略、微信策略、银行卡策略。
  • 代码示例:

    java
    // 不建议写大量的 if-else
    /* if (type == "AliPay") { ... }
    else if (type == "WeChat") { ... }
    */
    
    // 建议使用策略模式
    paymentStrategy.pay(100); // 具体的 strategy 是多态传入的

模板方法模式

模板方法模式 (Template Method):

  • 场景: 定义一个算法的骨架,将一些步骤延迟到子类中实现。
  • 现实例子:
    • 做菜: 开火 -> (倒具体的油) -> (炒具体的菜) -> 加盐 -> 出锅。
      • “倒油”和“炒菜”是抽象方法,由子类决定,但整体流程是父类定死的。

总结速查表

总结速查表:

分类常用模式一句话口诀
创建型单例 (Singleton)保证只有一个实例
工厂 (Factory)封装创建细节,解耦
建造者 (Builder)复杂对象链式构建
结构型代理 (Proxy)找中介,增强功能 (AOP)
适配器 (Adapter)接口转换,兼容老代码
装饰器 (Decorator)动态加功能 (IO 流)
行为型观察者 (Observer)一对多通知 (发布-订阅)
策略 (Strategy)替换算法,消除 if-else
模板 (Template)定义流程骨架,子类填空

应该如何学习

应该如何学习:

  1. 不要死记硬背: 模式是为了解决问题的。先有痛点(比如代码太乱、耦合太紧),再用模式。

  2. 关注源码: JDK 和 Spring 框架源码中充满了设计模式。

    • Runtime 类是单例的。
    • IO 流是装饰器。
    • JdbcTemplate 是模板模式。
  3. 从单例开始: 它是最简单也最容易踩坑(线程安全)的模式。

单例模式

概述

单例模式(Singleton Pattern) 是 Java 中最基础、也是面试时坑最多的设计模式。它的核心目标极其简单:确保一个类在 JVM 中只有一个实例,并提供一个全局访问点。

虽然概念简单,但在多线程环境下如何写出高性能线程安全的单例,却包含了 Java 并发编程的精髓(锁、volatile、类加载机制)。

以下是 Java 中单例模式的全方位详解,包含 5 种常见写法及其优劣势分析。

核心三要素

无论哪种写法,所有的单例模式都必须满足这三点:

  1. 构造方法私有化 (private): 禁止外部通过 new 关键字随便创建对象。

  2. 内部持有实例 (private static): 类自己必须维护这个唯一的实例。

  3. 对外提供获取点 (public static): 提供一个公共的静态方法,让外部拿到这个实例。

五种常见写法

饿汉式@

饿汉式 (Eager Initialization) —— 最简单,推荐:

原理: 只要类被加载,实例就会立即被创建。不管你后边用不用,先造出来再说(所以叫“饿”)

java
public class SingletonEager {
  // 1. 类加载时直接实例化 (static final 保证唯一且不可变)
  private static final SingletonEager INSTANCE = new SingletonEager();  

  // 2. 构造私有
  private SingletonEager() {}  

  // 3. 直接返回
  public static SingletonEager getInstance() {  
    return INSTANCE;
  }
}
  • 优点:写法简单,天生线程安全(JVM 类加载机制保证)。
  • 缺点即使没使用,也会占用内存。如果初始化工作很重,会拖慢启动速度。

懒汉式@

懒汉式 (Lazy Initialization) —— 线程不安全,不推荐:

原理只有当第一次调用 getInstance() 时才去调用构造器创建实例对象(延迟加载)

java
public class SingletonLazy {
  // 1. 不在初始化属性时创建实例对象
  private static SingletonLazy instance;

  private SingletonLazy() {}

  public static SingletonLazy getInstance() {
    // 2. 只有当第一次调用 getInstance() 时才去创建实例对象
    // ⚠️ 致命问题:多线程下,两个线程同时判断 instance == null,会创建两个对象!
    if (instance == null) {
      instance = new SingletonLazy();
    }
    return instance;
  }
}
  • ⚠️ 致命问题线程不安全,多线程下,两个线程同时判断 instance == null,会创建两个对象!
  • 评价:只能在单线程下玩玩,严禁在生产环境使用

双重检查锁

双重检查锁 (DCL, Double-Checked Locking) —— 面试必考:

原理: 为了解决懒汉式的线程安全问题,同时又不希望每次获取实例都加锁(那样太慢),我们只在实例化那一刻加锁。

注意:这里必须加 volatile 关键字!

java
public class SingletonDCL {
  // ⚡️ 必须加 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 SingletonDCL(); 这行代码在 JVM 中分为三步:

  1. 分配内存空间。

  2. 初始化对象。

  3. instance 指向该内存地址。

如果没有 volatile,CPU 可能会指令重排,变成 1 -> 3 -> 2

  • 线程 A 执行完 1 和 3(此时 instance 已经不是 null 了,但还没初始化)。
  • 线程 B 进来判断 instance != null,直接拿走了这个还没初始化好的“半成品”对象去使用,导致程序崩溃。
  • volatile 禁止了这种重排序。

静态内部类@

静态内部类 (Static Inner Class) —— 最优雅,推荐:

原理: 利用 Java 的类加载机制来实现延迟加载和线程安全。

  • 外部类加载时,不会立即加载内部类。

  • 只有当调用 getInstance() 时,JVM 才会加载 SingletonHolder,进而初始化 INSTANCE

    java
    public class SingletonInner {
      private SingletonInner() {}
    
      // 静态内部类,只有被调用时才会加载
      private static class SingletonHolder {
        private static final SingletonInner INSTANCE = new SingletonInner();
      }
    
      public static SingletonInner getInstance() {
        return SingletonHolder.INSTANCE;
      }
    }
  • 优点: 既实现了延迟加载,又保证了线程安全,代码还简洁。

枚举单例

枚举单例 (Enum) —— 最安全,大神推荐:

原理: 这是《Effective Java》作者 Joshua Bloch 极力推荐的写法。

java
public enum SingletonEnum {
  INSTANCE; // 这就是一个天然的单例

  public void doSomething() {
    System.out.println("我是单例的方法");
  }
}

// 调用方式
// SingletonEnum.INSTANCE.doSomething();
  • 优点:
  1. 代码极少。

  2. 绝对防止破坏: 前面 4 种写法,通过反射 (Reflection)序列化 (Serialization) 都可以强行创建新实例,破坏单例。只有枚举类型,JVM 从根本上禁止了反射创建枚举实例,且自动处理序列化问题。

总结对比表

总结对比表:

写法懒加载 (Lazy)线程安全性能推荐指数备注
饿汉式❌ (否)✅ (是)⭐⭐⭐⭐简单实用,除非对象非常大
懒汉式有 Bug,别用
DCL (双重锁)⭐⭐⭐面试常考,注意 volatile
静态内部类⭐⭐⭐⭐⭐优雅,通用性强
枚举极高⭐⭐⭐⭐⭐防御性最强,无法被破坏

JDK 中的单例模式应用

JDK 中的单例模式应用:

  • java.lang.Runtime: 典型的饿汉式单例。每个 Java 应用程序只有一个 Runtime 实例。

    java
    public class Runtime {
      private static final Runtime currentRuntime = new Runtime();
      public static Runtime getRuntime() { return currentRuntime; }
      // ...
    }
  • Spring Bean: 在 Spring 容器中,Bean 默认的作用域(Scope)就是 singleton。Spring 会缓存 Bean 的实例,下次请求直接返回,保证单例。

工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的创建型设计模式之一。

它的核心思想非常简单:不要直接使用 new 关键字来创建复杂的对象,而是把创建对象的“脏活累活”交给一个专门的“工厂类”去做。

这就好比:你想要一台手机。

  • 没有工厂模式: 你需要自己买零件(屏幕、电池、CPU),自己组装(new)。
  • 有工厂模式: 你找“富士康”(工厂),告诉它“我要一台 iPhone 15”,它直接给你一台成品。你不需要知道它是怎么造出来的。

在 Java 中,工厂模式通常分为三种形态,复杂度依次递增:

  1. 简单工厂模式 (Simple Factory) —— 虽然不是标准 GoF 模式,但最常用。

  2. 工厂方法模式 (Factory Method) —— 标准的工厂模式。

  3. 抽象工厂模式 (Abstract Factory) —— 用于创建产品族。

简单工厂模式 (Simple Factory)

简单工厂模式 (Simple Factory):

这是最直观的写法。核心是一个静态方法,根据传入的参数,决定创建哪种产品。

场景模拟

场景模拟:

我们要开发一个画图软件,需要创建不同的形状(圆形、矩形)。

代码实现

代码实现:

java
// 1. 定义一个接口 (产品规范)
interface Shape {
  void draw();
}

// 2. 具体产品:圆形
class Circle implements Shape {
  public void draw() { System.out.println("画一个圆形"); }
}

// 3. 具体产品:矩形
class Rectangle implements Shape {
  public void draw() { System.out.println("画一个矩形"); }
}

// 4. 【简单工厂类】
class ShapeFactory {
  // 静态方法,根据类型生产对象
  public static Shape getShape(String type) {
    if (type == null) return null;
    if (type.equalsIgnoreCase("CIRCLE")) {
      return new Circle();
    } else if (type.equalsIgnoreCase("RECTANGLE")) {
      return new Rectangle();
    }
    return null;
  }
}

// 5. 使用者
public class Main {
  public static void main(String[] args) {
    // 使用者完全不需要知道 Circle 类是怎么 new 出来的
    Shape s1 = ShapeFactory.getShape("CIRCLE");
    s1.draw();
  }
}

优缺点

优缺点:

  • 优点: 简单粗暴,调用者只需要传名字就能拿到对象,解耦了调用者和具体实现类。
  • 缺点: 违背了“开闭原则” (Open-Closed Principle)。 如果你想新增一个“三角形”,你必须去修改 ShapeFactory 的源代码(加 else if),这在大型系统中是有风险的。

工厂方法模式 (Factory Method)

工厂方法模式 (Factory Method):

为了解决简单工厂“修改代码”的问题,我们把工厂也抽象化。不再用一个大工厂统管所有产品,而是给每种产品配备一个专门的工厂。

代码实现

代码实现:

java
// 1. 定义工厂接口 (定义造东西的能力)
interface Factory {
  Shape getShape();
}

// 2. 圆形专属工厂
class CircleFactory implements Factory {
  @Override
  public Shape getShape() {
    return new Circle();
  }
}

// 3. 矩形专属工厂
class RectangleFactory implements Factory {
  @Override
  public Shape getShape() {
    return new Rectangle();
  }
}

// 4. 使用者
public class Main {
  public static void main(String[] args) {
    // 我想要圆形,就找圆形工厂
    Factory circleFactory = new CircleFactory();
    Shape s1 = circleFactory.getShape();
    s1.draw();
  }
}

优缺点

优缺点:

  • 优点: 符合开闭原则。如果你想增加“三角形”,只需要新建一个 Triangle 类和一个 TriangleFactory 类,完全不需要修改现有的代码
  • 缺点: 类爆炸。每增加一种产品,就要增加一个对应的工厂类,代码量倍增。

抽象工厂模式 (Abstract Factory)

抽象工厂模式 (Abstract Factory):

这通常用于大型项目。如果你的工厂不是只造一种东西,而是造一套东西(产品族)

  • 场景: 皮肤主题。

  • 暗黑主题工厂: 生产“黑色按钮” + “黑色文本框”。

  • 亮白主题工厂: 生产“白色按钮” + “白色文本框”。

  • 特点: 保证你用出的按钮和文本框风格是统一的,不会出现“黑色按钮”配“白色文本框”的尴尬。

(由于代码较长,这里只做概念介绍,核心是接口里有多个方法:createButton(), createTextField()).

JDK 和框架中的真实案例

JDK 和框架中的真实案例:

Java 源码中到处都是工厂模式的影子:

  1. java.util.Calendar.getInstance()

    • 这是一个典型的简单工厂。它根据你的时区和语言环境,返回一个具体的日历对象(可能是 GregorianCalendar 或其他)。
  2. java.text.NumberFormat.getInstance()

    • 同样是工厂模式,根据地区返回不同的数字格式化对象。
  3. Spring 框架 (BeanFactory / ApplicationContext)

    • 这是工厂模式的终极形态。
    • Spring 通过配置文件或注解 (@Component) 知道要创建什么对象。
    • 当你调用 context.getBean("userService") 时,Spring 这个超级大工厂就把对象造好给你。这也就是 IOC (控制反转) 的基础。

总结:什么时候用

总结:什么时候用:

模式核心特点适用场景
简单工厂一个静态方法,有很多 if-else产品少,逻辑简单,不想写太多类的时候(最常用)。
工厂方法只有接口,由子类决定实例化谁产品多,经常需要扩展新产品,且不想修改旧代码。
抽象工厂生产“全家桶” (一系列相关产品)需要保证多个产品组合使用时的兼容性(如换皮肤、换数据库适配)。

策略模式

策略模式 (Strategy Pattern) 是 Java 中最常用的行为型设计模式之一。

如果用一句话概括它的核心作用,那就是:它是消除复杂的 if-elseswitch-case 逻辑的终极杀手。

核心思想:算法的封装与替换

核心思想:算法的封装与替换:

在软件开发中,我们经常遇到这样的场景:做同一件事,有多种不同的算法(方法)。

  • 生活案例: 你要从北京去上海。
  • 策略 A:坐飞机(最快)。
  • 策略 B:坐高铁(性价比高)。
  • 策略 C:自己开车(自由)。
  • 结果: 都能到达目的地,只是具体的执行方式不同。

策略模式的核心定义:

定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可独立于使用它的客户而变化。

为什么要用策略模式

为什么要用策略模式(痛点分析):

假设我们要写一个电商系统的支付功能。

不使用设计模式的写法 (Bad Code):

java
public class PaymentService {
  public void pay(String type, double amount) {
    if ("AliPay".equals(type)) {
      System.out.println("调用支付宝接口支付:" + amount);
      // 几十行支付宝的复杂逻辑...
    } else if ("WeChat".equals(type)) {
      System.out.println("调用微信接口支付:" + amount);
      // 几十行微信的复杂逻辑...
    } else if ("UnionPay".equals(type)) {
      System.out.println("调用银联接口支付:" + amount);
      // 几十行银联的复杂逻辑...
    } else {
      throw new IllegalArgumentException("不支持的支付方式");
    }
  }
}

问题所在:

  1. 违背开闭原则: 如果现在要接入“京东支付”,你必须修改 PaymentService 类的源码,在里面加 else if。代码改动越多,Bug 风险越大。

  2. 代码臃肿: 一个方法里塞了几百行不同支付渠道的逻辑,难以维护。

策略模式的代码实现

策略模式的代码实现:

策略模式包含三个角色:

  1. Strategy (策略接口): 规定大家都要干什么(比如:支付)。

  2. ConcreteStrategy (具体策略): 具体的实现(比如:支付宝怎么付、微信怎么付)。

  3. Context (上下文/环境): 持有一个策略对象的引用,负责调用策略。

定义策略接口

定义策略接口:

java
// 统一的支付接口
public interface PaymentStrategy {
  void pay(double amount);
}

定义具体策略

定义具体策略:

java
// 策略 A:支付宝
public class AliPayStrategy implements PaymentStrategy {
  @Override
  public void pay(double amount) {
    System.out.println("==> 支付宝到账:" + amount);
  }
}

// 策略 B:微信支付
public class WeChatPayStrategy implements PaymentStrategy {
  @Override
  public void pay(double amount) {
    System.out.println("==> 微信支付成功:" + amount);
  }
}

// 策略 C:银联支付
public class UnionPayStrategy implements PaymentStrategy {
  @Override
  public void pay(double amount) {
    System.out.println("==> 银联卡扣款:" + amount);
  }
}

定义上下文 (Context)

定义上下文 (Context):

这个类是给客户端用的,它不关心具体怎么付,只管执行当前设定的策略。

java
public class PaymentContext {
  // 持有一个策略接口的引用
  private PaymentStrategy strategy;

  // 通过构造方法或者 Setter 注入具体的策略
  public PaymentContext(PaymentStrategy strategy) {
    this.strategy = strategy;
  }

  // 动态更换策略的方法
  public void setStrategy(PaymentStrategy strategy) {
    this.strategy = strategy;
  }

  // 执行策略
  public void executePay(double amount) {
    if (strategy == null) {
      System.out.println("未选择支付方式!");
      return;
    }
    strategy.pay(amount);
  }
}

客户端调用

客户端调用:

java
public class Main {
  public static void main(String[] args) {
    // 1. 用户选择了支付宝
    PaymentContext context = new PaymentContext(new AliPayStrategy());
    context.executePay(100.0);

    // 2. 运行时动态切换为微信 (比如用户在页面上点了“切换微信支付”)
    System.out.println("--- 切换支付方式 ---");
    context.setStrategy(new WeChatPayStrategy());
    context.executePay(200.0);
  }
}

策略模式的优缺点

优点

优点:

  1. 符合开闭原则 (OCP): 增加一种新的支付方式(如 ApplePay),只需要新建一个类实现接口即可,完全不需要修改原有代码

  2. 避免多重条件判断: 彻底消灭了难看的 if-elseswitch

  3. 算法可自由切换: 可以在程序运行时动态改变对象的行为。

缺点

缺点:

  1. 类数量膨胀: 每增加一个算法,就要增加一个类。

  2. 客户端必须知道所有的策略: 上面的 Main 方法中,客户端必须显式地 new AliPayStrategy()。这意味着客户端需要理解不同策略的区别。

JDK 中的经典应用:Comparator

JDK 中的经典应用:Comparator:

Java 源码中最著名的策略模式应用就是 java.util.Comparator 接口。

java
// 对一个列表进行排序
List<String> names = Arrays.asList("Jack", "Tom", "Alice");

// 策略 1:按字母顺序排序
Collections.sort(names, new Comparator<String>() {
  @Override
  public int compare(String o1, String o2) {
    return o1.compareTo(o2);
  }
});

// 策略 2:按字符串长度排序
Collections.sort(names, new Comparator<String>() {
  @Override
  public int compare(String o1, String o2) {
    return o1.length() - o2.length();
  }
});
  • Collections.sort() 方法就是 Context
  • Comparator 接口就是 Strategy
  • 我们传入的匿名内部类就是 ConcreteStrategy
  • sort 方法不需要改动,但排序的行为变了。

策略模式 vs 工厂模式

策略模式 vs 工厂模式:

这两个模式经常让人混淆,因为它们看起来很像。

维度工厂模式 (Factory)策略模式 (Strategy)
关注点创建对象执行行为 (算法)
目的帮你把对象造出来,你不关心过程帮你把任务做完,你不关心具体怎么做
客户端感知客户端通常只需传一个类型参数 (String)客户端通常需要手动创建具体的策略对象传入
一句话区别"给我一辆车""用这辆车带我去北京"

实战中的结合:

通常我们会混合使用

  • 工厂模式根据用户选择(String type)创建出具体的策略对象
  • 策略模式去执行具体的逻辑。

这样客户端连具体的策略类都不需要知道了,实现了彻底的解耦。

下一步建议:

策略模式关注的是“算法的替换”。在行为型模式中,还有一个非常重要的模式叫 “模板方法模式” (Template Method)

  • 策略模式是:你选 A 路还是 B 路? (完全替换)
  • 模板模式是:流程我都定好了(先开火,再倒油,再___,最后出锅),中间那个空你来填。 (部分替换)

您想了解一下这个被 Spring 源码(如 JdbcTemplate)大量使用的模板方法模式吗?