Java 23 种设计模式:从踩坑到精通 | 番外:享元 vs 对象池 —— 不可变共享 vs 可变复用

摘要:享元模式和对象池模式都能减少对象创建、节约内存,但享元的核心是不可变对象的共享——相同内部状态的客户端拿到的是同一个只读实例;对象池的核心是可变对象的复用——客户端借出、用完归还,对象状态随时变化。本文从字符共享和数据库连接池两个场景出发,结合代码对比、UML 分析和 Integer.valueOf()、HikariCP 等 JDK 源码,帮你彻底分清这对“池化型”模式。

🗺️ 本文阅读地图(3 分钟速览)

  • 为什么字符共享是享元,连接池是对象池?
  • UML 对比:不可变共享 vs 借还复用
  • 手写享元工厂 vs 对象池,看代码感受差异
  • JDK Integer.valueOf()(享元)vs 数据库连接池(对象池)源码分析
  • 面试必问:“享元和对象池怎么区分?连接池是享元吗?”

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 当前:番外 · 享元 vs 对象池
🔗 返回系列总目录



1. 从“字符共享”和“连接池”两个场景说起

1.1 场景一:文本编辑器字符共享(享元模式)

一篇十万字的文章,如果每个字符都 new 一个 Character 对象,内存会迅速撑爆。享元模式的思路:将字符对象的状态分为内部状态(字符、字体、字号——不可变、可共享)和外部状态(行列位置——由客户端管理)。享元工厂维护一个池,每种内部状态只创建一个实例。

// 享元工厂:每种内部状态只创建一个不可变实例
public class CharacterFactory {
    private static final Map<String, TextCharacter> pool = new ConcurrentHashMap<>();

    public static TextCharacter getCharacter(char symbol, String font, int size) {
        String key = symbol + "_" + font + "_" + size;
        return pool.computeIfAbsent(key, k -> new ConcreteCharacter(symbol, font, size));
    }
}

// 客户端:11 个字符只创建了 8 个享元实例(l 和 o 重复)
TextCharacter c1 = CharacterFactory.getCharacter('l', "Arial", 12);  // 新创建
TextCharacter c2 = CharacterFactory.getCharacter('l', "Arial", 12);  // 复用!和 c1 是**同一个对象**
System.out.println(c1 == c2);  // true

💬 白话:享元池中的对象是“只读”的——创建后就不能修改。客户端只是拿来用,用完不用归还。多个客户端拿到的是同一个实例

1.2 场景二:数据库连接池(对象池模式)

数据库连接对象是有状态、可变的。一个连接可能处于“空闲”、“已借用”、“已关闭”等状态。对象池的运作方式:客户端从池中借一个空闲连接,用完后归还,池负责管理连接的生命周期。

// HikariCP 对象池(简化示例)
HikariDataSource dataSource = new HikariDataSource(config);

// 客户端1:借一个连接
Connection conn1 = dataSource.getConnection();   // 从池中借出
conn1.setAutoCommit(false);                      // 修改连接状态
// ... 执行业务操作 ...
conn1.close();                                   // 归还给池

// 客户端2:借一个连接(可能是同一个物理连接,但状态已重置)
Connection conn2 = dataSource.getConnection();   // 从池中借出
System.out.println(conn1 == conn2);              // false,不同对象(或状态已重置的同一对象)

💬 白话:对象池中的对象是“借出-归还”模式。借出时可能被修改状态,归还时池会重置状态(如回滚事务、清理上下文)。多个客户端拿到的是不同的实例(或状态已被重置的实例)。

1.3 关键差异已浮现

  • 享元:对象创建后就不可变,多个客户端共享同一个实例,不用归还;
  • 对象池:对象可变,客户端借出→使用→归还,池负责状态管理。

2. UML 对比

在这里插入图片描述


3. 三大核心区别

