🎯 设计模式专栏,持续更新中
欢迎订阅:JAVA实现设计模式
🛠️ 希望小伙伴们一键三连,有问题私信都会回复,或者在评论区直接发言
享元模式
享元模式(Flyweight Pattern) 是一种结构型设计模式,旨在通过共享细粒度对象来减少内存使用和对象创建的开销。享元模式可以在系统中重复使用多个相同或相似的对象,通过避免重复创建相同的对象来提高性能,特别是在大量对象需要频繁创建时,享元模式能够极大减少内存消耗。
核心思想:
享元模式将对象分为内部状态和外部状态,其中内部状态可以被共享,而外部状态则由外部提供。通过共享内部状态对象,可以避免创建大量类似的对象。
关键点:
- 内部状态:可以被共享的状态,不会随着环境改变。
- 外部状态:根据具体场景变化的状态,通常通过外部传递给享元对象。
- 享元工厂:用于管理和维护享元对象的共享,确保重复对象不会多次创建。
享元模式的原理类图
- FlyWeight 是抽象的享元角色, 他是产品的抽象类, 同时定义出对象的外部状态和内部状态的接口或实现
- ConcreteFlyWeight 是具体的享元角色,是具体的产品类,实现抽象角色定义相关业务
- UnSharedConcreteFlyWeight 是不可共享的角色,一般不会出现在享元工厂。
- FlyWeightFactory 享元工厂类,用于构建一个池容器(集合), 同时提供从池中获取对象方法
生动的案例:图形绘制系统 🎨
假设我们正在开发一个图形绘制系统,需要在屏幕上绘制大量的圆形(Circle)。每个圆形都有颜色、半径、位置等属性。为了提高性能,我们使用享元模式,共享相同颜色的圆形对象,避免重复创建相同颜色的圆形。
- 共享对象:具有相同颜色的圆形可以共享。
- 外部状态:圆形的半径和位置(这些信息是具体到每个圆形的,不能共享)
代码实现
Step 1: 创建 Flyweight
接口
定义享元模式的接口,draw()
方法接收外部状态。
// 享元抽象类
public interface Flyweight {
void draw(int x, int y, int radius); // 外部状态为位置和半径
}
Step 2: 实现具体的 Flyweight
类
实现 Flyweight
接口,ConcreteFlyweight
包含共享的颜色属性。
// 具体享元类
public class ConcreteFlyweight implements Flyweight {
private final String color; // 共享的内部状态
public ConcreteFlyweight(String color) {
this.color = color;
}
@Override
public void draw(int x, int y, int radius) {
System.out.println("Drawing a " + color + " circle at (" + x + ", " + y + ") with radius " + radius);
}
}
Step 3: 实现享元工厂类
工厂类负责管理共享的 Flyweight
对象,确保同样颜色的圆形只创建一次。
public class FlyweightFactory {
private static final Map<String, Flyweight> flyweights = new HashMap<>();
public static Flyweight getFlyweight(String color) {
Flyweight flyweight = flyweights.get(color);
if (flyweight == null) {
flyweight = new ConcreteFlyweight(color);
flyweights.put(color, flyweight);
System.out.println("Creating a " + color + " flyweight.");
}
return flyweight;
}
public static int getFlyweightCount(){
return flyweights.size();
}
}
Step 4: 客户端使用享元模式
客户端通过工厂获取 Flyweight
对象,并传入外部状态(位置和半径)。
public class Client {
public static void main(String[] args) {
Flyweight circle1 = FlyweightFactory.getFlyweight("Red");
circle1.draw(10, 10, 5);
Flyweight circle2 = FlyweightFactory.getFlyweight("Red");
circle2.draw(20, 20, 10);
Flyweight circle3 = FlyweightFactory.getFlyweight("Blue");
circle3.draw(15, 15, 7);
Flyweight circle4 = FlyweightFactory.getFlyweight("Red");
circle4.draw(30, 30, 15);
System.out.println("-------------------------------------");
System.out.println("Total number of flyweights: " + FlyweightFactory.getFlyweightCount());
}
}
输出结果
Creating a Red flyweight.
Drawing a Red circle at (10, 10) with radius 5
Drawing a Red circle at (20, 20) with radius 10
Creating a Blue flyweight.
Drawing a Blue circle at (15, 15) with radius 7
Drawing a Red circle at (30, 30) with radius 15
-------------------------------------
Total number of flyweights: 2
享元模式在源码中的应用
享元模式广泛应用于 Java 标准库和一些优秀的开源框架中,主要用于优化性能、减少内存占用。以下是几个使用了享元模式的经典例子:
1. Java 中的 String
常量池
Java 中的 String
类型是享元模式最典型的应用之一。String
类在 Java 中是不可变的,JVM 会在字符串常量池中缓存相同的字符串对象,以避免重复创建相同内容的字符串,从而节省内存空间。
享元模式的应用:
- 内部状态:字符串内容(相同的字符串内容可以被共享)。
- 外部状态:无(因为
String
是不可变的,所有内容都可以共享)
示例代码:
public class StringFlyweightExample {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "Hello";
// s1 和 s2 是同一个对象,因为 JVM 在常量池中共享了 "Hello"
System.out.println(s1 == s2); // 输出 true
// 通过 new 关键字创建新的字符串对象,不会使用常量池
String s3 = new String("Hello");
System.out.println(s1 == s3); // 输出 false
}
}
JVM 通过字符串常量池优化内存使用,避免重复创建相同内容的字符串。如果想看更深度解读String,请看这篇文章 https://blog.csdn.net/qq_44732500/article/details/141884904
2.Java 中的 Integer
缓存池
Integer
类中也使用了享元模式,对**-128 到 127 之间的整数**进行了缓存。这意味着当我们创建这些范围内的 Integer
对象时,它们将被共享,而不是每次都创建新对象。
享元模式的应用:
- 内部状态:整数值(-128 到 127 之间的整数被缓存共享)。
- 外部状态:无(这些整数是不可变的)。
源码片段:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
示例代码:
public class IntegerFlyweightExample {
public static void main(String[] args) {
Integer i1 = Integer.valueOf(127);
Integer i2 = Integer.valueOf(127);
// i1 和 i2 是同一个对象,因为 127 在缓存范围内
System.out.println(i1 == i2); // 输出 true
Integer i3 = Integer.valueOf(128);
Integer i4 = Integer.valueOf(128);
// i3 和 i4 不是同一个对象,因为 128 不在缓存范围内
System.out.println(i3 == i4); // 输出 false
}
}
Integer 缓存池通过共享 -128
到 127
范围内的整数,减少了内存使用。
3.MyBatis 中的 SqlSessionFactory
MyBatis 是一个持久层框架,它通过享元模式优化了 SqlSessionFactory
对象的创建。SqlSessionFactory
是一个重量级对象,通常一个应用只需要一个实例。MyBatis 通过工厂模式和享元模式确保每个数据库只创建一个 SqlSessionFactory
实例,并重复使用。
享元模式的应用:
- 内部状态:
SqlSessionFactory
对象(共享的连接配置)。 - 外部状态:无(
SqlSessionFactory
本身是不可变的)。
示例代码:
public class MyBatisFlyweightExample {
public static void main(String[] args) {
SqlSessionFactory sessionFactory1 = MyBatisUtil.getSqlSessionFactory();
SqlSessionFactory sessionFactory2 = MyBatisUtil.getSqlSessionFactory();
// sessionFactory1 和 sessionFactory2 是同一个实例
System.out.println(sessionFactory1 == sessionFactory2); // 输出 true
}
}
MyBatis 通过共享 SqlSessionFactory
实现了享元模式,避免重复创建数据库连接工厂,节省了系统资源。
4.Java 数据库连接池
在 Java 应用中,数据库连接池(如 HikariCP、DBCP 等)也是享元模式的经典应用。数据库连接是非常昂贵的资源,连接池通过共享有限的连接对象,避免重复创建和销毁连接,提升了系统性能。
享元模式的应用:
- 内部状态:连接池中的连接对象(可以共享)。
- 外部状态:连接的具体使用状态(如连接是否空闲)。
代码概念:
// 数据库连接池 Flyweight 模式
HikariDataSource dataSource = new HikariDataSource();
Connection conn1 = dataSource.getConnection();
Connection conn2 = dataSource.getConnection();
// 使用同一个数据库连接池管理连接对象
数据库连接池通过享元模式复用连接对象,大大提高了数据库连接的性能和资源管理。
总结
- 减少内存消耗:通过共享相同对象,减少重复对象的创建,特别适合大量相似对象的场景。
- 提高系统性能:通过共享对象,减少内存使用,提升程序的运行效率。
- 增加系统复杂性:为了区分内部状态和外部状态,系统设计可能会更加复杂
- 适用场景有限:享元模式适用于存在大量相同或相似对象的场景,如果对象的状态不易共享,享元模式的效果有限。
使用场景
- 需要大量重复对象的场景:如图形绘制系统、文本编辑器中字符对象的管理。
- 缓存池:享元模式常用于对象池或缓存池中,避免创建大量相同的对象。
- 游戏开发:在游戏中,地图上的草地、树木等对象可以使用享元模式进行共享。