Java-设计模式之享元模式

享元模式

引子

让我们先来复习下 java 中 String 类型的特性:String 类型的对象一旦被创造就不可改变;当两个 String 对象所包含的内容相同的时候,JVM 只创建一个 String 对象对应这两个不同的对象引用。让我们来证实下着两个特性吧(如果你已经了解,请跳过直接阅读第二部分)。

// 先来验证下第二个特性:
public class TestPattern {
    
    
    public static void main(String[] args){
    
    
        String n = "I Love Java";
        String m = "I Love Java";
        System.out.println(n==m);
    }
}

这段代码会告诉你 n==m 是 true,这就说明了在 JVM 中 n 和 m 两个引用了同一个 String对象。

那么接着验证下第一个特性:

在系统输出之前加入一行代码“m = m + “hehe”;”,这时候 n==m 结果为 false,为什么刚才两个还是引用相同的对象,现在就不是了呢?原因就是在执行后添加语句时,m 指向了一个新创建的 String 对象,而不是修改引用的对象。

String 类型的设计避免了在创建 N 多的 String 对象时产生的不必要的资源损耗,可以说是享元模式应用的范例,那么让我们带着对享元的一点模糊的认识开始,来看看怎么在自己的程序中正确的使用享元模式!

定义与分类

享元模式英文称为“Flyweight Pattern”,又译为羽量级模式或者蝇量级模式。我非常认同将 Flyweight Pattern 翻译为享元模式,因为这个词将这个模式使用的方式明白得表示了出来。

**享元模式的定义为:采用一个共享类来避免大量拥有相同内容的“小类”的开销。**这种开销中最常见、直观的影响就是增加了内存的损耗。享元模式以共享的方式高效的支持大量的细粒度对象,减少其带来的开销。

在名字和定义中都体现出了共享这一个核心概念,那么怎么来实现共享呢?事物之间都是不同的,但是又存在一定的共性,如果只有完全相同的事物才能共享,那么享元模式可以说就是不可行的;因此我们应该尽量将事物的共性共享,而又保留它的个性。为了做到这点,享元模式中区分了内蕴状态和外蕴状态。内蕴状态就是共性,外蕴状态就是个性了。

内蕴状态存储在享元内部,不会随环境的改变而有所不同,是可以共享的;外蕴状态是不可以共享的,它随环境的改变而改变的,因此外蕴状态是由客户端来保持(因为环境的变化是由客户端引起的)。在每个具体的环境下,客户端将外蕴状态传递给享元,从而创建不同的对象出来。

我们引用《Java 与模式》中的分类,将享元模式分为:单纯享元模式和复合享元模式。在下一个小节里面我们将详细的讲解这两种享元模式。

结构

先从简单的入手,看看单纯享元模式的结构。

  1. 抽象享元角色:为具体享元角色规定了必须实现的方法,而外蕴状态就是以参数的形式通过此方法传入。在 Java 中可以由抽象类、接口来担当。

  2. 具体享元角色:实现抽象角色规定的方法。如果存在内蕴状态,就负责为内蕴状态提供存储空间。

  3. 享元工厂角色:负责创建和管理享元角色。要想达到共享的目的,这个角色的实现是关键!

  4. 客户端角色:维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。

来用类图来形象地表示出它们的关系吧。

在这里插入图片描述

再来看看复合享元模式的结构。

  1. 抽象享元角色:为具体享元角色规定了必须实现的方法,而外蕴状态就是以参数的形式通过此方法传入。在 Java 中可以由抽象类、接口来担当。

  2. 具体享元角色:实现抽象角色规定的方法。如果存在内蕴状态,就负责为内蕴状态提供存储空间。

  3. 复合享元角色:它所代表的对象是不可以共享的,并且可以分解成为多个单纯享元对象的组合。

  4. 享元工厂角色:负责创建和管理享元角色。要想达到共享的目的,这个角色的实现是关键!

  5. 客户端角色:维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。

统比一下单纯享元对象和复合享元对象,里面只多出了一个复合享元角色,但是它的结构就发生了很大的变化。我们还是使用类图来表示下:

在这里插入图片描述

