本文是【GoF设计模式】系列第12篇

前言
为什么需要享元模式?
假设要做一个文字处理软件,一篇 10 万字的文档,每个字符都有字体、字号、颜色等格式属性。如果每个字符都独立存储一份格式对象,就要创建 10 万个格式对象------其中大量对象的属性完全相同(比如正文都是"宋体、12号、黑色"),内存直接爆掉。
java
// 每个字符一个格式对象:10万个对象,大量重复
class CharFormat {
String font;
int size;
String color;
}
游戏开发也是一样:场景中可能有 10000 棵树,每棵树的类型只有 3 种(橡树、松树、枫树)。如果每棵树都创建一个完整的类型对象,9997 个对象都是浪费------同类树木的颜色、纹理、耐旱度完全相同,只有坐标不同。
这种"大量对象的属性可以分为'相同的'和'不同的'两部分"的矛盾,就是享元模式要解决的问题。
概念
享元模式(Flyweight Pattern)是一种结构型设计模式 ,核心思想是通过共享相同对象来减少内存占用。
名字的含义
"享元"这个名字拆开来看:
- 享 = 共享(share),多个上下文共用同一个对象实例
- 元 = 元素(element),被共享的那个对象本身
英文名 Flyweight 来自拳击术语"蝇量级"(最轻量级),强调效果------通过共享,大量对象变得"轻量"。中文名强调机制 (共享),英文名强调效果(变轻),合在一起概括了这个模式的全貌。
内部状态与外部状态
享元模式的精髓在于区分两种状态:
- 内部状态 :存储在享元对象内部,对所有上下文都相同,不可变。例如公司公章的图案和文字------刻好之后就不会变了。
- 外部状态 :依赖上下文、可能变化的部分,不存储在享元对象内部,由客户端在使用时传入。例如盖章时合同上需要盖章的位置------每份合同不同。
举个例子:公司只有一个公章(内部状态固定),但可以盖在无数份合同的不同位置(外部状态变化)。不需要为每份合同刻一个新章------这就是享元模式"共享"的本质。
角色
享元模式包括以下四个角色:
- 享元接口 Flyweight:所有具体享元类的共享接口,包含接受外部状态的方法。
- 具体享元类 ConcreteFlyweight:实现享元接口,存储内部状态。
- 享元工厂 FlyweightFactory:创建并管理享元对象池,当用户请求时,提供已创建的实例或新建一个。
- 客户端 Client:维护外部状态,在使用享元对象时将外部状态传入。
#mermaid-svg-xS2i2SpDO3Jkj8vo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xS2i2SpDO3Jkj8vo .error-icon{fill:#552222;}#mermaid-svg-xS2i2SpDO3Jkj8vo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xS2i2SpDO3Jkj8vo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xS2i2SpDO3Jkj8vo .marker.cross{stroke:#333333;}#mermaid-svg-xS2i2SpDO3Jkj8vo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xS2i2SpDO3Jkj8vo p{margin:0;}#mermaid-svg-xS2i2SpDO3Jkj8vo g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-xS2i2SpDO3Jkj8vo g.classGroup text .title{font-weight:bolder;}#mermaid-svg-xS2i2SpDO3Jkj8vo .cluster-label text{fill:#333;}#mermaid-svg-xS2i2SpDO3Jkj8vo .cluster-label span{color:#333;}#mermaid-svg-xS2i2SpDO3Jkj8vo .cluster-label span p{background-color:transparent;}#mermaid-svg-xS2i2SpDO3Jkj8vo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xS2i2SpDO3Jkj8vo .cluster text{fill:#333;}#mermaid-svg-xS2i2SpDO3Jkj8vo .cluster span{color:#333;}#mermaid-svg-xS2i2SpDO3Jkj8vo .nodeLabel,#mermaid-svg-xS2i2SpDO3Jkj8vo .edgeLabel{color:#131300;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-xS2i2SpDO3Jkj8vo .label text{fill:#131300;}#mermaid-svg-xS2i2SpDO3Jkj8vo .labelBkg{background:#ECECFF;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-xS2i2SpDO3Jkj8vo .classTitle{font-weight:bolder;}#mermaid-svg-xS2i2SpDO3Jkj8vo .node rect,#mermaid-svg-xS2i2SpDO3Jkj8vo .node circle,#mermaid-svg-xS2i2SpDO3Jkj8vo .node ellipse,#mermaid-svg-xS2i2SpDO3Jkj8vo .node polygon,#mermaid-svg-xS2i2SpDO3Jkj8vo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xS2i2SpDO3Jkj8vo .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo g.clickable{cursor:pointer;}#mermaid-svg-xS2i2SpDO3Jkj8vo g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-xS2i2SpDO3Jkj8vo g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-xS2i2SpDO3Jkj8vo .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-xS2i2SpDO3Jkj8vo .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-xS2i2SpDO3Jkj8vo .dashed-line{stroke-dasharray:3;}#mermaid-svg-xS2i2SpDO3Jkj8vo .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-xS2i2SpDO3Jkj8vo #compositionStart,#mermaid-svg-xS2i2SpDO3Jkj8vo .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #compositionEnd,#mermaid-svg-xS2i2SpDO3Jkj8vo .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #dependencyStart,#mermaid-svg-xS2i2SpDO3Jkj8vo .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #dependencyStart,#mermaid-svg-xS2i2SpDO3Jkj8vo .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #extensionStart,#mermaid-svg-xS2i2SpDO3Jkj8vo .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #extensionEnd,#mermaid-svg-xS2i2SpDO3Jkj8vo .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #aggregationStart,#mermaid-svg-xS2i2SpDO3Jkj8vo .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #aggregationEnd,#mermaid-svg-xS2i2SpDO3Jkj8vo .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #lollipopStart,#mermaid-svg-xS2i2SpDO3Jkj8vo .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo #lollipopEnd,#mermaid-svg-xS2i2SpDO3Jkj8vo .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-xS2i2SpDO3Jkj8vo .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-xS2i2SpDO3Jkj8vo .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xS2i2SpDO3Jkj8vo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xS2i2SpDO3Jkj8vo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xS2i2SpDO3Jkj8vo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实现
创建和管理
获取享元
使用享元
<<interface>>
Flyweight
+operation(extrinsicState)
ConcreteFlyweight
-intrinsicState
+operation(extrinsicState)
FlyweightFactory
-pool: Map
+getFlyweight(key) : : Flyweight
Client
-extrinsicState
图中各类之间的关系:FlyweightFactory 依赖 Flyweight 接口创建和管理对象,ConcreteFlyweight 实现了 Flyweight 接口并持有内部状态,Client 依赖 FlyweightFactory 获取享元对象、同时依赖 Flyweight 接口使用享元对象------外部状态由 Client 自己维护,不体现为 Flyweight 的字段。
实现
标准实现
享元工厂维护一个对象池,按内部状态标识查找已有对象------未找到则创建并放入池中,找到则直接返回。
java
// 享元接口
interface Flyweight {
void operation(String extrinsicState);
}
// 具体享元类
class ConcreteFlyweight implements Flyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
public void operation(String extrinsicState) {
System.out.println("内部状态: " + intrinsicState
+ ", 外部状态: " + extrinsicState);
}
}
// 享元工厂
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);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight f1 = factory.getFlyweight("X");
f1.operation("First");
Flyweight f2 = factory.getFlyweight("X");
f2.operation("Second");
System.out.println(f1 == f2); // true,同一个实例
}
}
引入一个例子:「公司只有一个公章(内部状态固定),但可以盖在无数份合同的不同位置(外部状态变化)。不需要为每份合同刻一个新章------这就是享元模式"共享"的本质。」
java
// 公章(享元)
class Seal {
private String companyName; // 内部状态:公司名称
private String pattern; // 内部状态:公章图案
public Seal(String companyName, String pattern) {
this.companyName = companyName;
this.pattern = pattern;
}
public void stamp(String contractName, int x, int y) {
// 在合同的 (x, y) 位置盖章
System.out.println("在《" + contractName + "》的(" + x
+ "," + y + ")位置盖章:" + companyName + " " + pattern);
}
}
// 公章工厂 ------ 相当于"公章管理处"
class SealFactory {
private Map<String, Seal> pool = new HashMap<>();
public Seal getSeal(String companyName) {
if (!pool.containsKey(companyName)) {
// 实际项目中 pattern 也应作为参数传入,此处简化
pool.put(companyName, new Seal(companyName, "五角星"));
}
return pool.get(companyName);
}
}
// 公司(客户端)------ 相当于"盖章的人"
class Company {
public static void main(String[] args) {
SealFactory factory = new SealFactory();
// 公司只有一枚公章(内部状态固定)
Seal seal1 = factory.getSeal("阿里巴巴");
Seal seal2 = factory.getSeal("阿里巴巴");
System.out.println(seal1 == seal2); // true,同一枚公章
// 同一枚公章盖在不同合同的不同位置(外部状态变化)
seal1.stamp("采购合同", 100, 200); // 位置(100,200)
seal2.stamp("销售合同", 150, 300); // 位置(150,300)
seal1.stamp("劳动合同", 80, 150); // 位置(80,150)
// 三份合同用的是同一枚公章对象,不需要为每份合同刻一个新章
}
}
关键点:Seal 的公司名称和图案是内部状态(像公章上刻好的字),创建后永不改变;合同名称和盖章位置是外部状态(像盖在哪份合同的哪个位置),由客户端调用 stamp() 时传入。公司只需要一枚公章,就能盖无数份合同------这就是享元模式"共享"的本质。
享元工厂与缓存
享元就像图书馆里的一本书被多人同时借阅------每个人翻到不同页码(外部状态),但书本身(内部状态)只有一本。缓存就像把借过的书复印一份存起来,下次不用再借------省的是"借"的功夫,不是"书"的数量。
享元工厂里确实有个 Map 存着对象,看起来很像缓存,但二者的目的和机制完全不同:
| 对比维度 | 享元模式 | 缓存 |
|---|---|---|
| 核心目的 | 共享对象实例,节省内存 | 存储计算结果,避免重复计算/查询 |
| 关键机制 | 分离内部状态和外部状态 | 键值对存储 + 淘汰策略 |
| 返回结果 | 同一个对象实例(多处同时持有同一引用) | 可能是新对象、副本或同一引用 |
| 管理策略 | 创建后常驻,通常不淘汰 | 有 LRU/TTL 等淘汰策略 |
一句话区分:享元共享的是"对象本身",缓存存储的是"计算结果"。 享元能做到的事------让同一个对象同时被多个上下文使用,每次传入不同的外部状态------缓存做不到,因为缓存不关心内部/外部状态的分离。享元工厂本质上用了缓存的思想来存储对象,但享元多了内部/外部状态的分离和外部状态的参数化传递,这是缓存不具备的。
对象状态能分离为内外两类 → 享元(节省内存);不能分离,只是想避免重复计算 → 缓存(减少计算)。
总结
享元模式本质上是分离内部状态和外部状态,通过共享内部状态相同的对象来减少内存占用。
什么时候用:
- 系统中有大量相似对象,且对象的属性可以分为"相同的"和"不同的"两部分
- 内存占用是瓶颈,需要优化对象数量
- 对象的内部状态不可变,可以安全共享
什么时候不用:
- 对象数量本来就不多,共享没有意义
- 对象内部状态各不相同,无法共享
- 内部状态需要频繁修改,享元对象必须不可变
简单记忆:
享元分内外,共享省内存。内部不可变,外部传参用。
相似模式区分
| 模式 | 接口关系 | 核心意图 | 典型场景 |
|---|---|---|---|
| 享元 | 工厂按key管理享元对象池 | 共享内部状态相同的对象,节省内存 | 文本格式、游戏纹理、地图图标 |
| 单例 | 全局静态方法返回同一实例 | 确保全局只有一个实例 | 配置管理、日志记录 |
| 缓存 | 键值对存储 + 淘汰策略 | 存储计算结果,避免重复计算 | 数据库查询、API调用 |
口诀对比:享元省内存,单例保唯一,缓存减计算。
享元 vs 单例
| 维度 | 享元模式 | 单例模式 |
|---|---|---|
| 核心意图 | 共享大量相似对象,节省内存 | 确保全局只有一个实例 |
| 结构差异 | 多个实例(每种内部状态一个),工厂管理对象池 | 一个实例(全局唯一),静态方法返回 |
| 关注点 | 对象状态分离为内外两类,内部状态不可变 | 实例唯一性,可以有可变状态 |
| 典型场景 | 文本格式、游戏纹理、地图图标 | 配置管理、日志记录、数据库连接池 |
逐步区分法:
- 需要限制实例数量为唯一一个 → 单例
- 需要同一个对象实例被多处共享,对象状态可分离为内外两类 → 享元
记忆口诀:"单例一人独享,享元多人共享。"
练习题目
游戏场景 - 树木渲染
题目描述:在一个开放世界游戏中,场景中有大量树木需要渲染。树木分为三种类型:
| 类型 | 颜色 | 纹理 | 耐旱度 |
|---|---|---|---|
| OAK(橡树) | Green | Rough | 3 |
| PINE(松树) | DarkGreen | Smooth | 5 |
| MAPLE(枫树) | Red | Rough | 2 |
颜色、纹理、耐旱度是同类树木共有的内部状态,而每棵树的坐标 (x, y) 是外部状态。请使用享元模式实现树木渲染系统,使相同类型的树木共享同一个享元对象。不用享元模式的话,10000 棵树就要创建 10000 个 TreeType 对象------同类的 9997 个都是重复的,只有坐标不同,颜色/纹理完全相同,内存直接爆了。
输入描述:多行,每行一个种植命令,格式为:
树木类型 x y
输出描述:对于每个种植命令:
-
若该类型的享元对象首次创建,先输出:
Creating [类型]: color=[颜色], texture=[纹理], droughtTolerance=[耐旱度] -
然后输出:
[类型] planted at (x, y) -
所有命令处理完毕后输出:
Total trees planted: N
Flyweight objects created: M
输入示例:
OAK 10 20
PINE 30 40
OAK 15 25
MAPLE 5 15
PINE 50 60
OAK 10 20
输出示例:
Creating OAK: color=Green, texture=Rough, droughtTolerance=3
OAK planted at (10, 20)
Creating PINE: color=DarkGreen, texture=Smooth, droughtTolerance=5
PINE planted at (30, 40)
OAK planted at (15, 25)
Creating MAPLE: color=Red, texture=Rough, droughtTolerance=2
MAPLE planted at (5, 15)
PINE planted at (50, 60)
OAK planted at (10, 20)
Total trees planted: 6
Flyweight objects created: 3
解题思路 :树类型(颜色、纹理、耐旱度)是内部状态------同一类树的这些属性完全一样,只需创建一个共享对象。坐标是外部状态------每棵树的位置不同,由客户端调用 display(x, y) 时传入。享元工厂按类型名管理对象池,首次遇到某类型时创建并打印创建信息,之后再遇到直接返回已有对象。不用享元模式,6 棵树就要 new 6 次 TreeType;用了享元,只需要 new 3 次。
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
TreeFactory factory = new TreeFactory();
int totalPlanted = 0;
while (sc.hasNext()) {
String typeName = sc.next();
int x = sc.nextInt();
int y = sc.nextInt();
Tree tree = factory.getTree(typeName);
tree.display(x, y);
totalPlanted++;
}
System.out.println("Total trees planted: " + totalPlanted);
System.out.println("Flyweight objects created: "
+ factory.getPoolSize());
sc.close();
}
}
interface Tree {
void display(int x, int y);
}
class TreeType implements Tree {
private String type; // typeName 既是池的 key,也是享元的内部状态
private String color;
private String texture;
private int droughtTolerance;
public TreeType(String type, String color, String texture,
int droughtTolerance) {
this.type = type;
this.color = color;
this.texture = texture;
this.droughtTolerance = droughtTolerance;
}
public String getColor() { return color; }
public String getTexture() { return texture; }
public int getDroughtTolerance() { return droughtTolerance; }
public void display(int x, int y) {
System.out.println(type + " planted at (" + x + "," + y + ")");
}
}
class TreeFactory {
private Map<String, Tree> pool = new HashMap<>();
public Tree getTree(String typeName) {
if (!pool.containsKey(typeName)) {
TreeType tree = createTreeType(typeName);
pool.put(typeName, tree);
System.out.println("Creating " + typeName + ": color="
+ tree.getColor() + ", texture=" + tree.getTexture()
+ ", droughtTolerance="
+ tree.getDroughtTolerance());
}
return pool.get(typeName);
}
private TreeType createTreeType(String typeName) {
if ("OAK".equals(typeName)) {
return new TreeType("OAK", "Green", "Rough", 3);
} else if ("PINE".equals(typeName)) {
return new TreeType("PINE", "DarkGreen", "Smooth", 5);
} else { // MAPLE
return new TreeType("MAPLE", "Red", "Rough", 2);
}
}
public int getPoolSize() {
return pool.size();
}
}
扩展:实际项目中的享元模式
Java String 常量池
JVM 中的字符串常量池是享元模式最经典的实现。内容相同的字符串字面量在常量池中只存一份,所有引用指向同一个对象。
java
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,同一个对象
String s3 = new String("hello");
String s4 = s3.intern(); // 手动放入常量池,返回池中的引用
System.out.println(s1 == s4); // true
关键点:字符串的内容就是内部状态(不可变),intern() 相当于享元工厂的 getFlyweight() 方法------池中有则返回已有实例,没有则放入再返回。
Java Integer 缓存
Integer.valueOf() 对 -128 到 127 范围内的整数做了享元缓存,避免频繁装箱时创建大量重复的小整数对象。
java
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,超出缓存范围,各自创建新对象
关键点:IntegerCache 就是享元工厂,[-128, 127] 范围内的 Integer 对象就是具体享元,整数值本身就是内部状态(不可变)。这是 JDK 源码中可以直接查阅的享元模式实例------打开 java.lang.Integer 就能看到 IntegerCache 内部类。
游戏中的纹理共享
同一个纹理文件被场景中成百上千个粒子或模型引用,如果每个对象都加载一份纹理数据,内存直接爆掉。游戏引擎用享元模式让同类对象共享纹理,每个对象只维护自己的位置、旋转等外部状态。
java
// 纹理(享元)
class Texture {
private byte[] imageData; // 内部状态:纹理数据
public Texture(String path) {
this.imageData = loadImage(path); // 只加载一次
}
public void render(int x, int y) { /* 渲染逻辑 */ }
}
// 纹理工厂
class TextureFactory {
private Map<String, Texture> pool = new HashMap<>();
public Texture getTexture(String path) {
if (!pool.containsKey(path)) {
pool.put(path, new Texture(path));
}
return pool.get(path);
}
}
// 粒子(外部状态由粒子自己维护)
class Particle {
private Texture texture; // 共享的享元对象
private int x, y; // 外部状态:坐标
public void draw() {
texture.render(x, y); // 传入外部状态
}
}
关键点:TextureFactory 保证同一个纹理路径只加载一次,10000 个同类粒子共享同一份纹理数据。
地图标记图标共享
地图上可能有成千上万个标记点(加油站、餐厅、景点),同一类型的标记使用相同图标。每个标记点共享一个图标对象,各自维护自己的经纬度坐标。高德、百度等地图 SDK 内部大量使用类似机制管理标记图标。
java
// 地图图标(享元)
class MapIcon {
private String iconFile; // 内部状态:图标文件
public MapIcon(String file) {
this.iconFile = file;
}
public void render(double lat, double lng) { /* 渲染逻辑 */ }
}
// 图标工厂
class MapIconFactory {
private Map<String, MapIcon> pool = new HashMap<>();
public MapIcon getIcon(String type) {
if (!pool.containsKey(type)) {
pool.put(type, new MapIcon(type + ".png"));
}
return pool.get(type);
}
}
// 标记点(外部状态由标记点自己维护)
class MapMarker {
private MapIcon icon; // 共享的享元对象
private double lat, lng; // 外部状态:经纬度
public void draw() {
icon.render(lat, lng); // 传入外部状态
}
}
关键点:MapIconFactory 保证同一种图标只创建一次,成千上万个标记点共享同一个图标对象。