【设计模式】享元模式(Flyweight)大白话讲解!

享元模式(Flyweight)大白话讲解

一句话概括

就像字母积木:有限的字母积木可以拼出无数单词,不用为每个单词都造新积木


现实生活比喻

场景1:字母积木

  • 有限字母:26个字母积木
  • 无限组合:用这些字母可以拼出所有英文单词
  • 节省材料:不需要为每个单词都制造一套新字母

场景2:围棋棋子

  • 棋子类型:只有黑棋和白棋两种
  • 棋盘位置:361个位置可以放棋子
  • 节省资源:不需要为每个位置都创建新棋子对象

完整代码示例

场景1:文本编辑器中的字符处理

java 复制代码
/**
 * 享元模式 - 文本编辑器字符处理
 */
public class Main {
    public static void main(String[] args) {
        System.out.println("=== 文本编辑器字符处理 ===");
        
        // 创建字符工厂
        CharacterFactory factory = new CharacterFactory();
        
        // 编辑文档
        String document = "Hello World! Hello Design Patterns!";
        
        System.out.println("文档内容: " + document);
        System.out.println("文档长度: " + document.length() + " 个字符");
        
        // 处理每个字符
        List<TextCharacter> characters = new ArrayList<>();
        for (int i = 0; i < document.length(); i++) {
            char c = document.charAt(i);
            TextCharacter character = factory.getCharacter(c);
            characters.add(character);
        }
        
        // 显示字符信息
        System.out.println("\n=== 字符使用统计 ===");
        System.out.println("实际创建的字符对象数量: " + factory.getCharacterCount());
        System.out.println("文档中字符总数: " + characters.size());
        System.out.println("节省了 " + (characters.size() - factory.getCharacterCount()) + " 个对象");
        
        // 渲染文档
        System.out.println("\n=== 渲染文档 ===");
        for (int i = 0; i < characters.size(); i++) {
            characters.get(i).render(i, 0); // 位置作为外部状态
        }
    }
}

/**
 * 享元接口 - 字符
 */
interface TextCharacter {
    void render(int positionX, int positionY);  // 位置是外部状态
    char getSymbol();  // 获取字符符号
}

/**
 * 具体享元 - 具体字符
 */
class Character implements TextCharacter {
    private final char symbol;  // 内部状态 - 不变的部分
    private final String font;  // 内部状态 - 字体
    private final int size;     // 内部状态 - 字号
    
    public Character(char symbol) {
        this.symbol = symbol;
        this.font = "Arial";    // 假设所有字符使用相同字体
        this.size = 12;         // 假设所有字符使用相同字号
    }
    
    @Override
    public void render(int positionX, int positionY) {
        System.out.println("字符 '" + symbol + "' 在位置 (" + positionX + ", " + positionY + 
                          ") 字体: " + font + " 字号: " + size);
    }
    
    @Override
    public char getSymbol() {
        return symbol;
    }
}

/**
 * 享元工厂 - 字符工厂
 */
class CharacterFactory {
    private Map<Character, TextCharacter> characters = new HashMap<>();
    
    // 获取字符对象 - 核心方法
    public TextCharacter getCharacter(char symbol) {
        // 如果字符已经存在,直接返回
        if (characters.containsKey(symbol)) {
            return characters.get(symbol);
        }
        
        // 否则创建新字符并缓存
        TextCharacter character = new Character(symbol);
        characters.put(symbol, character);
        System.out.println("创建新字符对象: '" + symbol + "'");
        return character;
    }
    
    // 获取已创建的字符数量
    public int getCharacterCount() {
        return characters.size();
    }
}

运行结果

复制代码
=== 文本编辑器字符处理 ===
文档内容: Hello World! Hello Design Patterns!
文档长度: 37 个字符
创建新字符对象: 'H'
创建新字符对象: 'e'
创建新字符对象: 'l'
创建新字符对象: 'o'
创建新字符对象: ' '
创建新字符对象: 'W'
创建新字符对象: 'r'
创建新字符对象: 'd'
创建新字符对象: '!'
创建新字符对象: 'D'
创建新字符对象: 's'
创建新字符对象: 'i'
创建新字符对象: 'g'
创建新字符对象: 'n'
创建新字符对象: 'P'
创建新字符对象: 'a'
创建新字符对象: 't'

=== 字符使用统计 ===
实际创建的字符对象数量: 17
文档中字符总数: 37
节省了 20 个对象