正如你所想,复合享元模式采用了组合模式——为了将具体享元角色和复合享元角色同等对待和处理。这也就决定了复合享元角色中所包含的每个单纯享元都具有相同的外蕴状态,而这些单纯享元的内蕴状态可以是不同的。

举例

很遗憾,没有看到享元模式实用的例子。享元模式如何来共享内蕴状态的?在能见到的教学代码中,大概有两种实现方式:实用列表记录(或者缓存)已存在的对象和使用静态属性。下面的例子来自于 Bruce Eckel 的《Thinking in Patterns with java》一书。

设想一下有一个含有多个属性的对象,要被创建一百万次,并使用它们。这时候正是使用享元模式的好时机:

//这便是使用了静态属性来达到共享
//它使用了数组来存放不同客户对象要求的属性值
//它相当于享元角色(抽象角色被省略了)
class ExternalizedData {
    
    

    static final int size = 5000000;
    static int[] id = new int[size];
    static int[] i = new int[size];
    static float[] f = new float[size];

    static {
    
    
        for(int i = 0; i < size; i++)
            id[i] = i;
    }
}

//这个类仅仅是为了给 ExternalizedData 的静态属性赋值、取值
//这个充当享元工厂角色
class FlyPoint {
    
    

    private FlyPoint() {
    
    }

    public static int getI(int obnum) {
    
    
        return ExternalizedData.i[obnum];
    }

    public static void setI(int obnum, int i) {
    
    
        ExternalizedData.i[obnum] = i;
    }

    public static float getF(int obnum) {
    
    
        return ExternalizedData.f[obnum];
    }

    public static void setF(int obnum, float f) {
    
    
        ExternalizedData.f[obnum] = f;
    }

    public static String str(int obnum) {
    
    
        return "id: " +ExternalizedData.id[obnum] +
                ", i = " +
                ExternalizedData.i[obnum] +
                ", f = " +
                ExternalizedData.f[obnum];
    }
}

//客户程序
public class FlyWeightObjects {
    
    

    public static void main(String[] args) {
    
    

        for(int i = 0; i < ExternalizedData.size; i++) {
    
    
            FlyPoint.setI(i, FlyPoint.getI(i) + 1);
            FlyPoint.setF(i, 47.0f);
        }
        
        System.out.println(
                FlyPoint.str(ExternalizedData.size -1));
    }
}

另外一种实现方式大概是将已存在内蕴状态不同的对象储存在一个列表当中,通过享元工厂角色来控制重复对象的生成。而对于上面提到的复合享元模式,仅仅是在抽象享元角色下面添加一个有组合模式来构造的复合享元角色。而且复合享元中所包含的每个单纯享元都具有相同的外蕴状态,而这些单纯享元的内蕴状态往往是不同的。由于复合享元模式不能共享,所以不存在什么内外状态对应的问题。所以在复合享元类中我们不用实现抽象享元对象中的方法,因此这里采用的是透明式的合成模式。

复合享元角色仿佛没有履行享元模式存在的义务。复合享元角色是由多个具体享元角色来组成的,虽然复合享元角色不能被共享使用,但是组成它的具体享元角色还是使用了共享的方式。因此复合享元模式并没有违背享元模式的初衷。

使用优缺点

享元模式优点就在于它能够大幅度的降低内存中对象的数量;而为了做到这一步也带来了它的缺点:它使得系统逻辑复杂化,而且在一定程度上外蕴状态影响了系统的速度。

所以一定要切记使用享元模式的条件:

1)系统中有大量的对象,他们使系统的效率降低。

2)这些对象的状态可以分离出所需要的内外两部分。

外蕴状态和内蕴状态的划分以及两者关系的对应也是非常值得重视的。只有将内外划分妥当才能使内蕴状态发挥它应有的作用;如果划分失误,在最糟糕的情况下系统中的对象是一个也不会减少的!两者的对应关系的维护和查找也是要花费一定的空间(当然这个比起不使用共享对象要小得多)和时间的,可以说享元模式就是使用时间来换取空间的。可以采用相应的算法来提高查找的速度。

Guess you like

Origin blog.csdn.net/clearlxj/article/details/121373180