【软考设计模式】享元模式:共享技术支撑海量细粒度对象与代码填空精讲

【软考设计模式】享元模式:共享技术支撑海量细粒度对象与代码填空精讲

【软考设计模式】享元模式:共享技术支撑海量细粒度对象与代码填空精讲

系列定位:软考软件设计师 / 系统架构设计师 — 结构型模式专题第 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 一个新对象)
创建方式工厂管理,从池中获取静态实例或 DCLclone()复制现有对象
状态特点内部状态共享,外部状态传入无内外状态之分复制原型当前的所有状态
目的减少对象数量,节省内存确保全局唯一基于现有对象快速创建新对象
典型应用字符渲染、棋子、连接池配置管理器、日志管理器对象复制、模板克隆
软考判断工厂 + 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一个新对象,那就不是享元模式,只是一个普通工厂。软考代码填空里,工厂里必须有MapList作为缓存池,且getFlyweight()方法里必须有 "判断是否存在 → 不存在则创建 → 放入池中 → 返回" 的逻辑。

陷阱 6:外部状态存储在享元对象中

如果外部状态(如坐标、颜色、使用者)被存储在 ConcreteFlyweight 的字段中,那就违背了享元模式的设计。外部状态必须是无状态的,每次通过方法参数传入。如果 ConcreteFlyweight 里有private int x, y;这样的字段,且这些字段是外部状态,那就是错误的。

陷阱 7:连接池/线程池与享元的关系

连接池和线程池是享元模式的工程化应用,但不是纯享元模式。纯享元模式中,享元对象的数量等于内部状态种类的数量。而连接池限制了对象的总数(maxSize),与内部状态种类无关。软考中如果问 "连接池使用了哪种设计模式",答案是享元模式(或享元模式的变体)。


十一、总结

要点内容
定义运用共享技术有效地支持大量细粒度的对象
分类结构型模式(GoF 23 正式成员)
核心角色Flyweight(抽象享元)、ConcreteFlyweight(具体享元)、FlyweightFactory(享元工厂)、Client(客户端)
核心思想将对象状态分为内部状态(可共享)和外部状态(不可共享),通过共享减少对象数量
内部状态存储在享元对象内部,由工厂通过 Map 缓存共享
外部状态由客户端在使用时通过参数传入
享元工厂维护享元池(Map),getFlyweight(key)确保相同 key 只创建一个对象
与单例区别单例:全局只有一个实例;享元:相同内部状态共享一个实例,可以有多个
与原型区别原型:复制现有对象;享元:复用已有对象
适用场景大量相似对象、对象大部分状态可外部化、连接池/线程池
软考重点区分内部状态与外部状态;享元工厂的缓存逻辑;与单例/原型的辨析
答题技巧看到 "大量相似对象"、"共享"、"内部状态/外部状态"、"连接池" → 享元;看到工厂里有 Map 缓存 → 确认享元

系列预告:下一篇将讲代理模式—— 当需要控制对对象的访问时(如延迟加载、权限检查、远程调用),如何通过代理对象作为中间层,在不改变目标对象的前提下实现访问控制。咱们下回见。