享元模式,也叫蝇量模式(Flyweight Pattern)。运用共享技术有效地支持大量细粒度的对象。
享元模式常用于系统底层开发,解决系统的性能问题。例如数据库连接池,里面都是创建好的连接对象,在这些连接对象中,如果有我们需要的则直接拿来用,避免重新创建,如果没有,再去创建。
享元模式能够解决重复对象的内存浪费问题,在一些不需要总是创建新对象,可以使用对象资源池时,可以从资源池中获取。这样可以降低系统内存消耗,同时提高效率。
享元模式经典的应用场景就是池技术,String常量池、数据库连接池、缓冲池等等都是享元模式的应用。
享元模式经常会配合简单工厂来使用。
上图中,ShapeFactory是获取Shape的工厂类,其实这就是使用了简单工厂模式,其中的shapeMap是一个常量池,存储了Shape类型,本例中就是Circle对象。
getCircle方法需要传入一个参数,这个参数是颜色信息,方法内部会判断常量池中是否已经存在对应颜色信息的Circle对象,如果有则直接返回,如果没有,就会创建,并放入到shapeMap中,这是享元模式的关键代码。
注意,在使用享元模式时,有时候需要区分享元对象的内部状态和外部状态,以享元对象为参考系的话,内部状态是不变的,比如上图案例中,Circle对象是享元对象,其内部状态就是确定的颜色、圆心坐标、以及半径,而外部状态,可能是不同的使用者,因为享元对象是共享的,所以面对多个使用者,其内部状态自然就要求是不变的。在复杂的业务场景,可能需要享元对象暂时保存可变的外部状态,比如在数据库连接池的享元模式中,线程需要独占一个连接对象,这里面可能就需要连接对象暂时性保存线程的标识,待释放连接后才会清除状态。这个层面的问题是需要在使用享元模式时进一步思考的。
Shape产品抽象接口
public interface Shape {
void draw();
}
Circle具体的产品类
public class Circle implements Shape {
private String color;
private int x;
private int y;
private int radius;
public Circle(String color) {
super();
this.color = color;
}
@Override
public void draw() {
System.out.println("Circle: Draw() [Color : " + color + ", x : " + x + ", y :" + y + ", radius :" + radius);
}
// ----get、set---
}
ShapeFactory享元对象工厂
public class ShapeFactory {
private static final Map circleMap = new HashMap<>();
public static Shape getCircle(String color) {
System.out.println("获取" + color + "的圆形");
Circle circle = (Circle) circleMap.get(color);
if (circle == null) {
circle = new Circle(color);
circleMap.put(color, circle);
System.out.println("Creating circle of color : " + color);
}
return circle;
}
}
Client测试类
public class Client {
private static final String[] colors = { "Red", "Green", "Blue", "White", "Black" };
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Circle circle = (Circle) ShapeFactory.getCircle(getRandomColor());
circle.setX(getRandomX());
circle.setY(getRandomY());
circle.setRadius(100);
circle.draw();
}
}
private static String getRandomColor() {
String color = colors[(int) (Math.random() * colors.length)];
return color;
}
private static int getRandomX() {
return (int) (Math.random() * 100);
}
private static int getRandomY() {
return (int) (Math.random() * 100);
}
}
输出结果:
获取White的圆形
Creating circle of color : White
Circle: Draw() [Color : White, x : 45, y :49, radius :100
获取Green的圆形
Creating circle of color : Green
Circle: Draw() [Color : Green, x : 85, y :96, radius :100
获取Blue的圆形
Creating circle of color : Blue
Circle: Draw() [Color : Blue, x : 0, y :48, radius :100
获取Red的圆形
Creating circle of color : Red
Circle: Draw() [Color : Red, x : 7, y :75, radius :100
获取Red的圆形
Circle: Draw() [Color : Red, x : 96, y :82, radius :100
获取Green的圆形
Circle: Draw() [Color : Green, x : 76, y :86, radius :100
获取Green的圆形
Circle: Draw() [Color : Green, x : 64, y :77, radius :100
获取Black的圆形
Creating circle of color : Black
Circle: Draw() [Color : Black, x : 51, y :90, radius :100
获取Red的圆形
Circle: Draw() [Color : Red, x : 55, y :22, radius :100
获取Blue的圆形
Circle: Draw() [Color : Blue, x : 25, y :87, radius :100
获取Black的圆形
Circle: Draw() [Color : Black, x : 39, y :2, radius :100
获取Black的圆形
Circle: Draw() [Color : Black, x : 80, y :10, radius :100
获取White的圆形
Circle: Draw() [Color : White, x : 10, y :96, radius :100
获取Green的圆形
Circle: Draw() [Color : Green, x : 64, y :68, radius :100
获取Red的圆形
Circle: Draw() [Color : Red, x : 56, y :85, radius :100
获取White的圆形
Circle: Draw() [Color : White, x : 87, y :44, radius :100
获取Blue的圆形
Circle: Draw() [Color : Blue, x : 39, y :77, radius :100
获取Blue的圆形
在Integer整型包装类中,用到了享元模式:
这个valueOf()相当于上一节案例中的 shapeFactory 的 getCircle(String color) 方法。
valueOf 需要传入一个 int 类型的参数用于创建 Integer 对象,可以看到,方法中会判断参数的大小,如果正好处于 IntegerCache 常量池的范围内,那么就会直接返回,如果没有,再通过new关键字来创建。下图是 IntegerCache 的部分代码:
名为 IntegerCache 的静态内部类会初始化一个 Integer 数组 cache[],用于存储 -128 到 127 之间的整数,据专家说,这些数字在实际应用中的使用频率极高,那么如果可以将这些数字以常量池的形式缓存起来,供客户端使用,那么系统的性能将会得到一定程度的提升。这就是享元模式的具体应用。
享元模式需要在工厂模式的基础上维护一个资源池,供客户端获取享元对象。
它主要解决的问题是系统中大量重复对象的创建与销毁问题。
缓存已经创建好的对象并直接返回可以极大的减少重复对象的内存占用,以及降低创建和销毁对象的系统开销。
在 JDK 的 Integer 、String 等底层实现中,都有用到享元模式。
有时候,需要区分享元对象的内部状态和外部状态。以享元对象为参考系,内部状态是不变的,而外部状态可能是不同的调用者或者其他可变信息。这在复杂的享元模式中需要谨慎留意。