【软考设计模式】享元模式:共享技术支撑海量细粒度对象与代码填空精讲
【软考设计模式】享元模式:共享技术支撑海量细粒度对象与代码填空精讲
系列定位:软考软件设计师 / 系统架构设计师 — 结构型模式专题第 5 讲
考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现
难度等级:⭐⭐⭐⭐☆(内部状态与外部状态的区分是核心,也是高频失分点)
一、考纲定位与模式定义
1.1 考纲要求
享元模式在软考中属于结构型模式的中高难度内容。考察形式包括:
上午选择题:判断场景描述所属模式;识别内部状态(Intrinsic State)与外部状态(Extrinsic State)的区分;判断享元模式与单例模式、原型模式的区别;判断 FlyweightFactory 的缓存/池化逻辑
下午设计题:补全 FlyweightFactory 的
getFlyweight()方法(从池中获取或创建享元对象);补全 ConcreteFlyweight 的operation()方法(使用外部状态参数);识别类图中享元工厂与具体享元的关系
1.2 模式定义
享元模式:运用共享技术有效地支持大量细粒度的对象。
核心意图:当系统中存在大量相似对象(如文档中的字符、棋盘上的棋子、地图上的图标),这些对象的大部分状态是相同的,只有少量状态不同。如果为每个对象都创建独立实例,内存开销巨大。享元模式将对象的状态拆分为内部状态(Intrinsic State,可共享,存储在享元对象内部)和外部状态(Extrinsic State,不可共享,由客户端传入),让多个对象共享同一个享元实例,从而大幅减少对象数量。
通俗理解:
你在 Word 里打了一万字,每个字都是一个对象。如果每个字都创建一个独立对象,内存里就有一万个对象。但实际上,"字" 只有几千个常用汉字(内部状态:字符本身),而每个字在文档中的位置、字体大小、颜色(外部状态)是不同的。享元模式的思路是:内存里只保存一份 "中" 字对象,一万个 "中" 字都共享这同一个对象,只是在使用时传入各自的位置和颜色(外部状态)。
二、UML 类图与角色划分
┌─────────────────────────────────────────────────────────┐ │ Client │ │ + main() │ │ ├── Flyweight f = factory.getFlyweight("A"); │ │ └── f.operation(extrinsicState); // 传入外部状态 │ └───────────────────────┬─────────────────────────────────┘ │ uses ▼ ┌─────────────────────────────────────────────────────────┐ │ FlyweightFactory │ │ - flyweights: Map<String, Flyweight> │ │ + getFlyweight(key): Flyweight { │ │ if (!flyweights.containsKey(key)) { │ │ flyweights.put(key, new ConcreteFlyweight(key)); │ │ } │ │ return flyweights.get(key); │ │ } │ └───────────────────────┬─────────────────────────────────┘ │ manages ▼ ┌─────────────────────────────────────────────────────────┐ │ <<interface>> │ │ Flyweight │ │ + operation(extrinsicState) │ └───────────────────────┬─────────────────────────────────┘ ┌────────────┴────────────┐ ▼ ▼ ┌──────────────────────┐ ┌──────────────────────┐ │ ConcreteFlyweight │ │ UnsharedConcrete │ │ (共享的享元对象) │ │ Flyweight │ │ - intrinsicState │ │ (非共享享元对象) │ │ + operation(ext) { │ │ + operation(ext) { │ │ // 使用内部状态 │ │ // 不共享,独立实例 │ │ // 使用外部状态 │ │ // 所有状态独立 │ │ } │ │ } │ └──────────────────────┘ └──────────────────────┘| 角色 | 职责 | 软考填空关键词 |
|---|---|---|
| Flyweight(抽象享元) | 声明接口,通过operation()方法接收外部状态并执行操作 | interface+operation(extrinsicState) |
| ConcreteFlyweight(具体享元) | 实现 Flyweight 接口,存储内部状态(intrinsic),接收并使用外部状态(extrinsic) | intrinsicState字段 + 构造器初始化 |
| UnsharedConcreteFlyweight(非共享享元) | 不需要共享的享元子类,所有状态都是外部状态(或独立状态) | 不通过工厂缓存,每次new创建 |
| FlyweightFactory(享元工厂) | 创建并管理享元对象,维护一个享元池(Map),确保相同内部状态的对象只创建一次 | Map<String, Flyweight>+getFlyweight(key) |
| Client(客户端) | 通过工厂获取享元对象,传入外部状态调用operation() | factory.getFlyweight(key)+flyweight.operation(extrinsicState) |
类图识别要点:软考类图中,享元模式的核心特征是存在FlyweightFactory类,内部维护一个
Map(享元池),通过getFlyweight(key)方法获取享元对象。ConcreteFlyweight 通常只有一个或少数几个实例被共享。如果类图里有工厂 + Map + 共享对象 → 享元。
三、场景一:字符渲染(GoF 经典例子,内部状态与外部状态分离)
业务背景:文本编辑器中,每个字符都是一个对象。字符的字符编码(如 'A'、'中')是内部状态(可共享),字符的位置坐标 (x, y)和颜色是外部状态(每个字符不同,由客户端传入)。使用享元模式,内存中只保存有限数量的字符对象,而不是每个出现的字符都创建一个对象。
说明:这是 GoF 原书中享元模式的经典例子,也是软考下午题最爱考的 "内部状态 vs 外部状态" 场景。
3.1 代码实现
import java.util.HashMap; import java.util.Map; // Flyweight:字符享元接口 public interface CharFlyweight { void render(int x, int y, String color); } // ConcreteFlyweight:具体字符享元(共享对象) public class ConcreteChar implements CharFlyweight { // 内部状态:字符编码(可共享,存储在享元对象内部) private char charCode; public ConcreteChar(char charCode) { this.charCode = charCode; } @Override public void render(int x, int y, String color) { System.out.println("渲染字符 '" + charCode + "' 在位置(" + x + "," + y + "),颜色: " + color); } } // FlyweightFactory:字符享元工厂 public class CharFactory { private Map<Character, CharFlyweight> pool = new HashMap<>(); public CharFlyweight getChar(char charCode) { CharFlyweight flyweight = pool.get(charCode); if (flyweight == null) { flyweight = new ConcreteChar(charCode); pool.put(charCode, flyweight); System.out.println("[工厂] 创建新享元: '" + charCode + "'"); } else { System.out.println("[工厂] 复用享元: '" + charCode + "'"); } return flyweight; } public int getPoolSize() { return pool.size(); } } // Client:客户端 public class Client { public static void main(String[] args) { CharFactory factory = new CharFactory(); String text = "Hello"; int[][] positions = {{0,0}, {10,0}, {20,0}, {30,0}, {40,0}}; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); CharFlyweight flyweight = factory.getChar(c); flyweight.render(positions[i][0], positions[i][1], "黑色"); } System.out.println("\n文档共 " + text.length() + " 个字符,内存中实际创建 " + factory.getPoolSize() + " 个享元对象"); } }关键体会:
内部状态(
charCode):存储在ConcreteChar内部,由工厂通过Map缓存共享。相同的字符只创建一次。外部状态(
x, y, color):不存储在享元对象中,由客户端在使用时传入。每个字符的位置和颜色可以不同。文档有 5 个字符,但内存中只有 4 个享元对象('l' 被共享),节省了 20% 的内存。如果文档有 10 万字,节省的内存就非常可观了。
四、场景二:围棋棋子(内部状态:颜色;外部状态:坐标)
业务背景:围棋棋盘上有 361 个交叉点,每局棋可能有数百个棋子。棋子的颜色(黑/白)是内部状态(只有两种,可共享),棋子的位置坐标 (x, y)是外部状态(每个棋子不同)。使用享元模式,内存中只需保存 2 个棋子对象(黑棋、白棋),而不是数百个。
说明:这是软考上午题中常考的 "棋子共享" 场景,也是理解享元模式最直观的例子。
4.1 代码实现
import java.util.HashMap; import java.util.Map; // Flyweight:棋子享元接口 public interface ChessPiece { void place(int x, int y); } // ConcreteFlyweight:具体棋子享元(共享对象) public class ConcreteChessPiece implements ChessPiece { private String color; public ConcreteChessPiece(String color) { this.color = color; } @Override public void place(int x, int y) { System.out.println("在 (" + x + "," + y + ") 放置 " + color + " 棋"); } } // FlyweightFactory:棋子享元工厂 public class ChessPieceFactory { private Map<String, ChessPiece> pool = new HashMap<>(); public ChessPiece getPiece(String color) { ChessPiece piece = pool.get(color); if (piece == null) { piece = new ConcreteChessPiece(color); pool.put(color, piece); System.out.println("[工厂] 创建新棋子: " + color); } else { System.out.println("[工厂] 复用棋子: " + color); } return piece; } public int getPieceCount() { return pool.size(); } } // Client:客户端(模拟一局棋) public class Client { public static void main(String[] args) { ChessPieceFactory factory = new ChessPieceFactory(); ChessPiece black = factory.getPiece("黑"); black.place(3, 3); ChessPiece white = factory.getPiece("白"); white.place(4, 4); ChessPiece black2 = factory.getPiece("黑"); black2.place(5, 5); ChessPiece white2 = factory.getPiece("白"); white2.place(6, 6); System.out.println("\n本局共 4 个棋子,内存中实际创建 " + factory.getPieceCount() + " 个享元对象"); System.out.println("black == black2 ? " + (black == black2)); System.out.println("white == white2 ? " + (white == white2)); } }关键体会:
一局棋可能有 200 多个棋子,但内存中只有2 个享元对象(黑棋和白棋)。
每个棋子的位置是外部状态,由客户端在调用
place(x, y)时传入。如果不用享元模式,200 个棋子需要 200 个对象;用了享元模式,只需 2 个对象。内存节省99%。
软考高频考点:围棋/五子棋是享元模式的经典例子。内部状态 = 颜色(黑/白),外部状态 = 坐标 (x, y)。
五、场景三:数据库连接池(享元模式的工程化应用)
业务背景:系统需要频繁访问数据库,创建数据库连接是昂贵的操作(需要 TCP 握手、认证等)。使用享元模式维护一个连接池,池中每个连接是共享的享元对象。连接的配置信息(URL、用户名、密码)是内部状态,连接的当前使用者和使用状态(空闲/占用)是外部状态。
说明:这是享元模式在实际工程中的典型应用,也是软考上午题中 "连接池/线程池使用的设计模式" 的标准答案。
5.1 代码实现
import java.util.ArrayList; import java.util.List; // Flyweight:连接享元接口 public interface Connection { void execute(String sql, String user); void close(); } // ConcreteFlyweight:数据库连接(共享对象) public class DatabaseConnection implements Connection { private String url; private String dbUser; private String dbPassword; private int connectionId; public DatabaseConnection(int id, String url, String dbUser, String dbPassword) { this.connectionId = id; this.url = url; this.dbUser = dbUser; this.dbPassword = dbPassword; System.out.println("[连接池] 创建新连接 #" + id); } @Override public void execute(String sql, String user) { System.out.println("[连接 #" + connectionId + "] 用户 '" + user + "' 执行: " + sql); } @Override public void close() { System.out.println("[连接 #" + connectionId + "] 归还到连接池"); } } // FlyweightFactory:连接池(享元工厂) public class ConnectionPool { private List<Connection> available = new ArrayList<>(); private List<Connection> inUse = new ArrayList<>(); private String url; private String dbUser; private String dbPassword; private int maxSize; private int createdCount = 0; public ConnectionPool(String url, String dbUser, String dbPassword, int maxSize) { this.url = url; this.dbUser = dbUser; this.dbPassword = dbPassword; this.maxSize = maxSize; } public synchronized Connection getConnection() { if (!available.isEmpty()) { Connection conn = available.remove(0); inUse.add(conn); System.out.println("[连接池] 复用现有连接,空闲: " + available.size() + ", 占用: " + inUse.size()); return conn; } if (createdCount < maxSize) { createdCount++; Connection conn = new DatabaseConnection(createdCount, url, dbUser, dbPassword); inUse.add(conn); System.out.println("[连接池] 创建新连接,总数: " + createdCount); return conn; } throw new RuntimeException("连接池已满,无法获取连接"); } public synchronized void release(Connection conn) { inUse.remove(conn); available.add(conn); System.out.println("[连接池] 连接归还,空闲: " + available.size() + ", 占用: " + inUse.size()); } public int getCreatedCount() { return createdCount; } } // Client public class Client { public static void main(String[] args) { ConnectionPool pool = new ConnectionPool( "jdbc:mysql://localhost:3306/test", "root", "123456", 3 ); Connection conn1 = pool.getConnection(); conn1.execute("SELECT * FROM users", "用户A"); Connection conn2 = pool.getConnection(); conn2.execute("UPDATE orders SET status = 1", "用户B"); conn1.close(); pool.release(conn1); Connection conn3 = pool.getConnection(); conn3.execute("INSERT INTO logs VALUES (...)", "用户C"); System.out.println("\n连接池实际创建连接数: " + pool.getCreatedCount()); } }关键体会:
连接池是享元模式的工程化变体。连接的配置信息(URL、用户名、密码)是内部状态,所有连接共享相同的配置。
连接的当前使用者(
user参数)和使用状态(空闲/占用)是外部状态,由客户端和工厂共同管理。连接池通过限制享元对象的总数(
maxSize),避免资源耗尽。这与纯享元模式略有不同(纯享元模式不限制总数,只限制不同内部状态的数量),但核心思想一致:共享对象,减少创建。
六、三种场景对比与演进思路
| 维度 | 场景一:字符渲染 | 场景二:围棋棋子 | 场景三:数据库连接池 |
|---|---|---|---|
| 内部状态 | 字符编码(charCode) | 棋子颜色(黑/白) | 连接配置(URL、用户名、密码) |
| 外部状态 | 位置 (x, y)、颜色 | 坐标 (x, y) | 当前使用者、SQL 语句 |
| 享元对象数量 | 字符种类数(如 100 个汉字) | 2(黑棋、白棋) | 池大小(如 10 个连接) |
| 实际对象数量 | 文档字符数(如 10 万字) | 棋局棋子数(如 200 个) | 并发请求数(如 1000 个) |
| 节省比例 | 约 99%(10 万字 / 100 种字符) | 约 99%(200 个 / 2 种颜色) | 约 99%(1000 请求 / 10 连接) |
| 工厂管理 | Map 缓存,无上限 | Map 缓存,无上限 | List 池化,有上限(maxSize) |
| 软考考法 | 下午大题(区分内外状态) | 上午选择题(识别棋子共享) | 上午选择题(识别连接池模式) |
七、内部状态 vs 外部状态:核心对比
| 对比项 | 内部状态(Intrinsic State) | 外部状态(Extrinsic State) |
|---|---|---|
| 存储位置 | 存储在ConcreteFlyweight内部 | 不存储在享元对象中,由Client传入 |
| 是否共享 | ✅可共享,相同的内部状态对应同一个享元对象 | ❌不可共享,每个使用场景不同 |
| 变化频率 | 不变化(创建时确定) | 每次调用都可能变化 |
| 典型例子 | 字符编码、棋子颜色、连接配置 | 字符位置、棋子坐标、当前使用者 |
| 软考考点 | 通过工厂缓存,确保相同内部状态只创建一个对象 | 通过operation()参数传入,使用时临时确定 |
软考记忆口诀:内部状态是 "身份证"(固定不变,可共享),外部状态是 "当前位置"(随时变化,不可共享)。
八、软考高频考点与易混淆辨析
8.1 高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 结构型模式(GoF 23 正式成员) |
| 核心角色 | Flyweight(抽象享元)、ConcreteFlyweight(具体享元)、FlyweightFactory(享元工厂)、Client(客户端) |
| 核心思想 | 将对象状态拆分为内部状态(可共享)和外部状态(不可共享),通过共享减少对象数量 |
| 内部状态 | 存储在享元对象内部,由工厂通过 Map 缓存共享 |
| 外部状态 | 由客户端在使用时通过参数传入 |
| 享元工厂 | 维护享元池(Map),getFlyweight(key)方法确保相同 key 只创建一个对象 |
| 与单例区别 | 单例:整个系统只有一个实例;享元:相同内部状态的对象共享一个实例,不同内部状态的对象各自独立 |
| 与原型区别 | 原型:通过clone()复制现有对象;享元:通过工厂共享已有对象,不创建新对象 |
| 适用场景 | ① 大量相似对象 ② 对象的大部分状态可外部化 ③ 需要缓存/池化(如连接池、线程池) |
8.2 易混淆辨析:享元 vs 单例 vs 原型
| 对比项 | 享元模式 | 单例模式 | 原型模式 |
|---|---|---|---|
| 对象数量 | 有限个(每种内部状态一个) | 只有一个 | 任意多个(每次 clone 一个新对象) |
| 创建方式 | 工厂管理,从池中获取 | 静态实例或 DCL | clone()复制现有对象 |
| 状态特点 | 内部状态共享,外部状态传入 | 无内外状态之分 | 复制原型当前的所有状态 |
| 目的 | 减少对象数量,节省内存 | 确保全局唯一 | 基于现有对象快速创建新对象 |
| 典型应用 | 字符渲染、棋子、连接池 | 配置管理器、日志管理器 | 对象复制、模板克隆 |
| 软考判断 | 工厂 + Map + 共享对象 | getInstance()+ 私有构造器 | clone()+Cloneable |
关键区分:单例是 "只有一个",享元是 "相同内部状态的共享一个",原型是 "复制一个"。
九、真题风格模拟与代码填空
模拟题 1(上午选择题)
以下关于享元模式的叙述中,正确的是()。
A. 享元模式属于创建型模式,主要用于创建大量细粒度的对象
B. 在享元模式中,对象的所有状态都可以被共享,客户端无需传入任何额外信息
C. 享元模式通过将对象状态分为内部状态和外部状态,运用共享技术减少对象数量
D. 享元模式与单例模式没有本质区别,都是确保系统中只有一个对象实例
答案:C
解析:
A 错误:享元属于结构型模式,不是创建型。它的目的不是创建对象,而是共享对象。
B 错误:享元模式中只有内部状态可共享,外部状态不可共享,必须由客户端传入。
C 正确:这是享元模式的核心定义。
D 错误:两者有本质区别。单例是全局只有一个实例;享元是相同内部状态的对象共享一个实例,不同内部状态可以有多个实例。
模拟题 2(下午代码填空 — 补全享元工厂)
某系统使用享元模式实现图标渲染。图标类型(
type)是内部状态,位置坐标(x, y)是外部状态。请补全(1)~(4)。
import java.util.HashMap; import java.util.Map; // Flyweight interface Icon { void draw(int x, int y); } // ConcreteFlyweight class ConcreteIcon implements Icon { private String type; public ConcreteIcon(String type) { this.type = type; } public void draw(int x, int y) { System.out.println("绘制 '" + type + "' 图标在 (" + x + "," + y + ")"); } } // FlyweightFactory class IconFactory { private Map<String, Icon> pool = new HashMap<>(); public Icon getIcon(String type) { Icon icon = (1)______; if (icon == null) { icon = (2)______; pool.put(type, icon); } return (3)______; } public int getIconCount() { return (4)______; } } public class Client { public static void main(String[] args) { IconFactory factory = new IconFactory(); Icon home1 = factory.getIcon("home"); home1.draw(10, 10); Icon home2 = factory.getIcon("home"); home2.draw(20, 20); System.out.println("内存中图标对象数: " + factory.getIconCount()); } }答案:
(1)
pool.get(type)(2)
new ConcreteIcon(type)(3)
icon(4)
pool.size()
阅卷要点:
(1) 必须是
pool.get(type),从享元池中获取已有对象。如果写new ConcreteIcon(type)会零分,因为这样就失去了共享的意义。(2) 必须是
new ConcreteIcon(type),池中不存在时创建新对象。(3) 必须是
icon,返回获取到的享元对象(可能是池中复用的,也可能是新创建的)。(4) 必须是
pool.size(),返回池中享元对象的数量。如果写pool.length()会零分(Map 没有 length 方法)。
模拟题 3(下午代码填空 — 区分内部状态与外部状态)
某系统使用享元模式实现树形对象的渲染。树的种类(松树、柳树)是内部状态,树的位置坐标(x, y)和年龄是外部状态。请补全(1)~(3)。
import java.util.HashMap; import java.util.Map; // Flyweight interface Tree { void display(int x, int y, int age); } // ConcreteFlyweight class ConcreteTree implements Tree { // (1)______:树的种类(可共享) private String type; public ConcreteTree(String type) { this.type = type; } public void display(int x, int y, int age) { System.out.println(type + " 树在 (" + x + "," + y + "),树龄 " + age + " 年"); } } // FlyweightFactory class TreeFactory { private Map<String, Tree> pool = new HashMap<>(); public Tree getTree(String type) { Tree tree = pool.get(type); if (tree == null) { tree = new ConcreteTree(type); pool.put(type, tree); } return tree; } } public class Client { public static void main(String[] args) { TreeFactory factory = new TreeFactory(); for (int i = 0; i < 1000; i++) { Tree tree = factory.getTree("松树"); // (2)______:传入外部状态 tree.display(i * 10, i * 5, (3)______); } } }答案:
(1)
内部状态(或intrinsicState/可共享状态)(2)
tree.display(i * 10, i * 5, i % 100 + 1)(或任何使用外部状态的调用)(3)
i % 100 + 1(或任何表示年龄的外部状态值)
阅卷要点:
(1) 必须明确写出 "内部状态"(或同义词)。如果写 "外部状态" 会零分。
(2) 需要体现客户端传入外部状态调用享元对象的方法。
tree.display(...)中必须包含位置坐标和年龄参数。(3) 是外部状态的具体值,可以是任意表达式,只要体现 "由客户端传入、不存储在享元对象中" 即可。
十、常见陷阱与注意事项
陷阱 1:误认为享元是创建型模式
享元属于结构型模式,因为它解决的是对象之间的结构关系(共享 vs 独立),而不是对象的创建问题。虽然涉及工厂创建对象,但核心目的是共享而非创建。软考上午题如果问"以下属于创建型模式的是",选项里出现"享元"不能选。
陷阱 2:将所有状态都放入内部状态
这是软考最高频的失分点。享元模式的核心是状态分离:
内部状态:可共享,存储在享元对象内部,由工厂管理。
外部状态:不可共享,由客户端传入。
如果将所有状态都放入内部状态,那就失去了共享的意义(每个对象都独一无二)。如果将所有状态都作为外部状态,那享元对象就变成了无状态对象,虽然可以共享,但意义不大。关键是找到可共享的状态和不可共享的状态的分界线。
陷阱 3:与单例模式混淆
| 享元模式 | 单例模式 |
|---|---|
| 相同内部状态的对象共享一个实例 | 整个系统只有一个实例 |
| 可以有多个享元对象(不同内部状态) | 只能有一个对象 |
| 通过工厂按 key 获取 | 通过getInstance()获取 |
如果类图里有 Map 缓存多个对象 → 享元。如果只有一个静态实例 → 单例。
陷阱 4:与原型模式混淆
| 享元模式 | 原型模式 |
|---|---|
| 从工厂获取已有的共享对象 | 通过clone()复制现有对象 |
| 不创建新对象(复用) | 创建新对象(复制) |
| 目的是减少对象数量 | 目的是保留状态快速创建 |
陷阱 5:享元工厂没有缓存机制
享元工厂的核心是享元池(Map 或 List)。如果工厂每次调用getFlyweight()都new一个新对象,那就不是享元模式,只是一个普通工厂。软考代码填空里,工厂里必须有Map或List作为缓存池,且getFlyweight()方法里必须有 "判断是否存在 → 不存在则创建 → 放入池中 → 返回" 的逻辑。
陷阱 6:外部状态存储在享元对象中
如果外部状态(如坐标、颜色、使用者)被存储在 ConcreteFlyweight 的字段中,那就违背了享元模式的设计。外部状态必须是无状态的,每次通过方法参数传入。如果 ConcreteFlyweight 里有private int x, y;这样的字段,且这些字段是外部状态,那就是错误的。
陷阱 7:连接池/线程池与享元的关系
连接池和线程池是享元模式的工程化应用,但不是纯享元模式。纯享元模式中,享元对象的数量等于内部状态种类的数量。而连接池限制了对象的总数(maxSize),与内部状态种类无关。软考中如果问 "连接池使用了哪种设计模式",答案是享元模式(或享元模式的变体)。
十一、总结
| 要点 | 内容 |
|---|---|
| 定义 | 运用共享技术有效地支持大量细粒度的对象 |
| 分类 | 结构型模式(GoF 23 正式成员) |
| 核心角色 | Flyweight(抽象享元)、ConcreteFlyweight(具体享元)、FlyweightFactory(享元工厂)、Client(客户端) |
| 核心思想 | 将对象状态分为内部状态(可共享)和外部状态(不可共享),通过共享减少对象数量 |
| 内部状态 | 存储在享元对象内部,由工厂通过 Map 缓存共享 |
| 外部状态 | 由客户端在使用时通过参数传入 |
| 享元工厂 | 维护享元池(Map),getFlyweight(key)确保相同 key 只创建一个对象 |
| 与单例区别 | 单例:全局只有一个实例;享元:相同内部状态共享一个实例,可以有多个 |
| 与原型区别 | 原型:复制现有对象;享元:复用已有对象 |
| 适用场景 | 大量相似对象、对象大部分状态可外部化、连接池/线程池 |
| 软考重点 | 区分内部状态与外部状态;享元工厂的缓存逻辑;与单例/原型的辨析 |
| 答题技巧 | 看到 "大量相似对象"、"共享"、"内部状态/外部状态"、"连接池" → 享元;看到工厂里有 Map 缓存 → 确认享元 |
系列预告:下一篇将讲代理模式—— 当需要控制对对象的访问时(如延迟加载、权限检查、远程调用),如何通过代理对象作为中间层,在不改变目标对象的前提下实现访问控制。咱们下回见。