【设计模式】享元模式(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() {
    // 清理策略
}

总结

享元模式就是:

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

核心口诀:

对象大量又相似,

内存消耗成问题。

享元模式来共享,

内存性能双收益!

就像现实中的:

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

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

相关推荐
乙己4077 小时前
设计模式——单例模式(singleton)
java·c++·单例模式·设计模式
豐儀麟阁贵8 小时前
5.6对象
java·开发语言
格格步入8 小时前
🤔一次 OOM 排查(dump文件分析)
java·后端
蓝-萧8 小时前
Spring Security安全框架原理与实战
java·后端
Moe4888 小时前
CompletableFuture方法大全和使用详解(一步到位)
java·性能优化
敲代码的嘎仔8 小时前
数据结构算法学习day3——二分查找
java·开发语言·数据结构·学习·程序人生·算法·职场和发展
代码不停8 小时前
JavaEE多线程进阶
java·数据结构·java-ee
SimonKing8 小时前
聊聊Spring里那个不打扰Controller就能统一改响应的“神器”
java·后端·程序员
这不小天嘛8 小时前
23 种经典设计模式的名称、意图及适用场景概述
设计模式