享元模式(Flyweight Pattern)
一、模式概述
享元模式是一种结构型设计模式,它通过共享技术有效地支持大量细粒度的对象。享元模式将对象的状态分为内部状态(intrinsic state) 和外部状态(extrinsic state),内部状态可以共享,外部状态则由客户端维护。
核心思想:如果一个对象的很多状态是相同的,就把这些状态提取出来作为共享对象(享元),避免创建大量相同或相似的对象,从而节约内存。
二、核心概念
| 概念 | 说明 |
|---|---|
| 内部状态(Intrinsic State) | 存储在享元对象内部,不随环境变化而变化,可以共享 |
| 外部状态(Extrinsic State) | 不能共享的状态,随环境变化而变化,由客户端维护 |
| 享元工厂 | 管理享元池,负责创建和管理可共享的享元对象 |
| 享元池(Pool) | 存放享元对象的容器,通常是 HashMap |
内部状态 vs 外部状态
举个例子:文字编辑器中的字符对象
内部状态(可共享):字符形状 'A'、'B'、'C'(字体、大小、样式)
外部状态(不可共享):字符在屏幕上的位置 (x, y)、颜色、是否选中
字符 'A' 在文章中可能出现100次,但只需要创建1个享元对象,
100个不同的位置由外部状态维护。三、示例1:角色渲染系统(游戏场景)
问题场景
游戏中的角色有很多相同的外观(皮肤),但每个角色在地图上的位置、血量、装备不同。
java
import java.util.HashMap;
import java.util.Map;
// ========== 享元接口 ==========
/**
* 角色外观接口
*/
interface CharacterAppearance {
void display(int x, int y, int health); // 外部状态作为参数
}
// ========== 具体享元类 ==========
/**
* 战士外观(内部状态)
*/
class WarriorAppearance implements CharacterAppearance {
private final String skin = "战士皮肤";
private final String weapon = "大剑";
private final String animation = "挥剑动画";
@Override
public void display(int x, int y, int health) {
System.out.println(" [战士] 位置(" + x + "," + y + ") 血量:" + health
+ " 外观:" + skin + " 武器:" + weapon + " 动画:" + animation);
}
}
/**
* 法师外观(内部状态)
*/
class MageAppearance implements CharacterAppearance {
private final String skin = "法师皮肤";
private final String weapon = "法杖";
private final String animation = "施法动画";
@Override
public void display(int x, int y, int health) {
System.out.println(" [法师] 位置(" + x + "," + y + ") 血量:" + health
+ " 外观:" + skin + " 武器:" + weapon + " 动画:" + animation);
}
}
/**
* 弓箭手外观(内部状态)
*/
class ArcherAppearance implements CharacterAppearance {
private final String skin = "弓箭手皮肤";
private final String weapon = "长弓";
private final String animation = "射箭动画";
@Override
public void display(int x, int y, int health) {
System.out.println(" [弓箭手] 位置(" + x + "," + y + ") 血量:" + health
+ " 外观:" + skin + " 武器:" + weapon + " 动画:" + animation);
}
}
// ========== 享元工厂 ==========
/**
* 角色外观工厂
* 管理享元池,避免重复创建相同外观的对象
*/
class CharacterFactory {
private static final Map<String, CharacterAppearance> appearancePool = new HashMap<>();
public static CharacterAppearance getAppearance(String type) {
if (!appearancePool.containsKey(type)) {
switch (type) {
case "warrior":
appearancePool.put(type, new WarriorAppearance());
System.out.println("[享元工厂] 创建新的战士外观对象");
break;
case "mage":
appearancePool.put(type, new MageAppearance());
System.out.println("[享元工厂] 创建新的法师外观对象");
break;
case "archer":
appearancePool.put(type, new ArcherAppearance());
System.out.println("[享元工厂] 创建新的弓箭手外观对象");
break;
default:
throw new IllegalArgumentException("未知角色类型: " + type);
}
} else {
System.out.println("[享元工厂] 复用已有的 " + type + " 外观对象");
}
return appearancePool.get(type);
}
public static int getPoolSize() {
return appearancePool.size();
}
}
// ========== 角色类(包含外部状态) ==========
/**
* 游戏角色
* 外部状态(x, y, health)由角色自身维护
*/
class GameCharacter {
private String name;
private int x, y; // 外部状态:位置
private int health; // 外部状态:血量
private CharacterAppearance appearance; // 共享的内部状态(享元)
public GameCharacter(String name, String type, int x, int y, int health) {
this.name = name;
this.x = x;
this.y = y;
this.health = health;
this.appearance = CharacterFactory.getAppearance(type);
}
public void render() {
System.out.print("角色[" + name + "] ");
appearance.display(x, y, health);
}
public void moveTo(int newX, int newY) {
this.x = newX;
this.y = newY;
System.out.println(" 角色 " + name + " 移动到 (" + newX + "," + newY + ")");
}
}
// ========== 客户端代码 ==========
public class GameDemo {
public static void main(String[] args) {
System.out.println("===== 创建角色 =====");
// 创建大量角色,但享元对象只有3个
GameCharacter warrior1 = new GameCharacter("战士1号", "warrior", 10, 20, 100);
GameCharacter warrior2 = new GameCharacter("战士2号", "warrior", 30, 40, 80);
GameCharacter warrior3 = new GameCharacter("战士3号", "warrior", 50, 60, 120);
GameCharacter mage1 = new GameCharacter("法师1号", "mage", 15, 25, 60);
GameCharacter mage2 = new GameCharacter("法师2号", "mage", 35, 45, 55);
GameCharacter archer1 = new GameCharacter("弓箭手1号", "archer", 20, 30, 70);
GameCharacter archer2 = new GameCharacter("弓箭手2号", "archer", 40, 50, 75);
System.out.println("\n===== 渲染角色 =====");
warrior1.render();
warrior2.render();
warrior3.render();
mage1.render();
mage2.render();
archer1.render();
archer2.render();
System.out.println("\n===== 角色数量统计 =====");
System.out.println("角色总数: 7");
System.out.println("享元对象数(外观): " + CharacterFactory.getPoolSize());
System.out.println("节省了 " + (7 - CharacterFactory.getPoolSize()) + " 个对象的内存!");
System.out.println("\n===== 角色移动 =====");
warrior1.moveTo(100, 200);
warrior1.render();
}
}运行结果
===== 创建角色 =====
[享元工厂] 创建新的战士外观对象
[享元工厂] 复用已有的 warrior 外观对象
[享元工厂] 复用已有的 warrior 外观对象
[享元工厂] 创建新的法师外观对象
[享元工厂] 复用已有的 mage 外观对象
[享元工厂] 创建新的弓箭手外观对象
[享元工厂] 复用已有的 archer 外观对象
===== 渲染角色 =====
角色[战士1号] [战士] 位置(10,20) 血量:100 外观:战士皮肤 武器:大剑 动画:挥剑动画
角色[战士2号] [战士] 位置(30,40) 血量:80 外观:战士皮肤 武器:大剑 动画:挥剑动画
...
===== 角色数量统计 =====
角色总数: 7
享元对象数(外观): 3
节省了 4 个对象的内存!四、示例2:数据库连接池
java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
// ========== 享元对象:数据库连接 ==========
/**
* 数据库连接(享元对象)
* 连接配置(URL、用户名)是内部状态,可复用
* 事务隔离级别、超时时间等是外部状态
*/
class DatabaseConnection {
private final String url; // 内部状态
private final String username; // 内部状态
private boolean inUse = false; // 连接使用状态
public DatabaseConnection(String url, String username) {
this.url = url;
this.username = username;
System.out.println("[连接池] 创建新连接 -> " + url);
}
/**
* 使用连接(外部状态通过参数传入)
*/
public void execute(String sql, int timeout) {
System.out.println(" 执行SQL: " + sql + " (超时:" + timeout + "ms)");
}
public boolean isInUse() {
return inUse;
}
public void setInUse(boolean inUse) {
this.inUse = inUse;
}
public void close() {
this.inUse = false;
System.out.println(" 连接已归还 -> " + url);
}
}
// ========== 享元工厂:连接池 ==========
/**
* 数据库连接池(享元工厂)
* 管理一组可复用的数据库连接
*/
class ConnectionPool {
private final BlockingQueue<DatabaseConnection> pool;
private final String url;
private final String username;
private final int maxSize;
private int currentSize = 0;
public ConnectionPool(String url, String username, int maxSize) {
this.url = url;
this.username = username;
this.maxSize = maxSize;
this.pool = new LinkedBlockingQueue<>(maxSize);
System.out.println("[连接池] 初始化连接池,最大连接数: " + maxSize);
}
/**
* 获取连接(外部状态通过参数传入)
*/
public DatabaseConnection getConnection(int timeout) throws InterruptedException {
// 尝试从池中获取空闲连接
DatabaseConnection conn = pool.poll(timeout, TimeUnit.MILLISECONDS);
if (conn != null) {
conn.setInUse(true);
System.out.println("[连接池] 复用已有连接 (当前使用: " + currentSize + "/" + maxSize + ")");
return conn;
}
// 池中没有空闲连接,且未达到最大连接数,创建新连接
if (currentSize < maxSize) {
conn = new DatabaseConnection(url, username);
conn.setInUse(true);
currentSize++;
return conn;
}
// 已达到最大连接数
System.out.println("[连接池] 连接池已满,等待空闲连接...");
return null;
}
/**
* 归还连接
*/
public void releaseConnection(DatabaseConnection conn) {
if (conn != null) {
conn.close();
pool.offer(conn);
}
}
public int getPoolSize() {
return currentSize;
}
}
// ========== 客户端代码 ==========
public class ConnectionPoolDemo {
public static void main(String[] args) throws InterruptedException {
ConnectionPool pool = new ConnectionPool(
"jdbc:mysql://localhost:3306/mydb", "root", 5
);
System.out.println("\n===== 获取连接 =====");
DatabaseConnection conn1 = pool.getConnection(3000);
DatabaseConnection conn2 = pool.getConnection(3000);
DatabaseConnection conn3 = pool.getConnection(3000);
System.out.println("\n===== 使用连接 =====");
conn1.execute("SELECT * FROM users", 5000);
conn2.execute("INSERT INTO orders VALUES(...)", 5000);
conn3.execute("UPDATE products SET stock=100", 5000);
System.out.println("\n===== 归还连接 =====");
pool.releaseConnection(conn1);
pool.releaseConnection(conn2);
System.out.println("\n===== 重新获取 =====");
DatabaseConnection conn4 = pool.getConnection(3000); // 复用conn1
conn4.execute("DELETE FROM temp_data", 5000);
System.out.println("\n===== 连接池状态 =====");
System.out.println("连接池大小: " + pool.getPoolSize());
}
}五、String 源码中的享元模式
Java 的 String 类型就使用了享元模式:
java
public class StringFlyweightDemo {
public static void main(String[] args) {
// 字符串字面量使用字符串常量池(享元池)
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println("s1 == s2: " + (s1 == s2)); // true(同一个对象)
System.out.println("s1 == s3: " + (s1 == s3)); // false(不同对象)
// 使用 intern() 方法将字符串放入常量池
String s4 = s3.intern();
System.out.println("s1 == s4: " + (s1 == s4)); // true
// Integer 缓存池 -128~127
Integer a = 127;
Integer b = 127;
Integer c = new Integer(127);
System.out.println("a == b: " + (a == b)); // true(缓存池复用)
System.out.println("a == c: " + (a == c)); // false(新对象)
Integer d = 128;
Integer e = 128;
System.out.println("d == e: " + (d == e)); // false(超出缓存范围)
}
}六、UML 类图
┌────────────────────┐ ┌──────────────────┐
│ FlyweightFactory │──creates──▶│ Flyweight (接口) │
│ - pool: Map │ │ + operation() │
│ + getFlyweight() │ └───────┬──────────┘
└────────────────────┘ │
┌──────┴──────┐
┌──────┴───┐ ┌──────┴───┐
│Concrete │ │Concrete │
│FlyweightA│ │FlyweightB│
└──────────┘ └──────────┘
Client ──▶ FlyweightFactory ──▶ Flyweight(共享)
│
│ 管理
▼
┌─────────┐
│ Flyweight│ ← 享元池(HashMap)
│ Pool │
└─────────┘七、优缺点分析
优点
- 大幅减少内存占用:相同对象只创建一次,节省大量内存
- 提高性能:减少了对象创建和垃圾回收的开销
- 内部状态共享:核心数据只在享元对象中存储一份
缺点
- 增加复杂度:需要分离内部状态和外部状态,增加了设计的复杂度
- 线程安全问题:享元对象被多个客户端共享,需要注意线程安全
- 外部状态开销:外部状态需要每次传入,可能增加参数传递的开销
八、使用场景
- 系统存在大量相似对象:对象的大部分状态可以外部化
- 缓冲池:线程池、连接池、对象池等
- 文本编辑器:大量相同字符的渲染
- 游戏开发:大量相同外观的游戏对象
- Java 基础类型缓存:Integer、Long 等包装类的缓存池
九、最佳实践
- 合理划分内部状态和外部状态:这是享元模式成功的关键
- 注意线程安全:享元对象被共享使用,需要考虑并发访问
- 不要过度使用:如果对象数量不多,享元模式反而增加了复杂度
- 结合对象池使用:享元模式常与对象池技术结合使用
总结:享元模式的核心是"共享细粒度对象"。当系统中存在大量相似对象时,通过提取可共享的内部状态,可以显著减少内存占用和提高性能。在使用时,关键是正确划分内部状态(可共享)和外部状态(不可共享)。