Skip to content

享元模式(Flyweight Pattern) ​

一、模式概述 ​

享元模式是一种结构型设计模式,它通过共享技术有效地支持大量细粒度的对象。享元模式将对象的状态分为内部状态(intrinsic state) 和外部状态(extrinsic state),内部状态可以共享,外部状态则由客户端维护。

核心思想:如果一个对象的很多状态是相同的,就把这些状态提取出来作为共享对象(享元),避免创建大量相同或相似的对象,从而节约内存。

二、核心概念 ​

概念说明
内部状态(Intrinsic State)存储在享元对象内部,不随环境变化而变化,可以共享
外部状态(Extrinsic State)不能共享的状态,随环境变化而变化,由客户端维护
享元工厂管理享元池,负责创建和管理可共享的享元对象
享元池(Pool)存放享元对象的容器,通常是 HashMap

内部状态 vs 外部状态 ​

举个例子:文字编辑器中的字符对象

内部状态(可共享):字符形状 'A'、'B'、'C'(字体、大小、样式)
外部状态(不可共享):字符在屏幕上的位置 (x, y)、颜色、是否选中

字符 'A' 在文章中可能出现100次,但只需要创建1个享元对象,
100个不同的位置由外部状态维护。

三、示例1:角色渲染系统(游戏场景) ​

问题场景 ​

游戏中的角色有很多相同的外观(皮肤),但每个角色在地图上的位置、血量、装备不同。

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

// ========== 享元接口 ==========

/**
 * 角色外观接口
 */
interface CharacterAppearance {
    void display(int x, int y, int health);  // 外部状态作为参数
}

// ========== 具体享元类 ==========

/**
 * 战士外观(内部状态)
 */
class WarriorAppearance implements CharacterAppearance {
    private final String skin = "战士皮肤";
    private final String weapon = "大剑";
    private final String animation = "挥剑动画";

    @Override
    public void display(int x, int y, int health) {
        System.out.println("  [战士] 位置(" + x + "," + y + ") 血量:" + health
                + " 外观:" + skin + " 武器:" + weapon + " 动画:" + animation);
    }
}

/**
 * 法师外观(内部状态)
 */
class MageAppearance implements CharacterAppearance {
    private final String skin = "法师皮肤";
    private final String weapon = "法杖";
    private final String animation = "施法动画";

    @Override
    public void display(int x, int y, int health) {
        System.out.println("  [法师] 位置(" + x + "," + y + ") 血量:" + health
                + " 外观:" + skin + " 武器:" + weapon + " 动画:" + animation);
    }
}

/**
 * 弓箭手外观(内部状态)
 */
class ArcherAppearance implements CharacterAppearance {
    private final String skin = "弓箭手皮肤";
    private final String weapon = "长弓";
    private final String animation = "射箭动画";

    @Override
    public void display(int x, int y, int health) {
        System.out.println("  [弓箭手] 位置(" + x + "," + y + ") 血量:" + health
                + " 外观:" + skin + " 武器:" + weapon + " 动画:" + animation);
    }
}

// ========== 享元工厂 ==========

/**
 * 角色外观工厂
 * 管理享元池,避免重复创建相同外观的对象
 */
class CharacterFactory {
    private static final Map<String, CharacterAppearance> appearancePool = new HashMap<>();

    public static CharacterAppearance getAppearance(String type) {
        if (!appearancePool.containsKey(type)) {
            switch (type) {
                case "warrior":
                    appearancePool.put(type, new WarriorAppearance());
                    System.out.println("[享元工厂] 创建新的战士外观对象");
                    break;
                case "mage":
                    appearancePool.put(type, new MageAppearance());
                    System.out.println("[享元工厂] 创建新的法师外观对象");
                    break;
                case "archer":
                    appearancePool.put(type, new ArcherAppearance());
                    System.out.println("[享元工厂] 创建新的弓箭手外观对象");
                    break;
                default:
                    throw new IllegalArgumentException("未知角色类型: " + type);
            }
        } else {
            System.out.println("[享元工厂] 复用已有的 " + type + " 外观对象");
        }
        return appearancePool.get(type);
    }

    public static int getPoolSize() {
        return appearancePool.size();
    }
}

// ========== 角色类(包含外部状态) ==========

/**
 * 游戏角色
 * 外部状态(x, y, health)由角色自身维护
 */
class GameCharacter {
    private String name;
    private int x, y;          // 外部状态:位置
    private int health;         // 外部状态:血量
    private CharacterAppearance appearance;  // 共享的内部状态(享元)

    public GameCharacter(String name, String type, int x, int y, int health) {
        this.name = name;
        this.x = x;
        this.y = y;
        this.health = health;
        this.appearance = CharacterFactory.getAppearance(type);
    }

    public void render() {
        System.out.print("角色[" + name + "] ");
        appearance.display(x, y, health);
    }