对比维度 享元模式 对象池模式
对象可变性 不可变(创建后不能修改) 可变(借出后状态可能被修改)
共享方式 相同 key 返回同一个实例== 为 true) 借出不同的实例,用完归还
生命周期管理 常驻内存,一般不销毁 借出→使用→归还→可销毁(空闲超时)
典型应用 Integer.valueOf(-128~127)、字符串常量池 数据库连接池(HikariCP)、线程池
核心特征 只读共享,外部状态由客户端管理 复用归还,状态由池管理

4. JDK 源码深度对比

4.1 Integer.valueOf()(享元模式)

// Integer 对 -128~127 范围的整数进行了缓存
// 范围内的整数共享同一个不可变 Integer 实例
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b);  // true,同一个对象!

Integer c = Integer.valueOf(200);
Integer d = Integer.valueOf(200);
System.out.println(c == d);  // false,超出缓存范围,新建对象

为什么是享元?

  • Integer 对象不可变final class,没有 setter);
  • 相同值的整数返回同一个实例(在缓存范围内);
  • 客户端只是“拿来用”,用完不需要归还;
  • 内部状态(整数值)是可共享的,外部状态(无)由客户端管理。

4.2 HikariCP 数据库连接池(对象池模式)

// HikariCP 是典型的对象池模式
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setMaximumPoolSize(10);         // 最大连接数
config.setIdleTimeout(600000);         // 空闲超时 10 分钟
HikariDataSource dataSource = new HikariDataSource(config);

// 借出连接
Connection conn = dataSource.getConnection();
// 使用连接(可能修改状态:设置事务隔离级别、超时等)
conn.setAutoCommit(false);
// 归还连接(由池重置状态)
conn.close();

为什么是对象池?

  • Connection 对象可变(状态包括:是否自动提交、事务隔离级别、超时等);
  • 每次 getConnection() 返回的是不同实例状态已被重置的实例
  • 客户端必须归还(调用 close() 归还给池,而非真正关闭);
  • 池管理对象生命周期(最大连接数、空闲超时、连接验证)。

5. 什么时候不该用?

滥用场景 应该用什么 原因
对象状态需要频繁修改(如连接) 对象池 享元要求对象不可变,状态可变会导致数据污染
对象创建成本低,数量不大 直接 new 池化增加代码复杂度,收益不大
对象不可变且只有少量种类 享元 对象池管理复杂度高,不需要借还逻辑
线程复用 线程池(对象池变体) 线程是有状态、可变的,不是不可变共享

6. 面试必问 + 追问连环炮

基础必问

  • 享元和对象池有什么区别? → 享元共享不可变对象(相同 key 返回同一实例),对象池复用可变对象(借出-归还模式)。
  • 数据库连接池是享元模式吗?不是。连接池是对象池模式。连接对象有状态、可变,需要借出和归还。

面试官追问

  • Integer.valueOf() 是享元模式,那 String.intern() 呢?”
    👉 也是享元模式。intern() 将字符串放入常量池,相同内容的字符串共享同一个不可变实例。
  • “对象池中的对象归还时为什么要重置状态?”
    👉 防止上一个使用者的状态污染下一个使用者。例如归还连接时需要回滚未提交的事务、清除临时表、重置隔离级别。
  • “如果享元对象的内部状态被意外修改了会怎样?”
    👉 数据污染事故。A 客户端修改了共享对象,B 客户端拿到的是脏数据。因此享元对象必须设计为不可变(final 字段 + 无 setter),这是享元模式最致命的踩坑点。
  • “线程池是享元还是对象池?”
    👉 对象池。线程是有状态的(运行/阻塞/等待),借出后状态不断变化,用完归还回池中。ThreadLocal 可以用来给每个线程分配独立的上下文,但线程本身是对象池管理的。

🎉 恭喜:如果你能立刻说出 Integer.valueOf() 是享元(不可变共享)、数据库连接池是对象池(可变复用),并理解“享元对象必须不可变”的原因,你已经掌握了这对“池化型”模式的核心区分点。

💡 延伸阅读:本文是享元模式与对象池模式的深度对比。如果你想先了解享元模式的完整原理、UML、代码实现和面试题,可以先阅读正篇——享元模式:内存吃不消?试试共享细粒度对象,再回来看这篇对比,效果更佳。

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