GoF设计模式——享元模式

本文是【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 保证同一种图标只创建一次,成千上万个标记点共享同一个图标对象。

相关推荐
十五喵源码网1 小时前
基于springboot2+vue2的租房管理系统
java·毕业设计·springboot·论文笔记
摇滚侠1 小时前
IDEA 创建 Java 项目 手动整合 SSM 框架
java·ide·intellij-idea
源分享1 小时前
Java线程同步的多种实现方法(非常详细)
java·开发语言·jvm
Flittly1 小时前
【AgentScope Java新手村系列】(10)实战-多Agent天气助手
java·spring boot·spring
李少兄1 小时前
从原理到实战:Spring IoC/DI 核心知识体系与高频面试题全解
java·后端·spring
飞天狗1112 小时前
零基础JavaWeb入门——第五课第二小节:九大内置对象 · 第2个:response(响应对象)
java·开发语言
许彰午2 小时前
39_Java单元测试JUnit入门
java·junit·单元测试
shushangyun_2 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
JAVA9652 小时前
JAVA面试-JVM篇 03-JVM运行时数据区哪些是线程私有的哪些是共享的
java·jvm·面试