    public void moveTo(int newX, int newY) {
        this.x = newX;
        this.y = newY;
        System.out.println("  角色 " + name + " 移动到 (" + newX + "," + newY + ")");
    }
}

// ========== 客户端代码 ==========

public class GameDemo {
    public static void main(String[] args) {
        System.out.println("===== 创建角色 =====");
        // 创建大量角色,但享元对象只有3个
        GameCharacter warrior1 = new GameCharacter("战士1号", "warrior", 10, 20, 100);
        GameCharacter warrior2 = new GameCharacter("战士2号", "warrior", 30, 40, 80);
        GameCharacter warrior3 = new GameCharacter("战士3号", "warrior", 50, 60, 120);
        GameCharacter mage1    = new GameCharacter("法师1号", "mage", 15, 25, 60);
        GameCharacter mage2    = new GameCharacter("法师2号", "mage", 35, 45, 55);
        GameCharacter archer1  = new GameCharacter("弓箭手1号", "archer", 20, 30, 70);
        GameCharacter archer2  = new GameCharacter("弓箭手2号", "archer", 40, 50, 75);

        System.out.println("\n===== 渲染角色 =====");
        warrior1.render();
        warrior2.render();
        warrior3.render();
        mage1.render();
        mage2.render();
        archer1.render();
        archer2.render();

        System.out.println("\n===== 角色数量统计 =====");
        System.out.println("角色总数: 7");
        System.out.println("享元对象数(外观): " + CharacterFactory.getPoolSize());
        System.out.println("节省了 " + (7 - CharacterFactory.getPoolSize()) + " 个对象的内存!");

        System.out.println("\n===== 角色移动 =====");
        warrior1.moveTo(100, 200);
        warrior1.render();
    }
}

运行结果 ​

===== 创建角色 =====
[享元工厂] 创建新的战士外观对象
[享元工厂] 复用已有的 warrior 外观对象
[享元工厂] 复用已有的 warrior 外观对象
[享元工厂] 创建新的法师外观对象
[享元工厂] 复用已有的 mage 外观对象
[享元工厂] 创建新的弓箭手外观对象
[享元工厂] 复用已有的 archer 外观对象

===== 渲染角色 =====
角色[战士1号] [战士] 位置(10,20) 血量:100 外观:战士皮肤 武器:大剑 动画:挥剑动画
角色[战士2号] [战士] 位置(30,40) 血量:80 外观:战士皮肤 武器:大剑 动画:挥剑动画
...
===== 角色数量统计 =====
角色总数: 7
享元对象数(外观): 3
节省了 4 个对象的内存!

四、示例2:数据库连接池 ​

java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

// ========== 享元对象:数据库连接 ==========

/**
 * 数据库连接(享元对象)
 * 连接配置(URL、用户名)是内部状态,可复用
 * 事务隔离级别、超时时间等是外部状态
 */
class DatabaseConnection {
    private final String url;        // 内部状态
    private final String username;   // 内部状态
    private boolean inUse = false;   // 连接使用状态

    public DatabaseConnection(String url, String username) {
        this.url = url;
        this.username = username;
        System.out.println("[连接池] 创建新连接 -> " + url);
    }

    /**
     * 使用连接(外部状态通过参数传入)
     */
    public void execute(String sql, int timeout) {
        System.out.println("  执行SQL: " + sql + " (超时:" + timeout + "ms)");
    }

    public boolean isInUse() {
        return inUse;
    }

    public void setInUse(boolean inUse) {
        this.inUse = inUse;
    }

    public void close() {
        this.inUse = false;
        System.out.println("  连接已归还 -> " + url);
    }
}

// ========== 享元工厂:连接池 ==========

/**
 * 数据库连接池(享元工厂)
 * 管理一组可复用的数据库连接
 */
class ConnectionPool {
    private final BlockingQueue<DatabaseConnection> pool;
    private final String url;
    private final String username;
    private final int maxSize;
    private int currentSize = 0;

    public ConnectionPool(String url, String username, int maxSize) {
        this.url = url;
        this.username = username;
        this.maxSize = maxSize;
        this.pool = new LinkedBlockingQueue<>(maxSize);
        System.out.println("[连接池] 初始化连接池,最大连接数: " + maxSize);
    }

    /**
     * 获取连接(外部状态通过参数传入)
     */
    public DatabaseConnection getConnection(int timeout) throws InterruptedException {
        // 尝试从池中获取空闲连接
        DatabaseConnection conn = pool.poll(timeout, TimeUnit.MILLISECONDS);
        if (conn != null) {
            conn.setInUse(true);
            System.out.println("[连接池] 复用已有连接 (当前使用: " + currentSize + "/" + maxSize + ")");
            return conn;
        }

        // 池中没有空闲连接,且未达到最大连接数,创建新连接
        if (currentSize < maxSize) {
            conn = new DatabaseConnection(url, username);
            conn.setInUse(true);
            currentSize++;
            return conn;
        }

        // 已达到最大连接数
        System.out.println("[连接池] 连接池已满,等待空闲连接...");
        return null;
    }