=== 渲染文档 ===
字符 'H' 在位置 (0, 0) 字体: Arial 字号: 12
字符 'e' 在位置 (1, 0) 字体: Arial 字号: 12
字符 'l' 在位置 (2, 0) 字体: Arial 字号: 12
字符 'l' 在位置 (3, 0) 字体: Arial 字号: 12
字符 'o' 在位置 (4, 0) 字体: Arial 字号: 12
字符 ' ' 在位置 (5, 0) 字体: Arial 字号: 12
字符 'W' 在位置 (6, 0) 字体: Arial 字号: 12
字符 'o' 在位置 (7, 0) 字体: Arial 字号: 12
字符 'r' 在位置 (8, 0) 字体: Arial 字号: 12
字符 'l' 在位置 (9, 0) 字体: Arial 字号: 12
字符 'd' 在位置 (10, 0) 字体: Arial 字号: 12
字符 '!' 在位置 (11, 0) 字体: Arial 字号: 12
...

场景2:围棋游戏

java 复制代码
/**
 * 享元模式 - 围棋游戏
 */
public class GoGame {
    public static void main(String[] args) {
        System.out.println("=== 围棋游戏 ===");
        
        ChessFactory factory = new ChessFactory();
        GoBoard board = new GoBoard();
        
        // 下棋
        board.placeChess(factory.getChess("黑棋"), 3, 4);
        board.placeChess(factory.getChess("白棋"), 4, 4);
        board.placeChess(factory.getChess("黑棋"), 5, 5);
        board.placeChess(factory.getChess("白棋"), 4, 5);
        board.placeChess(factory.getChess("黑棋"), 3, 5);
        
        // 显示棋盘
        board.display();
        
        // 统计信息
        System.out.println("\n=== 棋子使用统计 ===");
        System.out.println("棋盘棋子数量: " + board.getChessCount());
        System.out.println("实际创建的棋子对象: " + factory.getChessCount());
        System.out.println("节省了 " + (board.getChessCount() - factory.getChessCount()) + " 个对象");
    }
}

/**
 * 享元接口 - 棋子
 */
interface Chess {
    String getColor();
    void display(int x, int y);
}

/**
 * 具体享元 - 具体棋子
 */
class GoChess implements Chess {
    private final String color;  // 内部状态 - 颜色
    
    public GoChess(String color) {
        this.color = color;
    }
    
    @Override
    public String getColor() {
        return color;
    }
    
    @Override
    public void display(int x, int y) {
        System.out.println(color + " 落在位置 (" + x + ", " + y + ")");
    }
}

/**
 * 享元工厂 - 棋子工厂
 */
class ChessFactory {
    private Map<String, Chess> chessMap = new HashMap<>();
    
    public Chess getChess(String color) {
        if (chessMap.containsKey(color)) {
            return chessMap.get(color);
        }
        
        Chess chess = new GoChess(color);
        chessMap.put(color, chess);
        System.out.println("创建新棋子: " + color);
        return chess;
    }
    
    public int getChessCount() {
        return chessMap.size();
    }
}

/**
 * 棋盘 - 管理外部状态(位置)
 */
class GoBoard {
    private List<ChessPosition> positions = new ArrayList<>();
    
    public void placeChess(Chess chess, int x, int y) {
        positions.add(new ChessPosition(chess, x, y));
    }
    
    public void display() {
        System.out.println("\n=== 棋盘状态 ===");
        for (ChessPosition position : positions) {
            position.display();
        }
    }
    
    public int getChessCount() {
        return positions.size();
    }
}

/**
 * 棋子位置 - 外部状态
 */
class ChessPosition {
    private Chess chess;  // 享元对象
    private int x;        // 外部状态 - X坐标
    private int y;        // 外部状态 - Y坐标
    
    public ChessPosition(Chess chess, int x, int y) {
        this.chess = chess;
        this.x = x;
        this.y = y;
    }
    
    public void display() {
        chess.display(x, y);
    }
}

运行结果

复制代码
=== 围棋游戏 ===
创建新棋子: 黑棋
创建新棋子: 白棋

=== 棋盘状态 ===
黑棋 落在位置 (3, 4)
白棋 落在位置 (4, 4)
黑棋 落在位置 (5, 5)
白棋 落在位置 (4, 5)
黑棋 落在位置 (3, 5)

=== 棋子使用统计 ===
棋盘棋子数量: 5
实际创建的棋子对象: 2
节省了 3 个对象

享元模式的核心概念

内部状态 vs 外部状态

内部状态(Intrinsic State)

  • 不变的、可共享的部分
  • 存储在享元对象内部
  • 例子:字符的符号、字体、字号;棋子的颜色

