享元模式(Flyweight)大白话讲解
一句话概括
就像字母积木:有限的字母积木可以拼出无数单词,不用为每个单词都造新积木

现实生活比喻
场景1:字母积木
- 有限字母:26个字母积木
- 无限组合:用这些字母可以拼出所有英文单词
- 节省材料:不需要为每个单词都制造一套新字母
场景2:围棋棋子
- 棋子类型:只有黑棋和白棋两种
- 棋盘位置:361个位置可以放棋子
- 节省资源:不需要为每个位置都创建新棋子对象
完整代码示例
场景1:文本编辑器中的字符处理
java
/**
* 享元模式 - 文本编辑器字符处理
*/
public class Main {
public static void main(String[] args) {
System.out.println("=== 文本编辑器字符处理 ===");
// 创建字符工厂
CharacterFactory factory = new CharacterFactory();
// 编辑文档
String document = "Hello World! Hello Design Patterns!";
System.out.println("文档内容: " + document);
System.out.println("文档长度: " + document.length() + " 个字符");
// 处理每个字符
List<TextCharacter> characters = new ArrayList<>();
for (int i = 0; i < document.length(); i++) {
char c = document.charAt(i);
TextCharacter character = factory.getCharacter(c);
characters.add(character);
}
// 显示字符信息
System.out.println("\n=== 字符使用统计 ===");
System.out.println("实际创建的字符对象数量: " + factory.getCharacterCount());
System.out.println("文档中字符总数: " + characters.size());
System.out.println("节省了 " + (characters.size() - factory.getCharacterCount()) + " 个对象");
// 渲染文档
System.out.println("\n=== 渲染文档 ===");
for (int i = 0; i < characters.size(); i++) {
characters.get(i).render(i, 0); // 位置作为外部状态
}
}
}
/**
* 享元接口 - 字符
*/
interface TextCharacter {
void render(int positionX, int positionY); // 位置是外部状态
char getSymbol(); // 获取字符符号
}
/**
* 具体享元 - 具体字符
*/
class Character implements TextCharacter {
private final char symbol; // 内部状态 - 不变的部分
private final String font; // 内部状态 - 字体
private final int size; // 内部状态 - 字号
public Character(char symbol) {
this.symbol = symbol;
this.font = "Arial"; // 假设所有字符使用相同字体
this.size = 12; // 假设所有字符使用相同字号
}
@Override
public void render(int positionX, int positionY) {
System.out.println("字符 '" + symbol + "' 在位置 (" + positionX + ", " + positionY +
") 字体: " + font + " 字号: " + size);
}
@Override
public char getSymbol() {
return symbol;
}
}
/**
* 享元工厂 - 字符工厂
*/
class CharacterFactory {
private Map<Character, TextCharacter> characters = new HashMap<>();
// 获取字符对象 - 核心方法
public TextCharacter getCharacter(char symbol) {
// 如果字符已经存在,直接返回
if (characters.containsKey(symbol)) {
return characters.get(symbol);
}
// 否则创建新字符并缓存
TextCharacter character = new Character(symbol);
characters.put(symbol, character);
System.out.println("创建新字符对象: '" + symbol + "'");
return character;
}
// 获取已创建的字符数量
public int getCharacterCount() {
return characters.size();
}
}
运行结果
=== 文本编辑器字符处理 ===
文档内容: Hello World! Hello Design Patterns!
文档长度: 37 个字符
创建新字符对象: 'H'
创建新字符对象: 'e'
创建新字符对象: 'l'
创建新字符对象: 'o'
创建新字符对象: ' '
创建新字符对象: 'W'
创建新字符对象: 'r'
创建新字符对象: 'd'
创建新字符对象: '!'
创建新字符对象: 'D'
创建新字符对象: 's'
创建新字符对象: 'i'
创建新字符对象: 'g'
创建新字符对象: 'n'
创建新字符对象: 'P'
创建新字符对象: 'a'
创建新字符对象: 't'
=== 字符使用统计 ===
实际创建的字符对象数量: 17
文档中字符总数: 37
节省了 20 个对象
=== 渲染文档 ===
字符 'H' 在位置 (0, 0) 字体: Arial 字号: 12
字符 'e' 在位置 (1, 0) 字体: Arial 字号: 12
字符 'l' 在位置 (2, 0) 字体: Arial 字号: 12
字符 'l' 在位置 (3, 0) 字体: Arial 字号: 12
字符 'o' 在位置 (4, 0) 字体: Arial 字号: 12
字符 ' ' 在位置 (5, 0) 字体: Arial 字号: 12
字符 'W' 在位置 (6, 0) 字体: Arial 字号: 12
字符 'o' 在位置 (7, 0) 字体: Arial 字号: 12
字符 'r' 在位置 (8, 0) 字体: Arial 字号: 12
字符 'l' 在位置 (9, 0) 字体: Arial 字号: 12
字符 'd' 在位置 (10, 0) 字体: Arial 字号: 12
字符 '!' 在位置 (11, 0) 字体: Arial 字号: 12
...
场景2:围棋游戏
java
/**
* 享元模式 - 围棋游戏
*/
public class GoGame {
public static void main(String[] args) {
System.out.println("=== 围棋游戏 ===");
ChessFactory factory = new ChessFactory();
GoBoard board = new GoBoard();
// 下棋
board.placeChess(factory.getChess("黑棋"), 3, 4);
board.placeChess(factory.getChess("白棋"), 4, 4);
board.placeChess(factory.getChess("黑棋"), 5, 5);
board.placeChess(factory.getChess("白棋"), 4, 5);
board.placeChess(factory.getChess("黑棋"), 3, 5);
// 显示棋盘
board.display();
// 统计信息
System.out.println("\n=== 棋子使用统计 ===");
System.out.println("棋盘棋子数量: " + board.getChessCount());
System.out.println("实际创建的棋子对象: " + factory.getChessCount());
System.out.println("节省了 " + (board.getChessCount() - factory.getChessCount()) + " 个对象");
}
}
/**
* 享元接口 - 棋子
*/
interface Chess {
String getColor();
void display(int x, int y);
}
/**
* 具体享元 - 具体棋子
*/
class GoChess implements Chess {
private final String color; // 内部状态 - 颜色
public GoChess(String color) {
this.color = color;
}
@Override
public String getColor() {
return color;
}
@Override
public void display(int x, int y) {
System.out.println(color + " 落在位置 (" + x + ", " + y + ")");
}
}
/**
* 享元工厂 - 棋子工厂
*/
class ChessFactory {
private Map<String, Chess> chessMap = new HashMap<>();
public Chess getChess(String color) {
if (chessMap.containsKey(color)) {
return chessMap.get(color);
}
Chess chess = new GoChess(color);
chessMap.put(color, chess);
System.out.println("创建新棋子: " + color);
return chess;
}
public int getChessCount() {
return chessMap.size();
}
}
/**
* 棋盘 - 管理外部状态(位置)
*/
class GoBoard {
private List<ChessPosition> positions = new ArrayList<>();
public void placeChess(Chess chess, int x, int y) {
positions.add(new ChessPosition(chess, x, y));
}
public void display() {
System.out.println("\n=== 棋盘状态 ===");
for (ChessPosition position : positions) {
position.display();
}
}
public int getChessCount() {
return positions.size();
}
}
/**
* 棋子位置 - 外部状态
*/
class ChessPosition {
private Chess chess; // 享元对象
private int x; // 外部状态 - X坐标
private int y; // 外部状态 - Y坐标
public ChessPosition(Chess chess, int x, int y) {
this.chess = chess;
this.x = x;
this.y = y;
}
public void display() {
chess.display(x, y);
}
}
运行结果
=== 围棋游戏 ===
创建新棋子: 黑棋
创建新棋子: 白棋
=== 棋盘状态 ===
黑棋 落在位置 (3, 4)
白棋 落在位置 (4, 4)
黑棋 落在位置 (5, 5)
白棋 落在位置 (4, 5)
黑棋 落在位置 (3, 5)
=== 棋子使用统计 ===
棋盘棋子数量: 5
实际创建的棋子对象: 2
节省了 3 个对象
享元模式的核心概念
内部状态 vs 外部状态
内部状态(Intrinsic State)
- 不变的、可共享的部分
- 存储在享元对象内部
- 例子:字符的符号、字体、字号;棋子的颜色
外部状态(Extrinsic State)
- 变化的、不可共享的部分
- 由客户端存储和传递
- 例子:字符的位置;棋子的坐标
核心结构
Client(客户端)
↓ 使用
FlyweightFactory(享元工厂)
↓ 管理
Flyweight(享元接口)
↑ 实现
ConcreteFlyweight(具体享元)
适用场景
✅ 适合用享元模式的场景:
-
大量相似对象
java// 游戏中的子弹、粒子效果 // 文档编辑器中的字符 // 图形绘制中的图标 -
对象大部分状态可以外部化
java// 对象只有少量内部状态不同 // 大量状态可以从外部传入 -
内存敏感的应用
java// 移动设备应用 // 嵌入式系统 // 游戏引擎
❌ 不适合的场景:
-
对象状态经常变化
java// 如果外部状态太复杂,性能反而下降 -
对象数量很少
java// 如果只有几个对象,没必要用享元 -
对象差异很大
java// 如果每个对象都很独特,无法共享
Java中的实际应用
1. String常量池
java
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2); // true - 享元模式
System.out.println(s1 == s3); // false - 新对象
2. Integer缓存
java
Integer i1 = 127;
Integer i2 = 127;
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i1 == i2); // true - 享元缓存
System.out.println(i3 == i4); // false - 超出缓存范围
3. 连接池
java
// 数据库连接池、线程池都是享元思想
Connection conn1 = dataSource.getConnection(); // 可能返回缓存的连接
Connection conn2 = dataSource.getConnection(); // 可能返回缓存的连接
优缺点
优点:
- 大幅减少内存使用
- 减少对象创建开销
- 提高性能
缺点:
- 增加系统复杂度
- 需要分离内部/外部状态
- 可能引入线程安全问题
实现要点
1. 工厂模式结合
java
// 享元模式通常结合工厂模式使用
public class FlyweightFactory {
private Map<String, Flyweight> pool = new HashMap<>();
public Flyweight getFlyweight(String key) {
if (!pool.containsKey(key)) {
pool.put(key, new ConcreteFlyweight(key));
}
return pool.get(key);
}
}
2. 线程安全考虑
java
// 多线程环境下需要同步
public synchronized Flyweight getFlyweight(String key) {
// ...
}
3. 内存管理
java
// 可能需要清理长时间不用的享元对象
public void cleanup() {
// 清理策略
}
总结
享元模式就是:
- 资源共享:相同的对象只创建一个,大家共用
- 内外分离:不变的部分内部存储,变化的部分外部传递
- 池化思想:像连接池、线程池一样管理对象
核心口诀:
对象大量又相似,
内存消耗成问题。
享元模式来共享,
内存性能双收益!
就像现实中的:
- 🔤 活字印刷:有限的字模印刷无限书籍
- ♟️ 棋盘游戏:有限的棋子玩无限局游戏
- 🎨 颜料调色:有限的基色调出无限颜色
- 🧱 乐高积木:有限的积木块拼出无限造型
记住:当系统中存在大量相似对象,且这些对象可以共享部分状态时,考虑使用享元模式!