    /**
     * 归还连接
     */
    public void releaseConnection(DatabaseConnection conn) {
        if (conn != null) {
            conn.close();
            pool.offer(conn);
        }
    }

    public int getPoolSize() {
        return currentSize;
    }
}

// ========== 客户端代码 ==========

public class ConnectionPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ConnectionPool pool = new ConnectionPool(
            "jdbc:mysql://localhost:3306/mydb", "root", 5
        );

        System.out.println("\n===== 获取连接 =====");
        DatabaseConnection conn1 = pool.getConnection(3000);
        DatabaseConnection conn2 = pool.getConnection(3000);
        DatabaseConnection conn3 = pool.getConnection(3000);

        System.out.println("\n===== 使用连接 =====");
        conn1.execute("SELECT * FROM users", 5000);
        conn2.execute("INSERT INTO orders VALUES(...)", 5000);
        conn3.execute("UPDATE products SET stock=100", 5000);

        System.out.println("\n===== 归还连接 =====");
        pool.releaseConnection(conn1);
        pool.releaseConnection(conn2);

        System.out.println("\n===== 重新获取 =====");
        DatabaseConnection conn4 = pool.getConnection(3000);  // 复用conn1
        conn4.execute("DELETE FROM temp_data", 5000);

        System.out.println("\n===== 连接池状态 =====");
        System.out.println("连接池大小: " + pool.getPoolSize());
    }
}

五、String 源码中的享元模式 ​

Java 的 String 类型就使用了享元模式:

java
public class StringFlyweightDemo {
    public static void main(String[] args) {
        // 字符串字面量使用字符串常量池(享元池)
        String s1 = "hello";
        String s2 = "hello";
        String s3 = new String("hello");

        System.out.println("s1 == s2: " + (s1 == s2));   // true(同一个对象)
        System.out.println("s1 == s3: " + (s1 == s3));   // false(不同对象)

        // 使用 intern() 方法将字符串放入常量池
        String s4 = s3.intern();
        System.out.println("s1 == s4: " + (s1 == s4));   // true

        // Integer 缓存池 -128~127
        Integer a = 127;
        Integer b = 127;
        Integer c = new Integer(127);
        System.out.println("a == b: " + (a == b));   // true(缓存池复用)
        System.out.println("a == c: " + (a == c));   // false(新对象)

        Integer d = 128;
        Integer e = 128;
        System.out.println("d == e: " + (d == e));   // false(超出缓存范围)
    }
}

六、UML 类图 ​

┌────────────────────┐        ┌──────────────────┐
│   FlyweightFactory  │──creates──▶│ Flyweight (接口)    │
│   - pool: Map       │        │   + operation()  │
│   + getFlyweight()  │        └───────┬──────────┘
└────────────────────┘                │
                               ┌──────┴──────┐
                        ┌──────┴───┐  ┌──────┴───┐
                        │Concrete  │  │Concrete  │
                        │FlyweightA│  │FlyweightB│
                        └──────────┘  └──────────┘

Client ──▶ FlyweightFactory ──▶ Flyweight(共享)
              │
              │ 管理
              ▼
         ┌─────────┐
         │ Flyweight│  ← 享元池(HashMap)
         │   Pool   │
         └─────────┘

七、优缺点分析 ​

优点 ​

  1. 大幅减少内存占用:相同对象只创建一次,节省大量内存
  2. 提高性能:减少了对象创建和垃圾回收的开销
  3. 内部状态共享:核心数据只在享元对象中存储一份

缺点 ​

  1. 增加复杂度:需要分离内部状态和外部状态,增加了设计的复杂度
  2. 线程安全问题:享元对象被多个客户端共享,需要注意线程安全
  3. 外部状态开销:外部状态需要每次传入,可能增加参数传递的开销

八、使用场景 ​

  • 系统存在大量相似对象:对象的大部分状态可以外部化
  • 缓冲池:线程池、连接池、对象池等
  • 文本编辑器:大量相同字符的渲染
  • 游戏开发:大量相同外观的游戏对象
  • Java 基础类型缓存:Integer、Long 等包装类的缓存池

九、最佳实践 ​

  1. 合理划分内部状态和外部状态:这是享元模式成功的关键
  2. 注意线程安全:享元对象被共享使用,需要考虑并发访问
  3. 不要过度使用:如果对象数量不多,享元模式反而增加了复杂度
  4. 结合对象池使用:享元模式常与对象池技术结合使用

总结:享元模式的核心是"共享细粒度对象"。当系统中存在大量相似对象时,通过提取可共享的内部状态,可以显著减少内存占用和提高性能。在使用时,关键是正确划分内部状态(可共享)和外部状态(不可共享)。