外部状态(Extrinsic State)

  • 变化的、不可共享的部分
  • 由客户端存储和传递
  • 例子:字符的位置;棋子的坐标

核心结构

复制代码
Client(客户端)
   ↓ 使用
FlyweightFactory(享元工厂)
   ↓ 管理
Flyweight(享元接口)
   ↑ 实现
ConcreteFlyweight(具体享元)

适用场景

适合用享元模式的场景:

  1. 大量相似对象

    java 复制代码
    // 游戏中的子弹、粒子效果
    // 文档编辑器中的字符
    // 图形绘制中的图标
  2. 对象大部分状态可以外部化

    java 复制代码
    // 对象只有少量内部状态不同
    // 大量状态可以从外部传入
  3. 内存敏感的应用

    java 复制代码
    // 移动设备应用
    // 嵌入式系统
    // 游戏引擎

不适合的场景:

  1. 对象状态经常变化

    java 复制代码
    // 如果外部状态太复杂,性能反而下降
  2. 对象数量很少

    java 复制代码
    // 如果只有几个对象,没必要用享元
  3. 对象差异很大

    java 复制代码
    // 如果每个对象都很独特,无法共享

Java中的实际应用

1. String常量池

java 复制代码
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");

System.out.println(s1 == s2); // true - 享元模式
System.out.println(s1 == s3); // false - 新对象

2. Integer缓存

java 复制代码
Integer i1 = 127;
Integer i2 = 127;
Integer i3 = 128;
Integer i4 = 128;

System.out.println(i1 == i2); // true - 享元缓存
System.out.println(i3 == i4); // false - 超出缓存范围

3. 连接池

java 复制代码
// 数据库连接池、线程池都是享元思想
Connection conn1 = dataSource.getConnection(); // 可能返回缓存的连接
Connection conn2 = dataSource.getConnection(); // 可能返回缓存的连接

优缺点

优点:

  • 大幅减少内存使用
  • 减少对象创建开销
  • 提高性能

缺点:

  • 增加系统复杂度
  • 需要分离内部/外部状态
  • 可能引入线程安全问题

实现要点

1. 工厂模式结合

java 复制代码
// 享元模式通常结合工厂模式使用
public 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);
    }
}

2. 线程安全考虑

java 复制代码
// 多线程环境下需要同步
public synchronized Flyweight getFlyweight(String key) {
    // ...
}

3. 内存管理

java 复制代码
// 可能需要清理长时间不用的享元对象
public void cleanup() {
    // 清理策略
}

总结

享元模式就是:

  • 资源共享:相同的对象只创建一个,大家共用
  • 内外分离:不变的部分内部存储,变化的部分外部传递
  • 池化思想:像连接池、线程池一样管理对象

核心口诀:

对象大量又相似,

内存消耗成问题。

享元模式来共享,

内存性能双收益!

就像现实中的:

  • 🔤 活字印刷:有限的字模印刷无限书籍
  • ♟️ 棋盘游戏:有限的棋子玩无限局游戏
  • 🎨 颜料调色:有限的基色调出无限颜色
  • 🧱 乐高积木:有限的积木块拼出无限造型

记住:当系统中存在大量相似对象,且这些对象可以共享部分状态时,考虑使用享元模式!

相关推荐
专注API从业者9 分钟前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库
摇滚侠26 分钟前
DBeaver 导入数据库 导入 SQL 文件 MySQL 备份恢复
java·数据库·mysql
keep one's resolveY1 小时前
SpringBoot实现重试机制的四种方案
java·spring boot·后端
天空属于哈夫克31 小时前
企业微信API常见的错误和解决方案
java·数据库·企业微信
摇滚侠2 小时前
VMvare 虚拟机 Oracle19c 安装步骤,远程连接 Oracle19c,百度网盘安装包
java·oracle
梁萌2 小时前
idea报错找不到XX包的解决方法
java·intellij-idea·启动报错·缺少包
Agent产品评测局2 小时前
生产排期与MES/ERP系统打通,实操方法详解 —— 2026企业级智能体自动化选型与实战指南
java·运维·人工智能·ai·chatgpt·自动化
阿丰资源3 小时前
基于Spring Boot的电影城管理系统(直接运行)
java·spring boot·后端
呱牛do it3 小时前
企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 8)
java
消失的旧时光-19434 小时前
Spring Boot 工程化进阶:统一返回 + 全局异常 + AOP 通用工具包
java·spring boot·后端·aop·自定义注解