配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.flyweight

系列导航:Java 25 设计模式学习指南

上一篇:外观 · 下一篇:代理

一页排版里有许多字形,每个字形都有字符和坐标,但大量字形使用相同字体。若每个字形都重复持有一份复杂字体样式,内存可能主要消耗在重复信息上。

享元先把状态拆开:字体样式可以共享,字符和坐标属于一次具体绘制。共享对象应该避免保存调用者传入的坐标,否则后绘制的字形会污染先前字形。

从问题提炼设计意图

享元共享大量细粒度对象中的内在状态,把随上下文变化的外在状态交给调用方保存,并在操作时传入。

对象职责与协作关系

示例角色 职责
TextStyle 享元,保存不可变字体信息
StyleFactory 享元工厂,按键复用样式对象
Glyph 上下文,保存字符和坐标等外在状态
Demo 装配共享样式与多个独立上下文
classDiagram
    StyleFactory o-- TextStyle : shares by font
    Glyph --> TextStyle : shared style
    Demo ..> StyleFactory : requests

代码思路:变化应该落在哪个对象上

TextStyle 只保存 font。draw 的 symbol 和 x 都是参数,不会写入字段,因此一个样式对象可以服务多个不同位置的字形。

StyleFactory 用字体名作为缓存键,computeIfAbsent 在首次请求时创建对象,以后同键请求返回同一引用。这里比较对象身份具有意义,因为享元的目标确实包含共享,而不只是两个对象的值相等。

Glyph 保存自己的字符和 positionX,同时引用共享 TextStyle。draw 时把这些外在状态传给样式。测试先绘制 A,再绘制 B,再绘制 A,确认 B 的位置没有进入共享样式而覆盖 A。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.flyweight,源码目录为 src/main/java/com/hanserwei/patterns/flyweight/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

TextStyle.java

package com.hanserwei.patterns.flyweight;

import java.util.Objects;

/** 可以被多个字形共享的不可变内在状态. */
public final class TextStyle {
  /** 该样式使用的字体名称. */
  private final String font;

  /** 创建指定字体样式,仅允许同包工厂直接调用. */
  TextStyle(String font) {
    this.font = Objects.requireNonNull(font, "font");
  }

  /** 接收外在坐标并生成绘制说明,不保存坐标. */
  public String draw(char symbol, int x) {
    return font + ":" + symbol + "@" + x;
  }
}

StyleFactory.java

package com.hanserwei.patterns.flyweight;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/** 在单线程学习场景中按字体复用样式. */
public final class StyleFactory {
  /** 字体到共享样式的缓存;生命周期由工厂实例控制. */
  private final Map<String, TextStyle> styles = new HashMap<>();

  /** 返回相同字体的共享对象,首次请求时创建. */
  public TextStyle get(String font) {
    Objects.requireNonNull(font, "font");
    return styles.computeIfAbsent(font, TextStyle::new);
  }
}

Glyph.java

package com.hanserwei.patterns.flyweight;

import java.util.Objects;

/** 保存非共享字符与坐标,并引用共享样式的上下文. */
public final class Glyph {
  /** 当前字符. */
  private final char symbol;

  /** 当前横坐标,是外在状态. */
  private final int positionX;

  /** 与其他字形共享的样式. */
  private final TextStyle style;

  /** 装配外在状态和共享样式. */
  public Glyph(char symbol, int positionX, TextStyle style) {
    this.symbol = symbol;
    this.positionX = positionX;
    this.style = Objects.requireNonNull(style, "style");
  }

  /** 把当前外在状态传入共享对象完成绘制. */
  public String draw() {
    return style.draw(symbol, positionX);
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.flyweight;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    StyleFactory factory = new StyleFactory();
    System.out.println(new Glyph('A', 10, factory.get("Serif")).draw());
    System.out.println(new Glyph('B', 20, factory.get("Serif")).draw());
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runFlyweight
./gradlew test --tests 'com.hanserwei.patterns.flyweight.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

Serif:A@10
Serif:B@20

用测试确认模式的行为

相同字体复用同一对象,不同字体得到不同对象;不同字形坐标始终独立。该测试证明共享结构,不等于完成内存收益基准测试。

对应测试位于 src/test/java/com/hanserwei/patterns/flyweight/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 文档排版、地图标记和游戏场景中有大量重复样式。
  • 语法树或词法符号中存在可安全共享的不可变描述。
  • 经过测量,重复内在状态确实构成显著内存成本。

适用边界与容易踩的坑

共享必须先有可靠的键。如果样式还包含字号、颜色和粗体,键只使用字体名就会错误复用不兼容状态;应把所有影响内在状态的维度纳入不可变键。

缓存也会占内存。工厂长期存活、键无限增长时,享元可能变成泄漏来源,需要容量、淘汰或生命周期设计。不要在没有测量时先制造一个永久全局缓存。

本例的 HashMap 工厂仅面向单线程;多线程共享时需要并发容器或同步,而且享元自身仍应保持不可变。用并发 map 只保证缓存操作的并发语义,不会自动让其中的可变对象安全。

与相近模式比较

缓存代理通常是为了省掉昂贵访问,并保持目标接口;享元关注内存共享和内外状态划分。对象池还涉及可变对象的借还与重置,不能直接等同于不可变享元。

动手练习

加入字号和颜色,设计完整样式键,并测试不同字号不能误共享。创建大量字形后比较“每个字形都新建样式”和“通过工厂共享”两种对象数量,说明何时共享成本可能超过节省。

系列导航:Java 25 设计模式学习指南

上一篇:外观 · 下一篇:代理