怪物卡车机器人
83.87MB · 2025-09-30
你有没有这样的经历?辛辛苦苦写完一个程序,一运行就发现内存占用像个无底洞,分分钟飙到 G,然后电脑就开始咆哮。如果你的项目需要处理成千上万个相似对象,这个问题就会变得尤为突出。
这时候,一个常常被我们忽略、但又极其强大的设计模式,或许能成为你的救星——它就是享元模式。很多人可能只知道它的名字,但它背后那套精妙的“省钱”哲学,才是真正值得我们深挖的。
简单来说,享元模式就像一个超级聪明的仓库管理员。他知道,很多货物虽然看起来是独立的,但它们的核心部件其实是完全相同的。与其为每一件货物都重新制造一遍核心部件,不如只造一份,然后让所有货物都共享它。
在软件里,这个“核心部件”就是对象的内在状态(Intrinsic State) ——那些可以被所有对象共享、并且创建后就不会改变的属性。而“货物本身”的独特之处,比如在仓库中的位置,就是它的外在状态(Extrinsic State) ——这部分是独一无二的,不能被共享。
享元模式的精髓,就在于把这两种状态拆开,然后用一个工厂来管理和复用那些可以共享的部分,从而从根本上解决内存爆炸的问题。
我们用两个例子,带你感受享元模式的魅力。
想象一下你在玩一个大型即时战略游戏,屏幕上有成千上万个士兵。如果每个士兵都包含完整的模型、贴图和属性,那电脑的内存会瞬间被榨干。
而有了享元模式,游戏开发者只需要为每一种士兵类型(比如步兵、弓箭手)创建一个享元对象(共享的模板),这个对象包含了所有同类型士兵都共有的属性(攻击力、防御力、模型文件等)。而每个士兵在地图上的位置、生命值等独有数据,则作为外在状态单独存储。
这样,无论你有十万个还是百万个士兵,内存中都只需要存储有限的几个模板对象,极大地提升了游戏的流畅度。
在 Java 中,String
是我们最常用的类之一,但你有没有想过,为什么 String s1 = "hello";
和 String s2 = "hello";
这两个字符串会是同一个对象呢?
这就是 Java 在底层默默地使用了享元模式。JVM 有一个字符串常量池,它就像一个享元工厂,专门存储那些字面量创建的字符串。当你创建一个新的字符串字面量时,JVM 会先去池中查找,如果已经有了,就直接返回已有的引用,而不是再创建一个新的。
这种设计确保了在处理大量文本数据时,程序不会因为创建无数个重复的字符串对象而耗尽内存。
为了更好地理解享元模式中各个角色之间的关系,我们可以通过一个 UML 类图来展示它的结构。
角色解析:
Flyweight
(享元) :定义一个接口,该接口通过外部状态来操作内部状态。ConcreteFlyweight
(具体享元) :实现 Flyweight
接口,并存储内在状态(可共享的部分)。UnsharedConcreteFlyweight
(非共享具体享元) :并非所有 Flyweight
子类都需要共享。一些复杂的对象可能不适合共享,可以单独实现。FlyweightFactory
(享元工厂) :负责创建和管理享元对象,它使用一个缓存来确保只有共享对象的一个实例存在。Client
(客户端) :使用享元工厂来获取享元对象,并传递外在状态给享元对象进行操作。理解了原理,我们再来看看如何用代码实现它。这里我们用一个简单的商品系统来演示。
首先,我们定义一个享元接口和具体的享元类。这个类必须是不可变的,因为我们要共享它。
public interface ProductFlyweight {
/**
* 显示商品信息。
* @param price 商品的当前价格(外在状态)。
* @param location 商品的物理位置(外在状态)。
*/
void display(double price, String location);
}
/**
* ConcreteFlyweight: 具体享元类,只包含可共享的内在状态。
*/
public final class Product implements ProductFlyweight {
// 真正的内在状态:不可变的、可共享的
private final String productName;
public Product(String productName) {
this.productName = productName;
}
@Override
public void display(double price, String location) {
System.out.println("显示商品: " + this.productName +
", 价格: " + price +
", 位置: " + location);
}
}
接下来,我们创建享元工厂,它负责管理和复用享元对象。
import java.util.HashMap;
import java.util.Map;
/**
* FlyweightFactory: 享元工厂,负责管理和复用享元对象。
*/
public class ProductFactory {
// 缓存池,用于存储享元对象
private static final Map<String, ProductFlyweight> productPool = new HashMap<>();
public static ProductFlyweight getProduct(String productName) {
// 使用 productName 作为 key 进行查找
if (!productPool.containsKey(productName)) {
System.out.println("创建新的商品对象: " + productName);
productPool.put(productName, new Product(productName));
}
return productPool.get(productName);
}
}
最后,在客户端中调用,并验证我们的对象是否被成功共享。
public class FlyweightPatternDemo {
public static void main(String[] args) {
// 客户端直接获取享元对象,并动态传入外在状态
ProductFlyweight laptop = ProductFactory.getProduct("笔记本电脑");
laptop.display(5999.00, "A1 货架");
laptop.display(4999.00, "线上促销");
ProductFlyweight phone = ProductFactory.getProduct("智能手机");
phone.display(3999.00, "B1 展台");
phone.display(3500.00, "双十一大促");
System.out.println("n--- 验证对象是否被共享 ---");
ProductFlyweight laptop2 = ProductFactory.getProduct("笔记本电脑");
System.out.println("laptop == laptop2 ? " + (laptop == laptop2));
}
}
运行结果:
从输出可以看出,尽管我们请求了多次相同的产品,但工厂只创建了一次实例。laptop1 == laptop2 和 phone == phone2 的结果都是 true,证明了对象的成功共享。
你可能会觉得享元模式和单例模式很像,都涉及到对象的复用。但它们的核心目标是不同的。
特性 | 享元模式(Flyweight) | 单例模式(Singleton) |
---|---|---|
目的 | 节省内存,通过复用相似对象的共享部分。 | 保证一个类在程序中只有一个实例。 |
实例数量 | 可以有多个实例,但其内在状态是共享的。 | 只有一个唯一的实例。 |
关注点 | 关注共享相似对象的内存。 | 关注全局资源的唯一性。 |
享元模式允许你创建多个实例,只要这些实例的内在状态是相同的。而单例模式则从根本上限制了实例的数量,保证全局唯一。
享元模式的精髓在于分治。它将一个看似庞大而复杂的问题(内存开销),分解为“不变的部分”和“变化的部分”,然后通过共享不变的部分来高效解决问题。它提醒我们,在设计程序时,要时刻思考哪些对象可以被复用,以及如何优雅地实现这种复用。