享元模式 (Flyweight)
📖 通俗理解
想象一个围棋游戏:
- 棋盘上有 361 个交叉点,最多可以放 361 个棋子
- 每个棋子都创建一个对象?那太浪费内存了!
- 其实棋子只有两种:黑棋和白棋
享元模式的思路:只创建两个棋子对象(黑/白),所有位置共享使用这两个对象。棋子的颜色是内部状态(不变),位置是外部状态(变化的,使用时传入)。
享元模式就是:共享对象,减少内存使用。
🎯 解决什么问题?
问题:系统中存在大量相似对象,占用大量内存。
例子:
- 游戏中的子弹(成千上万颗,但类型只有几种)
- 文字编辑器中的字符(每个字符都创建对象?)
- 地图中的树木(同一种树共享模型)
解决:把对象的状态分为内部状态(共享)和外部状态(不共享),共享相同内部状态的对象。
🌰 生活中的例子
- 共享单车:车是共享的,骑车的人和目的地是外部状态
- 图书馆:书是共享的,借阅人是外部状态
- 线程池:线程是共享的,任务是外部状态
- 字符串常量池:相同的字符串共享同一个对象
💻 Java 代码实现
场景:围棋棋子
第一步:定义享元接口
/**
* 享元接口:棋子
*/
public interface ChessPiece {
/**
* 落子
* @param x 外部状态:x 坐标
* @param y 外部状态:y 坐标
*/
void place(int x, int y);
}
第二步:实现具体享元
/**
* 具体享元:棋子实现
* 内部状态:颜色(不变)
*/
public class ConcreteChessPiece implements ChessPiece {
// 内部状态:颜色,创建后不变
private final String color;
public ConcreteChessPiece(String color) {
this.color = color;
System.out.println("创建了一个" + color + "棋子");
}
@Override
public void place(int x, int y) {
// 外部状态:位置,由调用者传入
System.out.println(color + "棋子落在位置 (" + x + ", " + y + ")");
}
public String getColor() {
return color;
}
}
第三步:创建享元工厂
import java.util.HashMap;
import java.util.Map;
/**
* 享元工厂:管理棋子对象池
*/
public class ChessPieceFactory {
// 享元池
private static final Map<String, ChessPiece> pieces = new HashMap<>();
/**
* 获取棋子(如果池中有就复用,没有就创建)
*/
public static ChessPiece getChessPiece(String color) {
ChessPiece piece = pieces.get(color);
if (piece == null) {
piece = new ConcreteChessPiece(color);
pieces.put(color, piece);
}
return piece;
}
/**
* 获取享元池大小
*/
public static int getPoolSize() {
return pieces.size();
}
}
第四步:使用享元
public class ChessGame {
public static void main(String[] args) {
System.out.println("=== 开始下棋 ===\n");
// 模拟下棋过程
// 黑棋先手
ChessPiece black1 = ChessPieceFactory.getChessPiece("黑");
black1.place(3, 3);
// 白棋
ChessPiece white1 = ChessPieceFactory.getChessPiece("白");
white1.place(4, 4);
// 黑棋(复用之前的黑棋对象)
ChessPiece black2 = ChessPieceFactory.getChessPiece("黑");
black2.place(3, 4);
// 白棋(复用之前的白棋对象)
ChessPiece white2 = ChessPieceFactory.getChessPiece("白");
white2.place(4, 3);
// 继续下...
ChessPieceFactory.getChessPiece("黑").place(5, 5);
ChessPieceFactory.getChessPiece("白").place(5, 4);
ChessPieceFactory.getChessPiece("黑").place(6, 6);
System.out.println("\n=== 游戏结束 ===");
System.out.println("共下了 7 步棋");
System.out.println("实际创建的棋子对象数量: " + ChessPieceFactory.getPoolSize());
// 验证是同一个对象
System.out.println("\nblack1 == black2 ? " + (black1 == black2));
System.out.println("white1 == white2 ? " + (white1 == white2));
}
}
输出:
=== 开始下棋 ===
创建了一个黑棋子
黑棋子落在位置 (3, 3)
创建了一个白棋子
白棋子落在位置 (4, 4)
黑棋子落在位置 (3, 4)
白棋子落在位置 (4, 3)
黑棋子落在位置 (5, 5)
白棋子落在位置 (5, 4)
黑棋子落在位置 (6, 6)
=== 游戏结束 ===
共下了 7 步棋
实际创建的棋子对象数量: 2
black1 == black2 ? true
white1 == white2 ? true
虽然下了 7 步棋,但只创建了 2 个对象!
🔥 实战案例:字符串常量池
Java 中的 String 就使用了享元模式:
public class StringPoolDemo {
public static void main(String[] args) {
// 字面量创建的字符串会放入常量池
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = s3.intern(); // 从常量池获取
System.out.println("s1 == s2 : " + (s1 == s2)); // true,共享
System.out.println("s1 == s3 : " + (s1 == s3)); // false,new 创建新对象
System.out.println("s1 == s4 : " + (s1 == s4)); // true,intern() 返回池中对象
}
}
🔥 实战案例:数据库连接池
import java.util.ArrayList;
import java.util.List;
/**
* 数据库连接(享元对象)
*/
public class Connection {
private String id;
private boolean inUse;
public Connection(String id) {
this.id = id;
this.inUse = false;
System.out.println("创建数据库连接: " + id);
}
public void execute(String sql) {
System.out.println(id + " 执行SQL: " + sql);
}
// getter setter...
}
/**
* 连接池(享元工厂)
*/
public class ConnectionPool {
private static final int MAX_SIZE = 5;
private List<Connection> pool = new ArrayList<>();
public ConnectionPool() {
// 预先创建连接
for (int i = 0; i < MAX_SIZE; i++) {
pool.add(new Connection("Conn-" + (i + 1)));
}
}
/**
* 获取连接
*/
public synchronized Connection getConnection() {
for (Connection conn : pool) {
if (!conn.isInUse()) {
conn.setInUse(true);
System.out.println("获取连接: " + conn.getId());
return conn;
}
}
System.out.println("没有可用连接!");
return null;
}
/**
* 归还连接
*/
public synchronized void releaseConnection(Connection conn) {
conn.setInUse(false);
System.out.println("归还连接: " + conn.getId());
}
public int getPoolSize() {
return pool.size();
}
}
使用方式:
public class DatabaseDemo {
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool();
System.out.println("\n=== 开始使用连接 ===");
// 获取连接执行操作
Connection conn1 = pool.getConnection();
conn1.execute("SELECT * FROM users");
Connection conn2 = pool.getConnection();
conn2.execute("INSERT INTO logs ...");
// 归还连接
pool.releaseConnection(conn1);
// 再次获取(复用 conn1)
Connection conn3 = pool.getConnection();
conn3.execute("UPDATE users SET ...");
System.out.println("\n连接池大小: " + pool.getPoolSize());
}
}
🔥 实战案例:Integer 缓存池
Java 的 Integer 类也使用了享元模式:
public class IntegerCacheDemo {
public static void main(String[] args) {
// -128 到 127 使用缓存
Integer a = 100;
Integer b = 100;
System.out.println("a == b : " + (a == b)); // true
// 超出缓存范围
Integer c = 200;
Integer d = 200;
System.out.println("c == d : " + (c == d)); // false
// valueOf 使用缓存
Integer e = Integer.valueOf(100);
Integer f = Integer.valueOf(100);
System.out.println("e == f : " + (e == f)); // true
}
}
📊 类图结构
┌─────────────┐ ┌────────────────────┐
│ Client │ -------> │ FlyweightFactory │
└─────────────┘ │ - flyweights: Map │
│ + getFlyweight() │
└─────────┬──────────┘
│ 创建/返回
▼
┌────────────────────┐
│ <<interface>> │
│ Flyweight │
│ + operation(state) │
└─────────▲──────────┘
│
┌──────────────┴──────────────┐
│ │
┌────────┴─────────┐ ┌──────────┴────────┐
│ ConcreteFlyweight │ │ UnsharedFlyweight │
│ - intrinsicState │ │ 不共享的享元 │
└──────────────────┘ └───────────────────┘
⚠️ 内部状态 vs 外部状态
| 状态类型 | 说明 | 例子(棋子) | 例子(字符) |
|---|---|---|---|
| 内部状态 | 存储在享元内部,不随环境变化 | 颜色 | 字符内容 |
| 外部状态 | 随环境变化,使用时传入 | 位置(x, y) | 字体、大小 |
✅ 适用场景
- 大量相似对象:系统中存在大量相似对象
- 对象状态可分离:状态可分为内部和外部
- 对象可共享:对象是不可变的或可以安全共享
⚠️ 优缺点
优点:
- 大大减少对象数量,节省内存
- 外部状态相对独立
缺点:
- 增加系统复杂度
- 需要分离内部和外部状态
小结
享元模式的核心:共享对象,减少内存使用。
关键点:
- 区分内部状态(共享)和外部状态(不共享)
- 用工厂管理享元池
- Java 中的 String、Integer 都使用了享元模式
👉 下一篇:代理模式
