← 返回题目列表

享元模式中的内部状态和外部状态有什么区别?

高频 中等 第 12 / 25 题 更新于 2026/07/28
享元模式内部状态外部状态

简化版

内部状态是对象可共享、相对固定、不随使用场景变化的状态;外部状态是不可共享、随上下文变化、由客户端在调用时传入的状态。享元模式能否成立,关键就在于能否正确拆分这两类状态。

详细版

以文本编辑器中的字符为例:

  • 内部状态:字符值、字体类型、基础样式等可复用信息。
  • 外部状态:字符所在行列、选中状态、上下文布局等变化信息。

享元对象只保存内部状态。外部状态不能放进享元对象里,否则共享对象会被不同调用方互相污染。

判断标准:

  • 多个对象是否相同?相同且稳定的部分倾向内部状态。
  • 是否随位置、用户、请求变化?变化部分倾向外部状态。
  • 是否可以被多个上下文同时安全使用?可以才适合内部状态。

完整版教学

一、内部状态是什么

内部状态是享元对象自身携带的、可以共享的状态。它通常在对象创建后不再变化。

例如围棋棋子:

  • 颜色。
  • 棋子图片。
  • 棋子渲染样式。

黑棋的颜色不会因为落在不同坐标而改变,所以颜色可以作为内部状态。

享元对象最好把内部状态设计成不可变字段:

class TreeType {
    private final String name;
    private final String texture;
    private final String color;
}

不可变能降低共享对象的线程安全风险。

二、外部状态是什么

外部状态是使用享元对象时才确定的上下文信息。

例如游戏中的树:

  • x 坐标。
  • y 坐标。
  • 当前生命值。
  • 是否被选中。

这些状态每棵树都不同,不适合放进共享的 TreeType 对象里。它们应该由客户端、上下文对象或独立数据结构维护。

三、为什么外部状态不能放进享元对象

如果把外部状态放进享元对象,会出现严重问题。

假设系统只有一个共享的黑棋对象,但它内部保存了坐标:

class BlackChessPiece {
    private int x;
    private int y;
}

第一次落在 (1, 2),第二次落在 (3, 4),同一个对象的坐标会被覆盖。之前棋子的位置就丢了。

所以享元对象应该代表“黑棋类型”,而不是“某一颗落在具体位置的黑棋”。

四、状态拆分的判断方法

可以用三个问题判断:

  1. 这个字段在很多对象之间是否重复?
  2. 这个字段是否不依赖具体使用位置或请求上下文?
  3. 共享这个字段是否不会导致数据串扰?

如果三个答案都为是,就适合作为内部状态。

如果字段会随上下文变化,就应该作为外部状态。

五、内部状态和外部状态的组合方式

享元对象在执行方法时,会把内部状态和外部状态组合起来使用。

class TreeType {
    private final String name;
    private final String texture;

    public void draw(int x, int y) {
        System.out.println("在 " + x + "," + y + " 绘制 " + name + ",纹理:" + texture);
    }
}

nametexture 来自享元对象,xy 来自外部调用。

六、常见错误划分

常见错误包括:

  • 把用户相关数据放进共享对象。
  • 把请求级临时状态放进共享对象。
  • 把会变化的计数器放进共享对象。
  • 把可变集合放进享元对象后又被多个地方修改。

这些都会让共享对象变成隐蔽的全局可变状态。

七、常见误区与追问

内部状态与外部状态的区分标准是“能否脱离一次使用上下文”。文本编辑器中字符字形、字体可作为内部状态,某个字符在第12行第8列的位置属于外部状态;同一字形可以在不同位置复用。外部状态由调用者传入,因此享元不能把它缓存成实例字段。

检查维度判定依据
内部状态稳定、重复、可安全共享
外部状态随位置或请求变化,由上下文提供
render(glyph共享, row=12, col=8)

心法:内部状态回答“它是什么”,外部状态回答“这次在哪里、为谁使用”。

  • 误区:只读字段都属于内部状态。 字段即使只读,若每个上下文值不同也无法有效共享。
  • 追问:外部状态能参与方法计算吗? 可以,它作为参数与内部状态共同决定最终行为。
  • 误区:内部状态一定是基本类型。 不可变纹理、字体描述等复杂对象也可共享。
  • 追问:如何判断拆分是否值得? 统计内部状态重复率,以及外部状态拆出后的参数和存储成本。
  • 追问:共享状态可以延迟加载吗? 可以,但加载结果和发布过程必须线程安全且语义稳定。

八、加强记忆

内部状态属于“对象类型”,外部状态属于“对象位置和上下文”。享元模式要共享的是类型,不是每一次具体使用;把上下文留在外面,享元对象才能安全复用。