← 返回题目列表

如何手写一个享元模式?Java 代码怎么实现?

高频 中等 第 4 / 25 题 更新于 2026/07/28
享元模式Java代码实现

简化版

手写享元模式一般分三步:定义享元接口,把可共享的内部状态放入具体享元类,再用享元工厂缓存对象。客户端从工厂获取享元,并在调用时传入外部状态。

详细版

以“游戏地图树木渲染”为例:

  • TreeType 是享元对象,保存树种、颜色、纹理等内部状态。
  • TreeFactory 是享元工厂,根据树种 key 复用 TreeType
  • Tree 或地图对象保存坐标等外部状态。
  • 渲染时调用 treeType.draw(x, y)

这样即使地图上有 10 万棵树,也只需要少量 TreeType 实例。

完整版教学

一、定义享元对象

享元对象保存内部状态:

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

    public TreeType(String name, String color, String texture) {
        this.name = name;
        this.color = color;
        this.texture = texture;
    }

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

这里 namecolortexture 是大量树木可共享的信息。

二、编写享元工厂

享元工厂负责缓存和复用:

class TreeFactory {
    private static final Map<String, TreeType> CACHE = new HashMap<>();

    public static TreeType getTreeType(String name, String color, String texture) {
        String key = name + ":" + color + ":" + texture;
        return CACHE.computeIfAbsent(key, k -> new TreeType(name, color, texture));
    }

    public static int size() {
        return CACHE.size();
    }
}

同样的树种、颜色和纹理只创建一次。后续请求直接返回缓存中的享元对象。

三、定义外部状态对象

外部状态可以放在普通对象中:

class Tree {
    private final int x;
    private final int y;
    private final TreeType type;

    public Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    public void draw() {
        type.draw(x, y);
    }
}

Tree 不是享元对象,它代表地图上的一棵具体树,保存坐标。TreeType 才是享元。

四、客户端使用

public class FlyweightDemo {
    public static void main(String[] args) {
        List<Tree> trees = new ArrayList<>();

        TreeType pine = TreeFactory.getTreeType("松树", "绿色", "pine.png");
        TreeType oak = TreeFactory.getTreeType("橡树", "深绿", "oak.png");

        trees.add(new Tree(10, 20, pine));
        trees.add(new Tree(30, 40, pine));
        trees.add(new Tree(50, 60, oak));

        for (Tree tree : trees) {
            tree.draw();
        }

        System.out.println("享元对象数量:" + TreeFactory.size());
    }
}

这里有 3 棵树,但只有 2 个树类型享元对象。

五、为什么不是把 Tree 全部共享

地图上的每棵 Tree 坐标不同,不能全部共享。真正重复的是 TreeType

如果把 Tree 也做成共享对象,就会遇到坐标覆盖问题。享元模式不是盲目共享所有对象,而是共享对象中可复用的部分。

六、并发环境怎么处理

如果享元工厂会被多线程访问,HashMap 不是线程安全的。可以改用 ConcurrentHashMap

class SafeTreeFactory {
    private static final ConcurrentHashMap<String, TreeType> CACHE = new ConcurrentHashMap<>();

    public static TreeType getTreeType(String name, String color, String texture) {
        String key = name + ":" + color + ":" + texture;
        return CACHE.computeIfAbsent(key, k -> new TreeType(name, color, texture));
    }
}

同时,享元对象内部状态尽量保持不可变,避免共享后被并发修改。

七、常见误区与追问

实现时应把外部状态放在方法参数,而不是写回共享对象。假设1000棵树只有3种纹理,工厂最多创建3个 TreeType,每棵树的位置 x、y 保存在场景节点;绘制时调用 type.draw(x,y)。若 draw 修改 type 的坐标字段,并发绘制就会互相污染。

检查维度判定依据
TreeType纹理、颜色等内部状态
Tree坐标等每个实例独有的外部状态
1000 Tree -> 3 shared TreeType

记忆钩子:享元对象带“模板”,调用现场带“位置”。

  • 误区:享元类必须实现 clone。 共享依赖复用同一实例,不需要通过克隆创建副本。
  • 追问:外部状态应该由谁保存? 通常由上下文对象、集合记录或调用者计算后传入。
  • 误区:享元接口可以暴露 setter。 可变 setter 会破坏共享安全,优先构造后不可变。
  • 追问:工厂为何返回接口而非具体类? 可隐藏共享策略,并允许非共享的 ConcreteFlyweight。
  • 追问:如何估算节省量? 比较 N 个完整对象与 N 个外部状态加 K 个共享内部对象的总内存。

八、加强记忆

手写享元模式记住四步:找重复状态,做成不可变享元;建工厂,用 Map 缓存;外部状态留给客户端;调用时把外部状态传给享元方法。