23种设计模式23种设计模式
首页
介绍
  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 建造者模式
  • 原型模式
  • 适配器模式
  • 桥接模式
  • 组合模式
  • 装饰器模式
  • 外观模式
  • 享元模式
  • 代理模式
  • 责任链模式
  • 命令模式
  • 解释器模式
  • 迭代器模式
  • 中介者模式
  • 备忘录模式
  • 观察者模式
  • 状态模式
  • 策略模式
  • 模板方法模式
  • 访问者模式
🚀 编程指南
首页
介绍
  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 建造者模式
  • 原型模式
  • 适配器模式
  • 桥接模式
  • 组合模式
  • 装饰器模式
  • 外观模式
  • 享元模式
  • 代理模式
  • 责任链模式
  • 命令模式
  • 解释器模式
  • 迭代器模式
  • 中介者模式
  • 备忘录模式
  • 观察者模式
  • 状态模式
  • 策略模式
  • 模板方法模式
  • 访问者模式
🚀 编程指南
  • 结构型模式

    • 适配器模式 (Adapter)
    • 桥接模式 (Bridge)
    • 组合模式 (Composite)
    • 装饰器模式 (Decorator)
    • 外观模式 (Facade)
    • 享元模式 (Flyweight)
    • 代理模式 (Proxy)

享元模式 (Flyweight)

📖 通俗理解

想象一个围棋游戏:

  • 棋盘上有 361 个交叉点,最多可以放 361 个棋子
  • 每个棋子都创建一个对象?那太浪费内存了!
  • 其实棋子只有两种:黑棋和白棋

享元模式的思路:只创建两个棋子对象(黑/白),所有位置共享使用这两个对象。棋子的颜色是内部状态(不变),位置是外部状态(变化的,使用时传入)。

享元模式就是:共享对象,减少内存使用。

🎯 解决什么问题?

问题:系统中存在大量相似对象,占用大量内存。

例子:

  • 游戏中的子弹(成千上万颗,但类型只有几种)
  • 文字编辑器中的字符(每个字符都创建对象?)
  • 地图中的树木(同一种树共享模型)

解决:把对象的状态分为内部状态(共享)和外部状态(不共享),共享相同内部状态的对象。

🌰 生活中的例子

  • 共享单车:车是共享的,骑车的人和目的地是外部状态
  • 图书馆:书是共享的,借阅人是外部状态
  • 线程池:线程是共享的,任务是外部状态
  • 字符串常量池:相同的字符串共享同一个对象

💻 Java 代码实现

场景:围棋棋子

第一步:定义享元接口

/**
 * 享元接口:棋子
 */
public interface ChessPiece {
    /**
     * 落子
     * @param x 外部状态:x 坐标
     * @param y 外部状态:y 坐标
     */
    void place(int x, int y);
}

第二步:实现具体享元

/**
 * 具体享元:棋子实现
 * 内部状态:颜色(不变)
 */
public class ConcreteChessPiece implements ChessPiece {
    
    // 内部状态:颜色,创建后不变
    private final String color;
    
    public ConcreteChessPiece(String color) {
        this.color = color;
        System.out.println("创建了一个" + color + "棋子");
    }
    
    @Override
    public void place(int x, int y) {
        // 外部状态:位置,由调用者传入
        System.out.println(color + "棋子落在位置 (" + x + ", " + y + ")");
    }
    
    public String getColor() {
        return color;
    }
}

第三步:创建享元工厂

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

/**
 * 享元工厂:管理棋子对象池
 */
public class ChessPieceFactory {
    
    // 享元池
    private static final Map<String, ChessPiece> pieces = new HashMap<>();
    
    /**
     * 获取棋子(如果池中有就复用,没有就创建)
     */
    public static ChessPiece getChessPiece(String color) {
        ChessPiece piece = pieces.get(color);
        
        if (piece == null) {
            piece = new ConcreteChessPiece(color);
            pieces.put(color, piece);
        }
        
        return piece;
    }
    
    /**
     * 获取享元池大小
     */
    public static int getPoolSize() {
        return pieces.size();
    }
}

第四步:使用享元

public class ChessGame {
    public static void main(String[] args) {
        System.out.println("=== 开始下棋 ===\n");
        
        // 模拟下棋过程
        // 黑棋先手
        ChessPiece black1 = ChessPieceFactory.getChessPiece("黑");
        black1.place(3, 3);
        
        // 白棋
        ChessPiece white1 = ChessPieceFactory.getChessPiece("白");
        white1.place(4, 4);
        
        // 黑棋(复用之前的黑棋对象)
        ChessPiece black2 = ChessPieceFactory.getChessPiece("黑");
        black2.place(3, 4);
        
        // 白棋(复用之前的白棋对象)
        ChessPiece white2 = ChessPieceFactory.getChessPiece("白");
        white2.place(4, 3);
        
        // 继续下...
        ChessPieceFactory.getChessPiece("黑").place(5, 5);
        ChessPieceFactory.getChessPiece("白").place(5, 4);
        ChessPieceFactory.getChessPiece("黑").place(6, 6);
        
        System.out.println("\n=== 游戏结束 ===");
        System.out.println("共下了 7 步棋");
        System.out.println("实际创建的棋子对象数量: " + ChessPieceFactory.getPoolSize());
        
        // 验证是同一个对象
        System.out.println("\nblack1 == black2 ? " + (black1 == black2));
        System.out.println("white1 == white2 ? " + (white1 == white2));
    }
}

输出:

=== 开始下棋 ===

