//客户端类
public class Client { public static void main(String[] args) { List<Character> compositeState = new ArrayList<Character>(); compositeState.add('a'); compositeState.add('b'); compositeState.add('c'); compositeState.add('a'); compositeState.add('b'); FlyweightFactory flyFactory = new FlyweightFactory(); Flyweight compositeFly1 = flyFactory.factory(compositeState); Flyweight compositeFly2 = flyFactory.factory(compositeState); compositeFly1.operation("Composite Call"); System.out.println("---------------------------------"); System.out.println("复合享元模式是否可以共享对象:" + (compositeFly1 == compositeFly2)); Character state = 'a'; Flyweight fly1 = flyFactory.factory(state); Flyweight fly2 = flyFactory.factory(state); System.out.println("单纯享元模式是否可以共享对象:" + (fly1 == fly2)); } }
运行结果如下:
享元模式与其他模式的联用
在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用 简单工厂模式来生成享元对象;
在一个系统中,通常只有唯一一个享元工厂,因此享元工厂类可以使用单例模式进行设计;
享元模式可以结合组合模式形成复合享元模式,统一对享元对象设置外部状态。
四、享元模式的分析
享元模式是一个 考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。
享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
典型的享元工厂类代码:
public class FlyweightFactory { private HashMap flyweights = new HashMap(); public Flyweight getFlyweight(String key) { if(flyweights.containsKey(key)) { return (Flyweight)flyweights.get(key); } else { Flyweight fw = new ConcreteFlyweight(); flyweights.put(key,fw); return fw; } } }
享元模式以共享的方式高效地支持大量的细粒度对象,享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。其中:
内部状态 是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
外部状态 是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。
典型的享元类代码:
public class Flyweight { //内部状态作为成员属性 private String intrinsicState; public Flyweight(String intrinsicState) { this.intrinsicState = intrinsicState; } public void operation(String extrinsicState) { ...... } }
五、享元模式在源码中的应用以及优缺点对比
享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片对象,通过在应用程序中设置该图片出现的位置,可以实现该图片在不同地方多次重复显示。
广义上讲,在JDK类库中定义的String类也是使用享元模式的典型。
Java中将String类定义为final(不可改变的),JVM中字符串一般保存在字符串常量池中,Java会确保一个字符串在常量池中只有一个拷贝,这个字符串常量池在JDK6.0之前是位于常量池中,位于永久代;而在JDK 7.0中,JVM将其从永久代中拿出来放置于堆中。
我们做一个测试
String类的final修饰的,以字面量的形式创建String变量时,JVM会在编译期间就把该字面量“hello”放到字符串常量池中,由java程序启动的时候就加载到内存中了。这个字符串常量池的特点就是有且只有一份相同的字面量,如果有其它相同的字面量,JVM则返回这个字面量的引用,如果没有相同的字面量,则在字符串常量池创建这个字面量并返回它的引用。 由于s2指向的字面量"hello"在常量池中已经存在了(s1先于s2),于是JVM就返回这个字面量绑定的引用,所以s1==s2; s3中字面量的拼接其实就是"hello",JVM在编译期间就已经对它进行优化,所以s1和s3是相等的。 s4中new String("lo")生成了两个对象,lo,new String("lo"),lo存在字符串常量池,lo中存在字符串常量池,new String("lo")存储在堆中,String s4 = "hel"+new String("lo")实际上是两个对象的相加,编译器不会进行优化,相加的结果存在堆中,而s1存在字符串常量池中,当然不相等。s1==s9的原理是一样的 s4==s5两个相加的结果都在堆中,不用说,肯定不相等。 s1==s6中,s5.intern()方法能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中(字符串常量池的内容是程序启动的时候就已经加载好了),如果字符串常量池中有该对象对饮的字面量,则返回该字面量在字符串常量池中的引用,否则,创建复制一份字面量到字符串常量池并返回它的引用。因此s1==s6输出true。