设计模式(二十)Flyweight模式
当使用new关键字生成类的实例时,需要给其分配足够的内存空间。当程序中需要大量对象时,如果都是用new关键字来分配内存,将会消耗大量内存空间。Flyweight模式就是尽量避免new出实例,而是通过尽量共用已经存在的实例。
示例程序类图。这个示例程序要实现的就是给定传统的普通数字字符可以得到对应数字的“大型字符”。
1 package bigjunoba.bjtu.flyweight; 2 3 import java.io.BufferedReader; 4 import java.io.FileReader; 5 import java.io.IOException; 6 7 public class BigChar { 8 //字符名字 9 private char charname; 10 //大型字符对应的字符串,就是txt文本里面的内容 11 private String fontdata; 12 //构造函数 13 public BigChar(char charname) { 14 this.charname = charname; 15 try { 16 BufferedReader bufferedReader = new BufferedReader( 17 new FileReader("big" + this.charname + ".txt") 18 ); 19 String line; 20 StringBuffer stringBuffer = new StringBuffer(); 21 while ((line = bufferedReader.readLine()) != null) { 22 stringBuffer.append(line); 23 stringBuffer.append("\n"); 24 } 25 bufferedReader.close(); 26 this.fontdata = stringBuffer.toString(); 27 28 } catch (IOException e) { 29 this.fontdata = this.charname + "?"; 30 } 31 } 32 //显示大型字符 33 public void print() { 34 System.out.print(fontdata); 35 } 36 } 37
BigChar类是表示“大型字符”的类。这个类的构造函数中,首先会读取给定数字字符对应的.txt文件,然后将读取到的文件先存到缓冲区bufferedReader中,最后再把bufferedReader中的内容存到stringBuffer中,并用fontdata字段来保存。由于“大型字符”会消耗很多内存,因此我们需要考虑如何共享BigChar类的实例。
1 package bigjunoba.bjtu.flyweight; 2 3 import java.util.HashMap; 4 5 public class BigCharFactory { 6 7 // 管理已经生成的BigChar的实例 8 private HashMap<String, BigChar> pool = new HashMap<String, BigChar>(); 9 // Singleton模式 10 private static BigCharFactory singleton = new BigCharFactory(); 11 // 构造函数 12 private BigCharFactory() { 13 } 14 // 获取唯一的实例 15 public static BigCharFactory getInstance() { 16 return singleton; 17 } 18 // 生成(共享)BigChar类的实例 19 public synchronized BigChar getBigChar(char charname) { 20 BigChar bc = (BigChar)pool.get("" + charname); 21 if (bc == null) { 22 bc = new BigChar(charname); // 生成BigChar的实例 23 pool.put("" + charname, bc); 24 } 25 return bc; 26 } 27 }
BigCharFactory类是生成BigChar类的实例的工厂。它实现了共享实例的功能。pool字段中保存的是已经生成的BigChar类的实例,并用HashMap来管理“字符串--实例”的对应关系。pool.put("" + charname, bc);中的put方法可以将某个字符串与一个实例关联起来,就可以通过键来获取它对应的值。getInstance方法用于获取BigCharFactory类的实例,这里是Singleton模式。getBigChar是该模式的核心,首先通过get方法查找输入的字符串是否存在对应的BigChar类的实例,如果没有就创建一个,并记录对应关系,如果有就直接将对应的实例返回。
这里为什么要用synchronized关键字,给出如下解答:
1 package bigjunoba.bjtu.flyweight; 2 3 public class BigString { 4 // “大型字符”的数组 5 private BigChar[] bigchars; 6 // 构造函数 7 public BigString(String string) { 8 bigchars = new BigChar[string.length()]; 9 BigCharFactory factory = BigCharFactory.getInstance(); 10 for (int i = 0; i < bigchars.length; i++) { 11 bigchars[i] = factory.getBigChar(string.charAt(i)); 12 } 13 } 14 // 显示 15 public void print() { 16 for (int i = 0; i < bigchars.length; i++) { 17 bigchars[i].print(); 18 } 19 } 20 }
BigString类表示由BigChar组成的“大型字符串”的类。bigchars字段中保存的是BigChar类的实例。factory.getBigChar(string.charAt(i));这句话就实现了共享实例。
1 package bigjunoba.bjtu.flyweight; 2 3 public class Main { 4 public static void main(String[] args) { 5 if (args.length == 0) { 6 System.out.println("Usage: java Main digits"); 7 System.out.println("Example: java Main 1212123"); 8 System.exit(0); 9 } 10 BigString bs = new BigString(args[0]); 11 bs.print(); 12 } 13 }
Main类作为测试类,很简单就不解释了。
......##........ ..######........ ......##........ ......##........ ......##........ ......##........ ..##########.... ................ ....######...... ..##......##.... ..........##.... ......####...... ..........##.... ..##......##.... ....######...... ................ ....######...... ..##......##.... ..........##.... ......####...... ....##.......... ..##............ ..##########.... ................ ....######...... ..##......##.... ..##............ ..########...... ..##......##.... ..##......##.... ....######...... ................ ......##........ ..######........ ......##........ ......##........ ......##........ ......##........ ..##########.... ................ ..##########.... ..##......##.... ..........##.... ........##...... ......##........ ......##........ ......##........ ................ ....######...... ..##......##.... ..........##.... ......####...... ..........##.... ..##......##.... ....######...... ................ ....######...... ..##......##.... ..##......##.... ..##......##.... ..##......##.... ..##......##.... ....######...... ................ ..##########.... ..##............ ..##............ ..########...... ..........##.... ..##......##.... ....######...... ................ ....######...... ..##......##.... ..........##.... ......####...... ....##.......... ..##............ ..##########.... ................ ..##########.... ..##............ ..##............ ..########...... ..........##.... ..##......##.... ....######...... ................
测试结果如上图。
BigString类的实例的bigchars字段对应Bigchar类的实例。
Flyweight模式的类图。
关于Flyweight模式有以下需要说明的:
1.如何判断可以共享实例,就是如果要改变被共享的对象,就会对多个地方产生影响。也就是说,一个实例的改变会同时反映到所有使用该实例的地方。因此,使用该模式时需要精挑细选出那些真正应该在多个地方共享的字段。
2.Intrinsic信息被称为应当共享的信息,而Extrinsic信息被称为不应当共享的信息。
3.注意不要让被共享的实例被垃圾回收器回收了:在示例程序中,使用HashMap类管理已经生成的BigChar实例。在Java程序中可以通过new关键字分配内存空间。如果分配了过多内存,就会导致内存不足。这是,Java虚拟机就会开始垃圾回收处理。它会查看自己的内存空间中是否存在没有被使用的实例,如果存在就释放该实例,这样就可以护手可用的内存空间。如果其他对象引用了该实例,垃圾回收器就会认为“该实例正在被使用”,不会将其当做垃圾回收掉。在示例程序中,pool字段负责管理已经生成的BigChar的实例,因此,只要是pool字段管理的BigChar的实例,就不会被看作垃圾,即使该BigChar的实例实际上已经不再被BigString类的实例所使用。也就是说,只要生成了一个BigChar的实例,就会长期驻留在内存中。在示例程序中,字符串的显示处理很快就结束了,因此不会发生内存不足的问题。但是如果应用程序需要长期运行或是需要以有限的内存来运行,那么在设计程序时,开发人员就必须时刻警惕“不要让共享的实例被垃圾回收器回收了”。虽然不能显示删除实例,但可以删除对实例的引用。要想让实例可以被垃圾回收器回收掉,只需要显式地将其置于管理对象外即可。例如,只要从HashMap中移除该实例的Entry,就删除了对该实例的引用。
4.这里还要提出的是,虽然共享实例可以减少内存使用量,但是时间也是一种资源,使用new关键字生成实例会花费时间。使用Flyweight模式共享实例可以减少使用new关键字生成实例的次数,这样就可以提高程序运行速度。文件句柄和窗口句柄等也是一种资源,如果不共享实例,应用程序在运行时很容易就会达到资源极限而导致崩溃,因为操作系统中,可以同时使用的文件句柄和窗口句柄是有限制的。