配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.singleton

系列导航:Java 25 设计模式学习指南

上一篇:原型 · 下一篇:适配器

所有文章入口使用相同的标题长度上限。这个规则体积很小,初始化之后不变化,也不需要按请求重新创建。单例可以为这样的对象提供统一实例,但“到处都能拿到它”也会使依赖变得不明显。

本章选择不可变站点策略作为最小案例。先观察唯一性,再理解它能保证到哪里,最后评估是否应该改为构造器注入。

从问题提炼设计意图

单例控制某个类型的实例数量,并提供获取该实例的入口。本例使用单元素枚举,在同一类加载器定义的类型范围内提供一个实例。

对象职责与协作关系

示例角色 职责
SitePolicy 单元素枚举,封装站点校验行为
INSTANCE 唯一枚举常量
maxTitleLength 不可变规则值
Demo 通过单例入口调用行为
classDiagram
    Demo ..> SitePolicy : INSTANCE
    SitePolicy : INSTANCE
    SitePolicy : accepts(title)

代码思路:变化应该落在哪个对象上

INSTANCE 是枚举常量,由 Java 的枚举初始化机制创建和发布。它避免手写双重检查锁定的复杂性,也无需通过公开构造器允许外部 new。这里展示标准类成员与方法,不依赖 JDK 25 的简化 main 写法。

maxTitleLength 是 final,accepts 只读取规则值和输入参数,没有写入共享可变状态。因此本例并发读取简单。标题长度使用 codePointCount,以 Unicode 码点计数,避免直接把 UTF-16 代码单元数量误称为字符数;它仍不等同于用户感知的字素簇数量。

测试让两个虚拟线程任务返回 SitePolicy.INSTANCE,再比较对象身份。它验证本运行环境中的共享行为;安全发布的依据来自语言初始化语义,不是一次并发测试碰巧没有出错。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.singleton,源码目录为 src/main/java/com/hanserwei/patterns/singleton/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

SitePolicy.java

package com.hanserwei.patterns.singleton;

/** 同一类加载器内共享的不可变站点策略. */
public enum SitePolicy {
  /** 唯一策略实例,由枚举初始化机制安全发布. */
  INSTANCE;

  /** 所有调用方共享的标题长度上限. */
  private final int maxTitleLength = 80;

  /** 返回按 Unicode 码点计算的标题长度上限. */
  public int maxTitleLength() {
    return maxTitleLength;
  }

  /** 检查标题非空白且不超过长度上限. */
  public boolean accepts(String title) {
    return title != null
        && !title.isBlank()
        && title.codePointCount(0, title.length()) <= maxTitleLength;
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.singleton;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    System.out.println(SitePolicy.INSTANCE.accepts("Java 25"));
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runSingleton
./gradlew test --tests 'com.hanserwei.patterns.singleton.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

true

用测试确认模式的行为

并发任务得到相同实例;80 个码点的标题被接受,81 个被拒绝,空白失败。测试同时关注身份与策略行为。

对应测试位于 src/test/java/com/hanserwei/patterns/singleton/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 生命周期覆盖整个应用、初始化后不可变的策略或元数据。
  • 确实需要统一身份的进程内协调对象,但仍需明确并发机制。
  • 遗留系统中必须提供统一访问入口,同时逐步梳理依赖。

适用边界与容易踩的坑

唯一实例不等于方法线程安全。给这个枚举新增普通 HashMap 后,并发写入仍然需要同步或并发容器。也不能用“只有一个实例”推出复合操作具备原子性。

单例不是集群唯一:多个 JVM、多个类加载器都可能拥有各自的实例。分布式任务选主、全局编号和跨进程锁不能由本模式直接解决。

全局入口会隐藏依赖,测试替换规则也更困难。若调用方依赖一个 Policy 接口,再通过构造器注入同一个实例,既可共享对象,又保留可替换性。依赖注入容器中的 singleton 作用域通常表示容器管理的共享生命周期,不等同于类型本身禁止创建第二个实例。

与相近模式比较

单例回答实例数量和访问方式;享元回答哪些细粒度对象的内在状态可以共享。享元工厂通常管理多个共享对象,不能因为“共享”就把它叫单例。

动手练习

抽取 TitlePolicy 接口,让一个服务通过构造器接收策略;保留枚举作为默认实现,再提供测试策略。比较直接访问 INSTANCE 和注入接口时,调用方测试需要知道多少全局配置。

系列导航:Java 25 设计模式学习指南

上一篇:原型 · 下一篇:适配器