享元模式
大约 9 分钟
享元模式
什么是享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享技术来有效地支持大量细粒度的对象。享元模式的主要目的是减少创建对象的数量,以减少内存占用和提高性能。
享元模式的核心思想是:
- 通过共享技术来有效地支持大量细粒度的对象
- 将对象的内部状态和外部状态分离
- 内部状态存储在享元对象内部,外部状态由客户端传递
为什么需要享元模式
在实际开发中,我们经常会遇到需要创建大量相似对象的情况,这些对象可能只有部分状态不同。如果为每个对象都创建一个实例,会消耗大量的内存资源。享元模式通过共享相同的部分来解决这个问题:
- 减少内存使用:通过共享对象来减少内存占用
- 提高性能:减少对象创建和垃圾回收的开销
- 支持大量对象:使得创建大量细粒度对象成为可能
享元模式的结构
享元模式包含以下几个角色:
- 抽象享元(Flyweight):声明一个接口,通过它可以接受并作用于外部状态
- 具体享元(ConcreteFlyweight):实现抽象享元接口,并为内部状态增加存储空间
- 非共享具体享元(UnsharedConcreteFlyweight):不需要共享的Flyweight子类
- 享元工厂(FlyweightFactory):创建并管理Flyweight对象,确保合理地共享Flyweight
- 客户端(Client):维护对Flyweight的引用,计算或存储Flyweight的外部状态
享元模式的实现
基本实现
// 抽象享元接口
interface Flyweight {
void operation(String externalState);
}
// 具体享元类
class ConcreteFlyweight implements Flyweight {
private String internalState;
public ConcreteFlyweight(String internalState) {
this.internalState = internalState;
}
@Override
public void operation(String externalState) {
System.out.println("内部状态: " + internalState + ", 外部状态: " + externalState);
}
}
// 享元工厂
class FlyweightFactory {
private Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
Flyweight flyweight = flyweights.get(key);
if (flyweight == null) {
flyweight = new ConcreteFlyweight(key);
flyweights.put(key, flyweight);
System.out.println("创建新的享元对象: " + key);
} else {
System.out.println("复用享元对象: " + key);
}
return flyweight;
}
public int getFlyweightCount() {
return flyweights.size();
}
}
// 使用示例
public class FlyweightDemo {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
// 获取享元对象
Flyweight flyweight1 = factory.getFlyweight("A");
flyweight1.operation("外部状态1");
Flyweight flyweight2 = factory.getFlyweight("B");
flyweight2.operation("外部状态2");
// 复用已存在的享元对象
Flyweight flyweight3 = factory.getFlyweight("A");
flyweight3.operation("外部状态3");
System.out.println("享元对象总数: " + factory.getFlyweightCount());
}
}
实际应用示例
// 文档编辑器示例
// 字符享元接口
interface Character {
void display(String font, int size, int x, int y);
}
// 具体字符享元类
class CharacterFlyweight implements Character {
private char symbol;
public CharacterFlyweight(char symbol) {
this.symbol = symbol;
}
@Override
public void display(String font, int size, int x, int y) {
System.out.println("字符: " + symbol +
", 字体: " + font +
", 大小: " + size +
", 位置: (" + x + "," + y + ")");
}
}
// 享元工厂
class CharacterFactory {
private Map<Character, CharacterFlyweight> characters = new HashMap<>();
public CharacterFlyweight getCharacter(char symbol) {
CharacterFlyweight character = characters.get(symbol);
if (character == null) {
character = new CharacterFlyweight(symbol);
characters.put(symbol, character);
System.out.println("创建字符享元: " + symbol);
}
return character;
}
public int getCharacterCount() {
return characters.size();
}
}
// 文档类
class Document {
private List<CharacterPosition> characters = new ArrayList<>();
private CharacterFactory factory = new CharacterFactory();
// 字符位置信息(外部状态)
static class CharacterPosition {
CharacterFlyweight character;
String font;
int size;
int x, y;
CharacterPosition(CharacterFlyweight character, String font, int size, int x, int y) {
this.character = character;
this.font = font;
this.size = size;
this.x = x;
this.y = y;
}
}
public void addCharacter(char symbol, String font, int size, int x, int y) {
CharacterFlyweight character = factory.getCharacter(symbol);
characters.add(new CharacterPosition(character, font, size, x, y));
}
public void display() {
System.out.println("=== 显示文档 ===");
for (CharacterPosition cp : characters) {
cp.character.display(cp.font, cp.size, cp.x, cp.y);
}
System.out.println("字符享元总数: " + factory.getCharacterCount());
}
}
// 使用示例
public class DocumentFlyweightDemo {
public static void main(String[] args) {
Document document = new Document();
// 添加文档内容
document.addCharacter('H', "Arial", 12, 0, 0);
document.addCharacter('e', "Arial", 12, 10, 0);
document.addCharacter('l', "Arial", 12, 20, 0);
document.addCharacter('l', "Arial", 12, 30, 0); // 重复字符
document.addCharacter('o', "Arial", 12, 40, 0);
document.addCharacter(' ', "Arial", 12, 50, 0); // 空格
document.addCharacter('W', "Arial", 12, 60, 0);
document.addCharacter('o', "Arial", 12, 70, 0); // 重复字符
document.addCharacter('r', "Arial", 12, 80, 0);
document.addCharacter('l', "Arial", 12, 90, 0); // 重复字符
document.addCharacter('d', "Arial", 12, 100, 0);
// 显示文档
document.display();
}
}
享元模式的应用场景
- 系统中存在大量相似对象:当系统中存在大量相似对象时,可以使用享元模式来减少对象数量
- 需要缓冲池的场景:如数据库连接池、线程池等
- 字符串常量池:Java中的字符串常量池就是享元模式的应用
- 图形渲染:在游戏中渲染大量相同或相似的图形对象
- 文本编辑器:文档中大量重复字符的处理
享元模式的优缺点
优点
- 减少内存使用:通过共享对象来减少内存占用
- 提高性能:减少对象创建和垃圾回收的开销
- 支持大量对象:使得创建大量细粒度对象成为可能
- 外部状态分离:将内部状态和外部状态分离,提高灵活性
缺点
- 增加系统复杂性:需要分离内部状态和外部状态,增加了系统复杂性
- 外部状态管理:需要客户端维护外部状态,增加了客户端的复杂性
- 线程安全问题:共享对象可能带来线程安全问题
享元模式与其他模式的比较
与单例模式的区别
- 享元模式:可以有多个实例,通过共享来减少内存使用
- 单例模式:确保一个类只有一个实例
与对象池模式的区别
- 享元模式:关注于对象的共享,减少内存使用
- 对象池模式:关注于对象的复用,提高性能
与缓存模式的区别
- 享元模式:通过共享来减少对象数量
- 缓存模式:通过存储来避免重复计算
享元模式的变体
1. 带过期时间的享元模式
// 带过期时间的享元工厂
class ExpiringFlyweightFactory {
private Map<String, ExpiringFlyweight> flyweights = new HashMap<>();
private long expirationTime = 30000; // 30秒过期
class ExpiringFlyweight {
Flyweight flyweight;
long createTime;
ExpiringFlyweight(Flyweight flyweight) {
this.flyweight = flyweight;
this.createTime = System.currentTimeMillis();
}
boolean isExpired() {
return System.currentTimeMillis() - createTime > expirationTime;
}
}
public Flyweight getFlyweight(String key) {
// 清理过期的享元
flyweights.entrySet().removeIf(entry -> entry.getValue().isExpired());
ExpiringFlyweight expiringFlyweight = flyweights.get(key);
if (expiringFlyweight == null || expiringFlyweight.isExpired()) {
Flyweight flyweight = new ConcreteFlyweight(key);
expiringFlyweight = new ExpiringFlyweight(flyweight);
flyweights.put(key, expiringFlyweight);
System.out.println("创建新的享元对象: " + key);
return flyweight;
} else {
System.out.println("复用享元对象: " + key);
return expiringFlyweight.flyweight;
}
}
}
2. 线程安全的享元模式
// 线程安全的享元工厂
class ThreadSafeFlyweightFactory {
private final Map<String, Flyweight> flyweights = new ConcurrentHashMap<>();
public Flyweight getFlyweight(String key) {
return flyweights.computeIfAbsent(key, ConcreteFlyweight::new);
}
public int getFlyweightCount() {
return flyweights.size();
}
}
实际项目中的应用
// 游戏中怪物对象的享元模式示例
// 怪物类型(内部状态)
class MonsterType {
private String name;
private String sprite; // 精灵图片
private int baseHealth;
private int baseAttack;
private String behavior; // 行为模式
public MonsterType(String name, String sprite, int baseHealth, int baseAttack, String behavior) {
this.name = name;
this.sprite = sprite;
this.baseHealth = baseHealth;
this.baseAttack = baseAttack;
this.behavior = behavior;
}
// Getter方法
public String getName() { return name; }
public String getSprite() { return sprite; }
public int getBaseHealth() { return baseHealth; }
public int getBaseAttack() { return baseAttack; }
public String getBehavior() { return behavior; }
@Override
public String toString() {
return "MonsterType{name='" + name + "', sprite='" + sprite +
"', health=" + baseHealth + ", attack=" + baseAttack +
", behavior='" + behavior + "'}";
}
}
// 怪物实例(外部状态)
class Monster {
private MonsterType type; // 内部状态
private int x, y; // 外部状态:位置
private int level; // 外部状态:等级
private int currentHealth; // 外部状态:当前血量
public Monster(MonsterType type, int x, int y, int level) {
this.type = type;
this.x = x;
this.y = y;
this.level = level;
// 根据等级计算当前血量
this.currentHealth = type.getBaseHealth() * level;
}
public void display() {
System.out.println("怪物: " + type.getName() +
", 位置: (" + x + "," + y + ")" +
", 等级: " + level +
", 血量: " + currentHealth +
", 精灵: " + type.getSprite());
}
// 移动方法
public void move(int newX, int newY) {
this.x = newX;
this.y = newY;
System.out.println(type.getName() + " 移动到 (" + x + "," + y + ")");
}
// 攻击方法
public void attack() {
int attackPower = type.getBaseAttack() * level;
System.out.println(type.getName() + " 发动攻击,攻击力: " + attackPower);
}
// Getter方法
public MonsterType getType() { return type; }
public int getX() { return x; }
public int getY() { return y; }
public int getLevel() { return level; }
public int getCurrentHealth() { return currentHealth; }
}
// 怪物类型工厂(享元工厂)
class MonsterTypeFactory {
private static final Map<String, MonsterType> monsterTypes = new HashMap<>();
static {
// 初始化一些怪物类型
monsterTypes.put("哥布林", new MonsterType("哥布林", "goblin.png", 50, 10, "随机移动"));
monsterTypes.put("骷髅", new MonsterType("骷髅", "skeleton.png", 80, 15, "直线移动"));
monsterTypes.put("史莱姆", new MonsterType("史莱姆", "slime.png", 30, 5, "跳跃移动"));
monsterTypes.put("巨龙", new MonsterType("巨龙", "dragon.png", 500, 100, "飞行移动"));
}
public static MonsterType getMonsterType(String typeName) {
MonsterType type = monsterTypes.get(typeName);
if (type == null) {
System.out.println("未知的怪物类型: " + typeName);
return null;
}
System.out.println("获取怪物类型: " + typeName);
return type;
}
public static int getMonsterTypeCount() {
return monsterTypes.size();
}
}
// 游戏场景类
class GameScene {
private List<Monster> monsters = new ArrayList<>();
public void addMonster(String typeName, int x, int y, int level) {
MonsterType type = MonsterTypeFactory.getMonsterType(typeName);
if (type != null) {
Monster monster = new Monster(type, x, y, level);
monsters.add(monster);
System.out.println("添加怪物: " + typeName + " 到位置 (" + x + "," + y + ")");
}
}
public void displayAllMonsters() {
System.out.println("\n=== 游戏场景中的所有怪物 ===");
for (Monster monster : monsters) {
monster.display();
}
System.out.println("怪物类型总数: " + MonsterTypeFactory.getMonsterTypeCount());
System.out.println("怪物实例总数: " + monsters.size());
}
public void moveAllMonsters() {
System.out.println("\n=== 怪物移动 ===");
for (Monster monster : monsters) {
// 简单的移动逻辑
monster.move(monster.getX() + 1, monster.getY() + 1);
}
}
public void allMonstersAttack() {
System.out.println("\n=== 怪物攻击 ===");
for (Monster monster : monsters) {
monster.attack();
}
}
}
// 使用示例
public class GameFlyweightDemo {
public static void main(String[] args) {
GameScene scene = new GameScene();
// 添加大量怪物(很多重复类型)
scene.addMonster("哥布林", 10, 20, 1);
scene.addMonster("哥布林", 15, 25, 2);
scene.addMonster("哥布林", 20, 30, 1);
scene.addMonster("骷髅", 30, 40, 3);
scene.addMonster("骷髅", 35, 45, 2);
scene.addMonster("史莱姆", 50, 60, 1);
scene.addMonster("史莱姆", 55, 65, 1);
scene.addMonster("史莱姆", 60, 70, 2);
scene.addMonster("巨龙", 100, 100, 10);
scene.addMonster("哥布林", 12, 22, 1); // 重复的哥布林
// 显示所有怪物
scene.displayAllMonsters();
// 怪物移动
scene.moveAllMonsters();
// 怪物攻击
scene.allMonstersAttack();
}
}
// Java字符串常量池示例
public class StringPoolDemo {
public static void main(String[] args) {
// 字面量创建的字符串会进入常量池
String str1 = "Hello";
String str2 = "Hello";
String str3 = "Hello";
// 使用new创建的字符串不会进入常量池
String str4 = new String("Hello");
String str5 = new String("Hello");
System.out.println("str1 == str2: " + (str1 == str2)); // true
System.out.println("str2 == str3: " + (str2 == str3)); // true
System.out.println("str1 == str4: " + (str1 == str4)); // false
System.out.println("str4 == str5: " + (str4 == str5)); // false
// 使用intern()方法可以将字符串放入常量池
String str6 = str4.intern();
System.out.println("str1 == str6: " + (str1 == str6)); // true
// 查看字符串常量池的大小
System.out.println("字符串常量池中的字符串:");
String[] testStrings = {"Hello", "World", "Java", "Flyweight", "Pattern"};
for (String s : testStrings) {
System.out.println(" " + s + " (hash: " + s.hashCode() + ")");
}
}
}
总结
享元模式是一种非常实用的结构型设计模式,它通过共享技术来有效地支持大量细粒度的对象,从而减少内存使用和提高性能。在实际开发中,当我们需要创建大量相似对象时,享元模式是一个很好的选择。
使用享元模式的关键点:
- 识别出对象中的内部状态和外部状态
- 将内部状态存储在享元对象中,外部状态由客户端管理
- 创建享元工厂来管理享元对象的创建和复用
- 客户端通过享元工厂获取享元对象,并传递外部状态
享元模式的优点是可以显著减少内存使用,支持大量对象的创建,但也需要注意正确分离内部状态和外部状态,以及处理线程安全问题。在现代Java开发中,享元模式广泛应用于游戏开发、文本处理、数据库连接池、线程池等需要大量相似对象的场景。