适用场景:文本处理/游戏开发/大规模GUI组件
一、模式本质
用空间换时间的艺术
通过共享细粒度对象,减少内存消耗并提升性能。核心在于区分:
- 内在状态(Intrinsic):可共享的固定属性(如字符Unicode值)
- 外在状态(Extrinsic):不可共享的上下文属性(如字符坐标)
二、模式结构解剖
1. 核心组件
组件 | 作用 | 类比现实案例 |
---|---|---|
FlyweightFactory | 享元对象仓库(带缓存机制) | 字体库 |
Flyweight | 享元对象接口 | 字符模板协议 |
ConcreteFlyweight | 具体享元对象(存储内在状态) | 具体字符'A'的模板 |
UnsharedFlyweight | 非共享扩展对象(可选组件) | 特殊字符组合 |
2. 工作流程
客户端 --> 请求对象 -->首次请求:[FlyweightFactory] --> 新建[ConcreteFlyweight] --> 存入对象池 -->重复请求:返回已有[Flyweight],传递外在状态 --> 执行操作
三、Java实现
以下是通过享元模式实现奶茶点单的Java代码示例。享元模式的核心是“共享对象”,通过分离内在状态(不变属性)和外在状态(可变属性),减少重复对象的创建。
内在状态(可共享):奶茶的基础类型(如珍珠奶茶、抹茶拿铁)、杯型(大/中/小)。
外在状态(不可共享):订单号、客户名称、加料(珍珠、椰果等)。
享元对象接口
//享元接口:定义奶茶内在状态(类型、杯型)
public interface MilkTea {
//类型
String getType();
//杯型
String getSize();
}
具体享元类:存储奶茶的内在状态
//具体享元类:存储奶茶的内在状态
public class ConcreteMilkTea implements MilkTea {
// 类型(如"珍珠奶茶")
private final String type;
// 杯型(如"大杯")
private final String size;
public ConcreteMilkTea(String type, String size) {
this.type = type;
this.size = size;
}
@Override
public String getType() {
return type;
}
@Override
public String getSize() {
return size;
}
}
享元工厂
//享元工厂:缓存已创建的奶茶对象
public class MilkTeaFactory {
private static final Map<String, MilkTea> milkTeaMap = new HashMap<>();
public static MilkTea getMilkTea(String type, String size) {
String key = type + "_" + size;
if (!milkTeaMap.containsKey(key)) {
milkTeaMap.put(key, new ConcreteMilkTea(type, size));
}
return milkTeaMap.get(key);
}
}
订单类:存储外在状态(客户、订单号、加料)
//订单类:存储外在状态(客户、订单号、加料)
public class Order {
private final MilkTea milkTea;
private final String customer;
private final String orderId;
private final String toppings; // 加料(如"珍珠+椰果")
public Order(MilkTea milkTea, String customer, String orderId, String toppings) {
this.milkTea = milkTea;
this.customer = customer;
this.orderId = orderId;
this.toppings = toppings;
}
public void printOrder() {
System.out.printf("订单号:%s\n客户:%s\n奶茶:%s %s\n加料:%s\n\n", orderId, customer, milkTea.getSize(), milkTea.getType(),
toppings);
}
}
客户端
public class ClientApp {
public static void main(String[] args) {
// 创建享元对象(共享内在状态)
MilkTea milkTea1 = MilkTeaFactory.getMilkTea("珍珠奶茶", "大杯");
MilkTea milkTea2 = MilkTeaFactory.getMilkTea("珍珠奶茶", "大杯"); // 复用对象
MilkTea milkTea3 = MilkTeaFactory.getMilkTea("抹茶拿铁", "中杯");
// 创建订单(处理外在状态)
Order order1 = new Order(milkTea1, "张三", "20250224001", "珍珠+椰果");
Order order2 = new Order(milkTea2, "李四", "20250224002", "布丁");
Order order3 = new Order(milkTea3, "王五", "20250224003", "无");
// 打印订单信息
order1.printOrder();
order2.printOrder();
order3.printOrder();
}
}
运行结果
订单号:20250224001
客户:张三
奶茶:大杯 珍珠奶茶
加料:珍珠+椰果
订单号:20250224002
客户:李四
奶茶:大杯 珍珠奶茶
加料:布丁
订单号:20250224003
客户:王五
奶茶:中杯 抹茶拿铁
加料:无
四、模式深潜
内存优化对比
实现方式 | 对象数量 | 内存消耗 | 适用场景 |
---|---|---|---|
传统new对象 | 10000 | 100GB | 小规模场景 |
享元模式 | 5 | 50MB | 大规模重复元素场景 |
性能陷阱警示
// 错误的外在状态存储方式
class BadFlyweight {
private List<Position> positions = new ArrayList<>();
//存储外在状态
void addPosition(Position pos) {
positions.add(pos);
}
}
正确做法:外在状态应由客户端持有并通过参数传递,而非存储在享元对象中
五、高频面试闪电战
Q1:享元模式与对象池的区别?
- 享元:对象复用(同一对象多处使用)
- 对象池:资源回收(防止频繁创建销毁)
Q2:如何处理线程安全问题?
//双检锁优化后的工厂
public static TreeModel getTree(String type) {
if(!cache.containsKey(type)) {
synchronized(cache) {
if(!cache.containsKey(type)) {
cache.put(type, createTree(type));
}
}
}
return cache.get(type);
}
六、模式延展思考
现代框架中的应用
- Java String常量池:"abc"复用机制
- GUI组件复用:Swing的Border共享
- 数据库连接池:Proxy模式混合应用
新型变种
- Lazy Flyweight:延迟加载模型数据
- Distributed Flyweight:分布式对象缓存(如Redis共享)