目 录CONTENT

文章目录

享元模式

半糖
2024-08-22 / 0 评论 / 0 点赞 / 22 阅读 / 10800 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2024-08-23,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

名称

享元模式(FLYWEIGHT)

目的

运用共享技术有效地支持大量细粒度的对象

适用性

Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都可以使用Flyweight模式:

  • 一个应用程序使用了大量的对象。

  • 完全由于使用大量的对象,造成很大的存储开销。

  • 对象的大多数状态都可变为外部状态。
    内部状态:指的是对象的不变数据或者共享数据,这些数据是对象的核心组成部分,并且不会随着环境的变化而变化。这部分状态可以直接存储在享元对象内部,可以被多个对象共享。
    外部状态:指的是对象的可变数据或与特定上下文相关的数据,这些数据依赖于对象的使用环境,不能被共享。这部分状态通常不在享元对象内部存储,而是通过外部传递给享元对象,在需要的时候才设置。

  • 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。

  • 应用程序不依赖于对象标识(内存地址)。由于Flyweight对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。

结构

享元(Flyweight):描述一个接口,通过这个接口flyweight可以接受并作用于外部状态

具体享元(ConcreteFlyweight)

  • 实现Flyweight接口,并为内部状态(如果有的话)增加存储空间。ConcreteFlyweight对象必须是可共享的。它所存储的状态必须是内部的;也就是说它必须独立于ConcreteFlyweight对象的场景。

  • 非共享具体享元(UnsharedConcreteFlyweigh):并非所有的Flyweight子类都需要被共享。Flyweight接口使共享成为可能,但它并不强制共享。在Flyweight对象结构的某些层次,UnsharedConcreteFlyweight对象通常将ConcreteFlyweight对象作为子节点

享元工厂(FlyweightFactory)

  • 创建并管理flyweight对象。

  • 确保合理地共享flyweight。当用户请求一个flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)。

客户端(Client)

  • 维持一个对flyweight的引用。

  • 计算或存储一个或多个flyweight的外部状态。

协作

  1. flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于ConcreteFlyweight对象之中;而外部对象则由Client对象存储或计算。当用户调用flyweight对象的操作时,将该状态传递给它。

  2. 用户不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享。

共享flyweight如下面对象图所示

效果

优点

  1. 节省内存。通过共享对象而不是创建新的对象,减少了内存中对象的数量,从而降低了内存消耗。

  2. 提高性能。减少对象创建的数量可以降低垃圾回收的压力,提高系统的整体性能。

  3. 简化对象创建。对于需要大量相似对象的情况,享元模式可以简化创建过程,减少不必要的重复工作。

缺点

  1. 外部状态管理复杂。为了使对象可以共享,需要将一些不能共享的状态(即外部状态)从对象内部移出,这可能会增加程序的复杂性。

  2. 运行时性能开销。在运行时,需要读取享元模式的外部状态来组合完整的对象状态,这可能会稍微增加运行时间。

  3. 设计复杂度增加。实现享元模式可能需要对现有系统进行较大的重构,以支持对象的共享机制,这可能会增加系统的维护成本。

怎么优化享元模式?

使用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 来改变缓存的上下限

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区