创建了一个黑棋子
黑棋子落在位置 (3, 3)
创建了一个白棋子
白棋子落在位置 (4, 4)
黑棋子落在位置 (3, 4)
白棋子落在位置 (4, 3)
黑棋子落在位置 (5, 5)
白棋子落在位置 (5, 4)
黑棋子落在位置 (6, 6)

=== 游戏结束 ===
共下了 7 步棋
实际创建的棋子对象数量: 2

black1 == black2 ? true
white1 == white2 ? true

虽然下了 7 步棋,但只创建了 2 个对象!

🔥 实战案例:字符串常量池

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

public class StringPoolDemo {
    public static void main(String[] args) {
        // 字面量创建的字符串会放入常量池
        String s1 = "hello";
        String s2 = "hello";
        String s3 = new String("hello");
        String s4 = s3.intern();  // 从常量池获取
        
        System.out.println("s1 == s2 : " + (s1 == s2));  // true,共享
        System.out.println("s1 == s3 : " + (s1 == s3));  // false,new 创建新对象
        System.out.println("s1 == s4 : " + (s1 == s4));  // true,intern() 返回池中对象
    }
}

🔥 实战案例:数据库连接池

import java.util.ArrayList;
import java.util.List;

/**
 * 数据库连接(享元对象)
 */
public class Connection {
    
    private String id;
    private boolean inUse;
    
    public Connection(String id) {
        this.id = id;
        this.inUse = false;
        System.out.println("创建数据库连接: " + id);
    }
    
    public void execute(String sql) {
        System.out.println(id + " 执行SQL: " + sql);
    }
    
    // getter setter...
}

/**
 * 连接池(享元工厂)
 */
public class ConnectionPool {
    
    private static final int MAX_SIZE = 5;
    private List<Connection> pool = new ArrayList<>();
    
    public ConnectionPool() {
        // 预先创建连接
        for (int i = 0; i < MAX_SIZE; i++) {
            pool.add(new Connection("Conn-" + (i + 1)));
        }
    }
    
    /**
     * 获取连接
     */
    public synchronized Connection getConnection() {
        for (Connection conn : pool) {
            if (!conn.isInUse()) {
                conn.setInUse(true);
                System.out.println("获取连接: " + conn.getId());
                return conn;
            }
        }
        System.out.println("没有可用连接!");
        return null;
    }
    
    /**
     * 归还连接
     */
    public synchronized void releaseConnection(Connection conn) {
        conn.setInUse(false);
        System.out.println("归还连接: " + conn.getId());
    }
    
    public int getPoolSize() {
        return pool.size();
    }
}

使用方式:

public class DatabaseDemo {
    public static void main(String[] args) {
        ConnectionPool pool = new ConnectionPool();
        
        System.out.println("\n=== 开始使用连接 ===");
        
        // 获取连接执行操作
        Connection conn1 = pool.getConnection();
        conn1.execute("SELECT * FROM users");
        
        Connection conn2 = pool.getConnection();
        conn2.execute("INSERT INTO logs ...");
        
        // 归还连接
        pool.releaseConnection(conn1);
        
        // 再次获取(复用 conn1)
        Connection conn3 = pool.getConnection();
        conn3.execute("UPDATE users SET ...");
        
        System.out.println("\n连接池大小: " + pool.getPoolSize());
    }
}

🔥 实战案例:Integer 缓存池

Java 的 Integer 类也使用了享元模式:

public class IntegerCacheDemo {
    public static void main(String[] args) {
        // -128 到 127 使用缓存
        Integer a = 100;
        Integer b = 100;
        System.out.println("a == b : " + (a == b));  // true
        
        // 超出缓存范围
        Integer c = 200;
        Integer d = 200;
        System.out.println("c == d : " + (c == d));  // false
        
        // valueOf 使用缓存
        Integer e = Integer.valueOf(100);
        Integer f = Integer.valueOf(100);
        System.out.println("e == f : " + (e == f));  // true
    }
}

📊 类图结构

┌─────────────┐          ┌────────────────────┐
│   Client    │ -------> │  FlyweightFactory  │
└─────────────┘          │  - flyweights: Map │
                         │  + getFlyweight()  │
                         └─────────┬──────────┘
                                   │ 创建/返回
                                   ▼
                         ┌────────────────────┐
                         │   <<interface>>    │
                         │     Flyweight      │
                         │ + operation(state) │
                         └─────────▲──────────┘
                                   │
                    ┌──────────────┴──────────────┐
                    │                             │
           ┌────────┴─────────┐        ┌──────────┴────────┐
           │ ConcreteFlyweight │        │ UnsharedFlyweight │
           │ - intrinsicState │        │ 不共享的享元       │
           └──────────────────┘        └───────────────────┘

⚠️ 内部状态 vs 外部状态

状态类型说明例子(棋子)例子(字符)
内部状态存储在享元内部,不随环境变化颜色字符内容
外部状态随环境变化,使用时传入位置(x, y)字体、大小

✅ 适用场景

  1. 大量相似对象:系统中存在大量相似对象
  2. 对象状态可分离:状态可分为内部和外部
  3. 对象可共享:对象是不可变的或可以安全共享

⚠️ 优缺点

优点:

  • 大大减少对象数量,节省内存
  • 外部状态相对独立

缺点:

  • 增加系统复杂度
  • 需要分离内部和外部状态

小结

享元模式的核心:共享对象,减少内存使用。

关键点:

  • 区分内部状态(共享)和外部状态(不共享)
  • 用工厂管理享元池
  • Java 中的 String、Integer 都使用了享元模式

👉 下一篇:代理模式

Prev
外观模式 (Facade)
Next
代理模式 (Proxy)