名称
享元模式(FLYWEIGHT)
目的
运用共享技术有效地支持大量细粒度的对象
适用性
Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都可以使用Flyweight模式:
一个应用程序使用了大量的对象。
完全由于使用大量的对象,造成很大的存储开销。
对象的大多数状态都可变为外部状态。
内部状态:指的是对象的不变数据或者共享数据,这些数据是对象的核心组成部分,并且不会随着环境的变化而变化。这部分状态可以直接存储在享元对象内部,可以被多个对象共享。
外部状态:指的是对象的可变数据或与特定上下文相关的数据,这些数据依赖于对象的使用环境,不能被共享。这部分状态通常不在享元对象内部存储,而是通过外部传递给享元对象,在需要的时候才设置。
如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
应用程序不依赖于对象标识(内存地址)。由于Flyweight对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。
结构
享元(Flyweight):描述一个接口,通过这个接口flyweight可以接受并作用于外部状态
具体享元(ConcreteFlyweight):
实现Flyweight接口,并为内部状态(如果有的话)增加存储空间。ConcreteFlyweight对象必须是可共享的。它所存储的状态必须是内部的;也就是说它必须独立于ConcreteFlyweight对象的场景。
非共享具体享元(UnsharedConcreteFlyweigh):并非所有的Flyweight子类都需要被共享。Flyweight接口使共享成为可能,但它并不强制共享。在Flyweight对象结构的某些层次,UnsharedConcreteFlyweight对象通常将ConcreteFlyweight对象作为子节点
享元工厂(FlyweightFactory):
创建并管理flyweight对象。
确保合理地共享flyweight。当用户请求一个flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)。
客户端(Client):
维持一个对flyweight的引用。
计算或存储一个或多个flyweight的外部状态。
协作
flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于ConcreteFlyweight对象之中;而外部对象则由Client对象存储或计算。当用户调用flyweight对象的操作时,将该状态传递给它。
用户不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享。
共享flyweight如下面对象图所示
效果
优点
节省内存。通过共享对象而不是创建新的对象,减少了内存中对象的数量,从而降低了内存消耗。
提高性能。减少对象创建的数量可以降低垃圾回收的压力,提高系统的整体性能。
简化对象创建。对于需要大量相似对象的情况,享元模式可以简化创建过程,减少不必要的重复工作。
缺点
外部状态管理复杂。为了使对象可以共享,需要将一些不能共享的状态(即外部状态)从对象内部移出,这可能会增加程序的复杂性。
运行时性能开销。在运行时,需要读取享元模式的外部状态来组合完整的对象状态,这可能会稍微增加运行时间。
设计复杂度增加。实现享元模式可能需要对现有系统进行较大的重构,以支持对象的共享机制,这可能会增加系统的维护成本。
怎么优化享元模式?
使用Flyweight模式时,传输、查找和/或计算外部状态都会产生运行时的开销,然而,空间上的节省抵消了这些开销,所以这是一个以空间换时间的策略。
从空间上来看,存储节约由一下节点决定:
① 因为共享而减少的实例数量
② 对象内部状态的数量
③ 外部状态是计算还是存储的
从①来看,共享的享元和享元实例越多,存储节约越多;共享的享元实例越多,时间节约会减少,这点需要平衡;从②来看内部状态(共享状态)越多,存储节约越多;从③来看,如果外部状态是计算的,存储节约越多,但是相应的时间节约越少,这需要根据情况来看哪个更划算。
应用
问题
当我们使用绘图工具绘制一个形状时,比如一个圆,首先会选择一个颜色的圆,然后把操作界面的某个坐标点(像素点)作为圆心,然后选择想要的直径,至此,一个圆就画好了。这个过程使用享元模式可以提升内存利用率,首先定义形状作为享元(可以扩展正方形等),然后定义一个享元工厂用来获取具体享元(圆,包含内部状态:颜色)和具体享元详情(圆详情,包含外部状态:经度、纬度、直径),获取对象的方法还是用享元作为引用来接收,客户端(人)获取具体享元对象绘制具体图形,获取具体享元详情对象绘制具体详情图形。
示例
UML
代码示例
享元:形状,具体享元可以是圆,也可以是正方形等,draw()为绘制图形方法
package com.ysj.part4.flyweight.flyweight;
public abstract class Shape {
public abstract void draw();
}
具体享元:圆,继承形状Shape,享元的内部状态color表示颜色,重写draw()
package com.ysj.part4.flyweight.concreteFlyweight;
import com.ysj.part4.flyweight.flyweight.Shape;
/**
* 需要共享的子类
*/
public class Circle extends Shape {
public String color;
public Circle(String color){
this.color = color;
}
public void draw() {
System.out.println("画了一个" + color +"的圆形");
}
}
具体享元详情:圆详情,继承圆Circle,享元的外部状态lat、lng、diameter分别表示纬度、经度、直径,重写draw()
package com.ysj.part4.flyweight.unsharedConcreteFlyweight;
import com.ysj.part4.flyweight.concreteFlyweight.Circle;
/**
* 不需要共享的子类
*/
public class DetailCircle extends Circle {
//位置
private Integer lat;
private Integer lng;
//直径
private Integer diameter;
public DetailCircle(String color, Integer lat, Integer lng, Integer diameter) {
super(color);
this.lat = lat;
this.lng = lng;
this.diameter = diameter;
}
@Override
public void draw() {
System.out.println("画了一个位置为(" + lat + "," + lng + ") ,直径为" + diameter + this.color + " 的圆形 ");
}
}
享元工厂:形状工厂,使用HashMap存储各种颜色的圆,getShape()根据颜色获取缓存或者新创建的圆对象,getDetailShape()获取圆详情对象,getSum()统计缓存了多少个颜色的圆对象。这个工厂可以改造成支持获取各个形状的具体享元,用颜色、形状、还有其他参数(其他方法也行,只要保证条件一致时只有一个key)按照一定规律排序后拼接作为key。
package com.ysj.part4.flyweight.flyweightFactory;
import com.ysj.part4.flyweight.concreteFlyweight.Circle;
import com.ysj.part4.flyweight.flyweight.Shape;
import com.ysj.part4.flyweight.unsharedConcreteFlyweight.DetailCircle;
import java.util.HashMap;
import java.util.Map;
public class FlyweightFactory {
static Map<String, Shape> shapes = new HashMap<String, Shape>();
public static Shape getShape(String key) {
Shape shape = shapes.get(key);
//如果shape==null,表示不存在,则新建,并且保持到共享池中
if (shape == null) {
shape = new Circle(key);
shapes.put(key, shape);
}
return shape;
}
public static Shape getDetailShape(String key, Integer lat, Integer lng, Integer diameter) {
return new DetailCircle(key, lat, lng, diameter);
}
public static int getSum() {
return shapes.size();
}
}
客户端:绘制图形的人
package com.ysj.part4.flyweight;
import com.ysj.part4.flyweight.flyweight.Shape;
import com.ysj.part4.flyweight.flyweightFactory.FlyweightFactory;
/**
* 享元模式
* 运用共享技术有效地支持大量细粒度的对象
*/
public class Client {
public static void main(String[] args) {
//使用享元模式中共享的子类
System.out.println("============使用享元模式中共享的子类============");
Shape shape1 = FlyweightFactory.getShape("红色");
shape1.draw();
Shape shape2 = FlyweightFactory.getShape("灰色");
shape2.draw();
Shape shape3 = FlyweightFactory.getShape("绿色");
shape3.draw();
Shape shape4 = FlyweightFactory.getShape("红色");
shape4.draw();
Shape shape5 = FlyweightFactory.getShape("灰色");
shape5.draw();
Shape shape6 = FlyweightFactory.getShape("灰色");
shape6.draw();
System.out.println("一共绘制了" + FlyweightFactory.getSum() + "种颜色的圆形");
//使用享元模式中非共享的子类
System.out.println("============使用享元模式中非共享的子类============");
Shape shape7 = FlyweightFactory.getDetailShape("灰色", 100, 100, 200);
shape7.draw();
}
}
已知应用
Java
Java 中的 String 类利用享元模式来存储字符串。String 对象是不可变的,因此多个相同的字符串字面量会共享同一内存位置,从而避免了不必要的内存占用。
Java 中,Integer 类的 valueOf 方法实现了一个简单的享元模式(使用“=”赋值也一样)。当调用 Integer.valueOf(int) 方法时,如果传递的整数值在特定范围内(默认为 -128 到 127),那么方法会返回一个预先创建并缓存的对象,而不是创建一个新的 Integer 对象。
扩展:
在写代码的时候,如果用“==”比较两个Integer对象,且两个对象数值相等,数值在[-128,127]范围内的话比较结果为true,数值在[-128,127]范围外的话比较结果为false,出现这种灵异现象就是因为赋值的时候Java使用到了享元模式,且用“==”比较的是地址,范围内的对象是缓存中取的,所以地址相同,范围外的对象因为没有缓存,所以地址不同。
那为什么既然用到了享元模式,不把Integer的享元实例范围扩展到Integer的极限呢? 因为这样做会导致两个严重的后果,一是会消耗大量的内存而且很多数字用不到,二是查找的时候性能会因为实例数过多而显著下降。[-128,127]的数值已经满足了大多数时候的使用,没必要缓存全部,如果需要扩大缓存范围,也可以通IntegerCache.high 和 IntegerCache.low 来改变缓存的上下限
评论区