Java设计模式——享元模式
一、引子
在 Java 中,String 类型具有一些独特的特性。其一,String 对象一旦被创建就不可改变(Immutable);其二,当两个 String 对象所包含的内容相同时,JVM 通常只会创建一个 String 对象,这两个不同的对象引用将指向该同一对象。我们可以通过以下代码来验证这两个特性:
public class TestPattern {
public static void main(String[] args) {
String n = "I Love Java";
String m = "I Love Java";
System.out.println(n == m);
}
}上述代码会输出 true,这表明在 JVM 中 n 和 m 两个引用指向了同一个 String 对象。若在系统输出之前添加一行代码 m = m + "hehe";,此时 n == m 的结果将变为 false。这是因为执行添加语句时,m 指向了一个新创建的 String 对象,而非修改原来引用的对象(因为 String 不可变)。
String 类型的设计避免了创建大量相同内容的 String 对象时产生的不必要资源损耗,是享元模式(Flyweight Pattern)应用的范例。下面让我们深入学习享元模式。
二、定义与分类
享元模式英文称为"Flyweight Pattern"。其定义为:采用共享技术来避免大量拥有相同内容对象的开销,这里最常见、直观的开销就是内存损耗。享元模式以共享的方式高效支持大量细粒度对象。
在该模式中,核心概念是共享。为实现共享,需要区分内蕴状态(Intrinsic State)和外蕴状态(Extrinsic State):
- 内蕴状态:是共性,存储在享元内部,不会随环境改变而不同,可共享。
- 外蕴状态:是个性,随环境改变而改变,由客户端保持。在具体环境下,客户端将外蕴状态传递给享元以创建不同对象。
根据《Java 与模式》,享元模式分为单纯享元模式和复合享元模式。
(一)单纯享元模式
结构
- 抽象享元角色(Flyweight):在 Java 中可由抽象类或接口担当,为具体享元角色规定必须实现的方法,外蕴状态以参数形式通过此方法传入。
- 具体享元角色(Concrete Flyweight):实现抽象角色规定的方法,若存在内蕴状态,负责为其提供存储空间。
- 享元工厂角色(Flyweight Factory):负责创建和管理享元角色,是实现共享的关键。其实现通常使用单例模式(Singleton),确保工厂对象只产生一个实例。
- 客户端角色(Client):维护对所有享元对象的引用,并存储对应的外蕴状态。
单纯享元模式的类图如下:
[此处可插入单纯享元模式类图,展示 Client、Flyweight、FlyweightFactory、ConcreteFlyweight 之间的关系]
- 该模式结构类似简单工厂模式,但重点不同。简单工厂模式主要使系统不依赖于实现细节,而享元模式旨在采用共享技术避免大量相同内容对象的开销。
举例(以咖啡店订单为例)
- 假设一家咖啡店有多种口味的咖啡(如拿铁、摩卡、卡布奇诺等),接到大量订单时,咖啡口味可设置为共享,不必为每一杯单独生成对象。
- 以下是相关代码实现:
import java.util.*;
// 抽象订单类
public abstract class Order {
// 执行卖出动作
public abstract void sell();
}
// 具体口味订单类
public class FlavorOrder extends Order {
public String flavor;
// 获取咖啡口味
public FlavorOrder(String flavor) {
this.flavor = flavor;
}
@Override
public void sell() {
System.out.println("卖出一份" + flavor + "的咖啡。");
}
}
// 口味工厂类
public class FlavorFactory {
private Map<String, Order> flavorPool = new HashMap<>();
// 静态工厂,负责生成订单对象
private static final FlavorFactory flavorFactory = new FlavorFactory();
private FlavorFactory() {
}
public static FlavorFactory getInstance() {
return flavorFactory;
}
public Order getOrder(String flavor) {
Order order = flavorPool.get(flavor);
if (order == null) {
order = new FlavorOrder(flavor);
flavorPool.put(flavor, order);
}
return order;
}
public int getTotalFlavorsMade() {
return flavorPool.size();
}
}
// 客户端类
public class Client {
// 客户下的订单
private static List<Order> orders = new ArrayList<>();
// 订单对象生成工厂
private static FlavorFactory flavorFactory;
// 增加订单
private static void takeOrders(String flavor) {
orders.add(flavorFactory.getOrder(flavor));
}
public static void main(String[] args) {
// 订单生成工厂
flavorFactory = FlavorFactory.getInstance();
// 增加订单
takeOrders("摩卡");
takeOrders("卡布奇诺");
takeOrders("香草星冰乐");
takeOrders("香草星冰乐");
takeOrders("拿铁");
takeOrders("卡布奇诺");
takeOrders("拿铁");
takeOrders("卡布奇诺");
takeOrders("摩卡");
takeOrders("香草星冰乐");
takeOrders("卡布奇诺");
takeOrders("摩卡");
takeOrders("香草星冰乐");
takeOrders("拿铁");
takeOrders("拿铁");
// 卖咖啡
for (Order order : orders) {
order.sell();
}
// 打印生成的订单 java 对象数量
System.out.println("\n客户一共买了 " + orders.size() + " 杯咖啡! ");
// 打印生成的订单 java 对象数量
System.out.println("共生成了 " + flavorFactory.getTotalFlavorsMade() + " 个 FlavorOrder java 对象! ");
}
}输出结果显示,通过口味共享极大减少了对象数目,降低了内存消耗。例如,客户一共买了 15 杯咖啡,但只生成了 4 个 FlavorOrder Java 对象。
(二)复合享元模式
结构
- 抽象享元角色:同单纯享元模式,为具体享元角色规定必须实现的方法,外蕴状态以参数形式传入。
- 具体享元角色:实现抽象角色规定的方法,负责内蕴状态的存储空间(若有)。
- 复合享元角色(Concrete Composite Flyweight):所代表对象不可共享,但可分解为多个单纯享元对象的组合。
- 享元工厂角色:负责创建和管理享元角色,实现共享的关键。
- 客户端角色:维护对所有享元对象的引用,并存储对应的外蕴状态。
复合享元模式的类图如下:
[此处可插入复合享元模式类图,展示 Client、Flyweight、FlyweightFactory、ConcreteFlyweight、ConcreteCompositeFlyweight 之间的关系]
- 该模式左半部类似简单工厂模式,右半部类似合成模式(Composite Pattern)。合成模式用于将具体享元角色和复合享元角色同等对待和处理,确保复合享元中包含的单纯享元具有相同外蕴状态,而单纯享元内蕴状态往往不同。
举例(在餐馆点菜场景下)
- 以去餐馆吃饭为例,内蕴状态代表菜肴种类,外蕴状态是点菜人。
- 首先定义抽象享元角色:
import java.util.*;
interface Menu {
// 规定实现类必须实现设置内外关系的方法
public void setPersonMenu(String person, List<String> list);
// 规定实现类必须实现查找外蕴状态对应的内蕴状态的方法
public List<String> findPersonMenu(String person, List<String> list);
}- 具体享元角色实现:
class PersonMenu implements Menu {
private String dish;
// 在构造方法中给内蕴状态赋值
public PersonMenu(String dish) {
this.dish = dish;
}
public synchronized void setPersonMenu(String person, List<String> list) {
list.add(person);
list.add(dish);
}
public List<String> findPersonMenu(String person, List<String> list) {
List<String> dishList = new ArrayList<>();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (person.equals(it.next())) {
dishList.add(it.next());
}
}
return dishList;
}
}- 享元工厂角色:
class FlyweightFactory {
private Map<String, Menu> menuList = new HashMap<>();
private static final FlyweightFactory factory = new FlyweightFactory();
// 单例模式,确保工厂对象唯一
private FlyweightFactory() {
}
public static FlyweightFactory getInstance() {
return factory;
}
// 享元模式关键方法,根据内蕴状态创建或获取对象
public synchronized Menu factory(String dish) {
if (menuList.containsKey(dish)) {
return menuList.get(dish);
} else {
Menu menu = new PersonMenu(dish);
menuList.put(dish, menu);
return menu;
}
}
// 验证生成对象数量
public int getNumber() {
return menuList.size();
}
// 支持复合享元创建的重载方法
public Menu factory(String[] dish) {
PersonMenuMuch menu = new PersonMenuMuch();
for (String key : dish) {
menu.add(key, this.factory(key));
}
return menu;
}
}- 复合享元角色:
class PersonMenuMuch implements Menu {
private Map<String, Menu> MenuList = new HashMap<>();
public PersonMenuMuch() {
}
// 增加一个新的单纯享元对象
public void add(String key, Menu menu) {
MenuList.put(key, menu);
}
// 两个无为的方法(因为复合享元不涉及内外状态对应)
public synchronized void setPersonMenu(String person, List<String> list) {
}
public List<String> findPersonMenu(String person, List<String> list) {
return null;
}
}- 客户端使用示例:
class Client {
private static FlyweightFactory factory;
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
factory = FlyweightFactory.getInstance();
Menu list = factory.factory("尖椒土豆丝");
list.setPersonMenu("ai92", list1);
list = factory.factory("红烧肉");
list.setPersonMenu("ai92", list1);
list = factory.factory("地三鲜");
list.setPersonMenu("ai92", list1);
list = factory.factory("地三鲜");
list.setPersonMenu("ai92", list1);
list = factory.factory("红焖鲤鱼");
list.setPersonMenu("ai92", list1);
list = factory.factory("红烧肉");
list.setPersonMenu("ai921", list1);
list = factory.factory("红焖鲤鱼");
list.setPersonMenu("ai921", list1);
list = factory.factory("地三鲜");
list.setPersonMenu("ai921", list1);
System.out.println(factory.getNumber());
List<String> list2 = list.findPersonMenu("ai921", list1);
if (list2 != null) {
Iterator<String> it = list2.iterator();
while (it.hasNext()) {
System.out.println(" " + it.next());
}
}
}
}(三)两种模式对比
| 对比维度 | 单纯享元模式 | 复合享元模式 |
|---|---|---|
| 复杂度 | 较低 | 较高,比单纯享元模式复杂 |
| 共享效果 | 良好,显著减少对象数量 | 未达预期,虽内部单纯享元可共享,但复合享元角色本身使用 Map 保存状态,未节省空间和对象个数 |
| 建议 | 推荐使用 | 违背享元模式初衷,应尽量使用单纯享元模式 |
三、使用优缺点
(一)优点
享元模式能大幅降低内存中对象数量,提高程序运行速度。例如在处理大量重复字符串或文本系统中字母对象时可节省资源。
(二)缺点
- 为实现对象共享,需将一些状态外部化,使程序逻辑复杂化。
- 读取外部状态会使运行时间稍变长。
(三)使用条件
- 系统中有大量对象,影响系统效率。
- 对象状态可分离为内外两部分,且内外状态划分及对应关系维护很重要。划分不当可能无法减少对象数量,对应关系维护需花费一定空间和时间。享元模式是以时间换空间,可使用 B 树等优化对应关系查找。
四、总结
享元模式较为复杂,实际应用相对较少,但共享思想对系统优化有益,在企业级架构设计中应用广泛,如缓存体系。Java 中的 String 和 Integer 类是其应用实例。
说明:本文代码示例旨在阐述享元模式的核心逻辑与结构。部分代码采用了较早期的 Java 编码风格(如部分裸类型使用),在实际现代 Java 开发中,建议严格使用泛型(Generics)并遵循最新的编码规范。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-she-ji-mo-shi--xiang-yuan-mo-shi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。