第十二章 享元模式
在Java开发或者面试过程中,经常会遇到一个面试题:
String a = new String("abc");
String b = new String("abc");
问:a == b ?
答案是肯定的,因为JVM在创建一个字符串后,会将其存储在 字符串池 中,下次new时会先去 字符串池 中查询是否已经存在。然后将引用地址返回。
这就是一个典型的 享元模式
案例。
一、设计一个围棋软件
对于围棋软件而言,棋盘中包含大量的黑白色棋子,它们的形状、大小一致,但是出现的位置不同。如果将所有棋子都 new 一个对象存储在内存中,则该软件在运行时所需的内存空间会非常大。
解决方案
享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象。
在享元模式中,存储这些共享实例对象的地方称为“享元池(Flyweight Pool)”
二、享元模式
享元模式以共享方式,高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了 内部状态(Intrinsic State)和外部状态(Extrinsic State)。
内部状态
内部状态是指存储在享元对象内部并且不会随环境变化而改变的状态,内部状态可以共享,但是不会被修改。
外部状态
外部状态是随环境变化而变化的、不可以共享的状态。
外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入享元对象内部。
享元模式的定义
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。
享元模式因其共享的是细粒度对象,因此又称为 轻量级模式
,属于对象结构型模式
。
享元模式中的几个角色
- Flyweight - 抽象享元类
- 接口或者抽象类,声明了具体享元类的公共方法
- 这些方法可以向外界提供内部数据
- 也可以有外界来设计外部数据
- ConcreteFlyweight - 具体享元类
- 实现了抽象享元类,实例被称为享元对象
- 具体享元类中为内部状态提供了存储空间
- 通常使用单例模式来设计具体享元类
- UnsharedConcreteFlyweight - 非共享具体享元类
- 不能被共享的子类可以设计为 非共享具体享元类
- 在使用时可以直接通过实例化创建
- FlyweightFactory - 享元工厂类
- 用于创建并管理享元对象,针对抽象享元类编程
- 存储享元池,一般设计为“键值对”的集合
- 可以结合工厂模式进行设计,返回唯一的实例
三、享元模式的核心
1. 享元工厂类
class FlyweightFactory {
// 定义享元池
private Map<String,Flyweight> flyweights = new HashMap();
public Flyweight getFlyweight(String key){
// 对象存在,则直接从享元池中取
if(flyweights.containsKey(key)){
return flyweights.get(key);
}
// 不存在,则创建一个享元对象,并放入享元池中返回
else {
Flyweight fy = new ConcreteFlyweight();
flyweights.put(key,fy);
return fy;
}
}
}
2. 享元类
享元类将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元模式中。
class Flyweight {
// 内部状态,与享元对象的内部状态一致
private String intrinsicState;
public Flyweight(String intrinsicState){
this.intrinsicState = intrinsicState;
}
// 外部状态,在使用时由外部设置,不保存在享元对象中。
// 即使是同一个享元对象,每次使用时也可以传入不同的外部状态
public void operation(String extrinsicState){
.......
}
}
四、完整实现
/**
* 享元模式
*/
// 围棋棋子类 - 抽象享元类
abstract class IgoChessman {
// 抽象内部状态
public abstract String getColor();
// 在方法调用时,注入外部状态
public void display(Integer x, Integer y) {
System.out.println("棋子颜色:" + getColor() + ",当前棋子位置为:" + x + "," + y);
}
}
// 黑色棋子类 - 具体享元类
class BlackIgoChessman extends IgoChessman {
@Override
public String getColor() {
return "黑色";
}
}
// 白色棋子类 - 具体享元类
class WhiteIgoChessman extends IgoChessman {
@Override
public String getColor() {
return "白色";
}
}
// 围棋棋子工厂类 - 享元工厂类,使用单例模式进行设计
class IgoChessmanFactory {
private static IgoChessmanFactory instance = new IgoChessmanFactory();
private static Hashtable<String, IgoChessman> ht; // 使用 Hashtable 来存储享元对象,充当享元池
private IgoChessmanFactory() {
ht = new Hashtable<>();
IgoChessman black = new BlackIgoChessman();
IgoChessman white = new WhiteIgoChessman();
ht.put("b", black);
ht.put("w", white);
}
// 返回享元工厂类的唯一实例
public static IgoChessmanFactory getInstance() {
return instance;
}
// 通过key来获取存储在 Hashtable 中的享元对象
public IgoChessman getIgoChessman(String color) {
return ht.get(color);
}
}
public class FlyweightPattern {
public static void main(String[] args) {
// 获取享元工厂对象
IgoChessmanFactory factory = IgoChessmanFactory.getInstance();
// 通过享元工厂获取棋子
IgoChessman black1 = factory.getIgoChessman("b");
IgoChessman black2 = factory.getIgoChessman("b");
IgoChessman black3 = factory.getIgoChessman("b");
IgoChessman white1 = factory.getIgoChessman("w");
IgoChessman white2 = factory.getIgoChessman("w");
black1.display(1,2);
black2.display(2,3);
black3.display(3,4);
white1.display(4,5);
white2.display(2,2);
}
}
五、单纯享元模式与复合享元模式
5.1 单纯享元模式
单纯享元模式中,所有具体享元类都是可以共享的,不存在非共享具体享元类。
5.2 复合享元模式
将一些单纯享元对象使用组合模式进行组合,形成复合享元对象。复合享元对象本身不能共享,但是可以将组合后的享元对象分解为单纯享元对象,进行共享。
六、补充
享元模式与其他模式的联用:
- 享元工厂类通常提供一个
静态的工厂方法
用于返回享元对象,使用简单工厂模式
来生成享元对象 - 享元工厂通常是唯一的,可以使用
单例模式
进行设计 - 可以使用
组合模式
形成复合享元模式,统一对多个享元对象设置外部状态
七、总结
“节约内存,提高性能”
优点
- 极大的减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而节约系统资源,提高系统性能
- 享元模式的外部状态相对独立,且不影响其内部状态,从而使得享元对象可以在不同环境中被共享
缺点
- 享元模式需要分离 内部状态和外部状态,使程序的逻辑更复杂
- 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长
适用场景
- 系统中有大量相同或相似的对象,造成大量的内存浪费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 享元模式在运行时需要维护一个享元池,势必会使内存一直占用,因此享元池中的享元对象需要多次重复使用时才值得
八、练习
Sunny软件公司欲开发一个多功能文档编辑器,在文本文档中可以插入图片、动画、视频等多媒体资料,为了节约系统资源,相同的图片、动画和视频在同一个文档中只需保存一份,但是可以多次重复出现,而且它们每次出现时位置和大小均可不同。试使用享元模式设计该文档编辑器。
// 抽象享元类 - 声明公共方法,向外界提供内部数据,由外界提供外部数据 abstract class Flyweight { public abstract String type(); public void insert(String location, String size) { System.out.println("插入类型:" + type() + ",位置:" + location + ",大小:" + size); } } // 具体享元类 - 实现抽象方法,内部数据的状态与对象状态一致,内部状态不能被修改 class Picture extends Flyweight { @Override public String type() { return "图片类型"; } } class Cartoon extends Flyweight { @Override public String type() { return "动画类型"; } } class Video extends Flyweight { @Override public String type() { return "视频类型"; } } class FlyweightFactory { // 使用单例模式,维持单一的工厂对象 private static FlyweightFactory factory = new FlyweightFactory(); private Map<String, Flyweight> map = new HashMap<>(); private FlyweightFactory() { Flyweight picture = new Picture(); Flyweight cartoon = new Cartoon(); Flyweight video = new Video(); map.put("picture", picture); map.put("cartoon", cartoon); map.put("video", video); } // 静态工厂方法 - 返回工厂对象 public static FlyweightFactory getInstance(){ return factory; } public Flyweight getFlyweight(String key) { if(map.containsKey(key)){ return map.get(key); } throw new RuntimeException("flyweight cann't support"); } } public class FlyweightPattern { public static void main(String[] args) { final FlyweightFactory factory = FlyweightFactory.getInstance(); Flyweight picture1 = factory.getFlyweight("picture"); Flyweight picture2 = factory.getFlyweight("picture"); Flyweight cartoon1 = factory.getFlyweight("cartoon"); Flyweight cartoon2 = factory.getFlyweight("cartoon"); Flyweight video = factory.getFlyweight("video"); picture1.insert("/Pic 下","273KB"); picture2.insert("/Pic 下","1.23MB"); cartoon1.insert("/cartoon 下","80MB"); cartoon2.insert("/cartoon 下","81MB"); video.insert("/video 下","1.3GB"); } }