概要
分配太多的对象到你的应用程序中,将有损程序的性能。在这部分Java
设计模式中, 作者David Geary 将向你演示如何实现Flyweight设计模式。这个模式将大大的削减你的程序创建的对象,降低程序内存的占用,以及增强程序的性能。
不管证明与否,有时真能感觉到Java
程序运行很慢。与其他语言——如C++——可以编译成设备代码的那些语言相比,Java是在运行时进行解释;其实所有其他的语言都一样,编译的设备代码的运行速度比解释的字节码的运行速度要快。当然,你可以会争辩说:“Java之所以能自成一派,主要归功于高级技术如刚好及时(JIT)编译器和Java
HotSpot。你说的基本上正确。今天的JVM就是现代科技的奇迹,它使得Java字节码具有了了不起的性能。
且不说现代JVM的功劳,你肯定也总是在寻找某种能够改进性能的编程技巧。在这部分Java
设计模式中,我就讨论了这些编程技巧中的一种。不用惊奇,它采用了这样一种设计模式:Flyweight模式。
让我们看看它是如何工作的。
注意:你可从Resources下载本文的源代码。
Flyweight模式
面向对象的设计有时在性能方面也不一致。从设计的观点来看,最好是将所有东西都封装在一个对象内,这样你可以统一的处理这些对象,重新使用他们,以及对他们进行扩展。不幸的是,
将所有东西当作对象进行建模在性能优化方面要付出不少代价。在Smalltalk时代,与Java语言相反,Smalltalk语言将所有事情当作对象建模可逐步地进行,它能提供对象和固有类型的混合。今天,Java比Smalltalk要流行的多,但很少有对JAVA的性能进行优化。因为小部份的修改对于JAVA性能的优化没有什么影响。
注意: Java 也提供静态类型检测,而Smalltalk不提供。尽管大家公认为静态类型检测很有用,但是大多数Smalltalk
开发者仍然反对使用它。
即使Java也提供对象和固有类型的混合,但是Java应用程序仍然创建出大量的对象。例如:树的行数可以不受限制,所以如果你将每一行都当作一个独立的Swing
组件来模拟,你就遇到大麻烦了。对于细粒度的对象如树节点来说,你可以实现Flyweight模式,创建共享对象的一个小型池,它可有效的削减创建的对象树。不久以后,你就会学会如何实现flyweights模式并且共享他们;同时,也可以充分地理解:flyweights就是共享的对象,使用flyweights可使得性能得到实质上的增强。
在 Design Patterns一书中,四人组(GOF)作者们是这样描述Flyweight
模式的:
使用共享对象可有效地支持大量的细粒度的对象。
图1展示了Flyweight 设计模式的一个类图。
图1. Flyweight 类图
Flyweights可通过flyweight factory的例子来说明,这个flyweight
factory创建了一定数量的flyweights并将他们定期的发放给客户端,一次发放一个。
这些flyweights
可根据某些标准显示。例如:你有一个知道如何划出直线的直线对象池。这时,
flyweight factory就能为每一个直线颜色创建一个直线对象,如一个对象用于白线,另一个对象用于蓝线。这些线,也就是flyweights,只要你画白线或者画蓝线,他们就会重新使用。如果你要画1000条白线和6000条蓝线,实际上只显示两条线,而不是7000条线。
图2的流程图描述了客户端和flyweight factory的交互过程。
图2. Flyweight流程图
客户端程序并不直接显示flyweights;相反的它们只是从flyweight
factory中读取flyweight。flyweight factory首先检查自己是否有符合特别标准(如,蓝线或者白线)的
flyweight ;如果有,就返回对该flyweight的访问。如果它不能找到符合标准的flyweight
,就会自己产生一个,加到共享池内,并将它返回给客户端程序。i
你可能不知道如何重新使用flyweights。例如:直线flyweight
如何才能画出所有这些位置和长度都不同的指定颜色的直线呢?要完成这个任务,你可将flyweight状态劈为两个状态:intrinsic
状态(如直线颜色)和extrinsic
状态(直线位置和长度)。这样flyweight
保存了内在状态,而外部状态必须在运行过程中传递给flyweight。
通过具体化外部状态,你可以轻松的重新使用适合于各种不同外部状态的flyweight。
Flyweight广泛存在于Java程序中,从简单的字符串到Swing
树节点再到组件边界都有它的存在。本文接下来将讨论这些flyweight并举例说明如何实现你自己的flyweight。
Java字符串
因为字符串在大多数应用程序中的出现频率较高,Java必须保持足够长度大跨步地前进以保证字符串的执行良好。首先,字符串为恒量,所以他们不必是线程安全的。因为同步的代价高,所以拖延执行的字符串被搁置起来。第二,正与你所猜得一样,在编译时间内指定的字符串是flyweights——包含同样特征序列的字符串是共享的。这种共享可以大大的削减内存的占用,从而优化性能。
Example 1的应用程序演示了Java 字符串的flyweight
性质。
Example 1. Flyweight strings
public class StringTest {
public static void main(String[] args) {
String fly = "fly", weight = "weight";
String fly2 = "fly", weight2 = "weight";
System.out.println(fly == fly2);
// fly and fly2 refer to the same String instance
System.out.println(weight == weight2);
// weight and weight2 also refer to
//the same String instance
String distinctString = fly + weight;
System.out.println(distinctString == "flyweight");
// flyweight and "flyweight" do not
// refer to the same instance
String flyweight = (fly + weight).intern();
System.out.println(flyweight == "flyweight");
// The intern() method returns a flyweight
}
}
|
该应用程序的输出为:true true false true.
在上述的应用程序中,字符串fly 和 fly2
指代同一个字符串对象(重新调用时,若它比较的对象是同一个对象则==
操作符求得的值为true )。weight 和weight2可依此类推。
在运行时间内计算的字符串,如上述程序中的distinctString默认为非flyweight;但是,你可以使用方法String.intern()来促进这个问题的解决。顺便说一句,这个方法String.intern()的名字真不好,老让人想起医疗问题,但是它可以为运行时间内计算的字符串返回flyweight。
因为Java编译器可实现用于字符串的 Flyweight模式,所以我不能说明它是如何实现这个模式的。现在,让我们转为讨论在Java
API内实现的另外一个flyweight 。
Swing 边界
购买Swing可配送该组件的全套边界组件,包括直线、标题、斜角等等。图3显示的应用程序安装了带有斜角边界组件的两个面板。
Figure 3. Swing borders
你可以使用很多方法来实现组件边界。如GOF的书中,就展示了如何使用Decorator模式(见175页)来实现组件边界。但是在Swing中,他们是作为用于最高性能的flyweight来实现的。另外,只要你愿意,Swing允许你每个组件都显示一个单一的边界,但是他也提供了一个可以创建flyweight边界的边界工厂,这样一个单一边界可被多个组件共享。Example
2,用于图3应用程序的边界flyweight的清单阐明了边界工厂的用途。
Example 2. Border flyweights
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
public class Test extends JFrame {
public static void main(String[] args) {
Test test = new Test();
test.setBounds(20,20,250,150);
test.show();
}
public Test() {
super("Border flyweights");
JPanel panel = new JPanel(), panel2 = new JPanel();
Border border = BorderFactory.createRaisedBevelBorder();
Border border2 = BorderFactory.createRaisedBevelBorder();
panel.setBorder(border);
panel.setPreferredSize(new Dimension(100,100));
panel2.setBorder(border2);
panel2.setPreferredSize(new Dimension(100,100));
Container contentPane = getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.add(panel);
contentPane.add(panel2);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
System.exit(0);
}
});
if(border == border2)
System.out.println("bevel borders are shared");
else
System.out.println("bevel borders are NOT shared");
}
}
|
上述应用程序的输出为:bevel borders are shared,它表示两个面板共享一个单一的边界。注意BorderFactory.createRaisedBevelBorder()
有点用词不当,因为如果工厂中已经有斜角边界的话,它实际上没有创建任何东西。
边界拥有方法paintBorder(),当描绘边界组件时,该组件可以调用方法paintBorder()。
下面就是这个方法的标记:
public void paintBorder(Component c,
Graphics g,
int x,
int y,
int width,
int height)
|
传递给paintBorder()的参数表示边界的外部状态,即组件,用来绘图的图形对象,以及边界的范围。因为边界也可以是flyweight,因此它也可被多个组件共享,而边界本身并不保存外部状态。
Swing树节点
Swing 树节点也是适合Flyweight
模式的极好的候选项。看一看图4,它显示的Swing
树可作为文件资源管理器使用。
Figure 4. Swing trees
作为Swing 的设计的实证,图4中的这一类文件资源管理器是很容易实现的。实际上,它的代码,空白行等等加起来只有144行。作为Swing
的设计的实证,这种文件资源管理器是fast。只要你愿意,你可以下载本文的源代码,将这个文件资源管理器稍微改造一下:使用许多文件扩展一些目录,这样你就会拥有与图4的树类似的带有许多节点的树(注意滚动块的范围)。然后抓住滚动块,尽可能地迅速上下移动它。你就会发现它的性能极好。
Swing树的速度极快,因为他们只使用了一个单一组件来表示树中的所有节点。
这个组件是树单元制造者使用极度详细的方法TreeCellRenderer.getTreeCellRendererComponent()来创建的。这个方法的标记列表如下:
public Component getTreeCellRendererComponent(JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus)
|
至此,getTreeCellRendererComponent()的所有参数都向你表明:该方法返回的组件是
flyweight。与Swing边界类似,外部状态传递给树单元制造者所以制造组件可以共享。方法getTreeCellRendererComponent()
依靠外部状态来设置组件的属性。例如:方法
getTreeCellRendererComponent()就可以使用文件夹图标安装文件夹组件;如果不能安装,它会使用文档图标来安装文件夹组件。
另外,Swing表格也采用与Swing树类似的方式来创建表格单元。一个组件就可以代表
一个单一表格的所有单元,所以Swing表格也会具有极好的性能。
至此,我已经详细地讨论Flyweight 模式在Java
中的三种不同的应用。你应该能完全理解Flyweight
模式了吧,现在让我们从头开始实现一个 flyweight 。
自己动手
接下来本文将带你实现一个直线flyweight
,它与本文开头讨论过的直线flyweight
类似。我们将用三个方法来完成这件工作。第一,我不借助直线对象,只是重复地在图形语境内绘出10,000直线。第二,我实现一个不能共享的简单的Line
类。第三,我就这个简单的类变成flyweight。图5显示了重复描绘直线的应用程序。
图5 使用Swing描绘直线
不使用直线对象的制图
Example 3 列表显示了图5的应用程序。
Example 3. Drawing lines without line objects
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class Test extends JFrame{
private static final Color colors[] = { Color.red, Color.blue,
Color.yellow, Color.orange,
Color.black, Color.white };
private static final int WINDOW_WIDTH=400,
WINDOW_HEIGHT=400,
NUMBER_OF_LINES=10000;
public Test() {
Container contentPane = getContentPane();
contentPane.setLayout(new BorderLayout());
JButton button = new JButton("draw lines");
final JPanel panel = new JPanel();
contentPane.add(panel, BorderLayout.CENTER);
contentPane.add(button, BorderLayout.SOUTH);
setBounds(20,20,WINDOW_WIDTH,WINDOW_HEIGHT);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Graphics g = panel.getGraphics();
for(int i=0; i < NUMBER_OF_LINES; ++i) {
g.setColor(getRandomColor());
g.drawLine(getRandomX(), getRandomY(),
getRandomX(), getRandomY());
}
}
});
}
public static void main(String[] args) {
Test test = new Test();
test.show();
}
private int getRandomX() {
return (int)(Math.random()*WINDOW_WIDTH);
}
private int getRandomY() {
return (int)(Math.random()*WINDOW_HEIGHT);
}
private Color getRandomColor() {
return colors[(int)(Math.random()*colors.length)];
}
}
|
上述应用程序创建了带有两个面板的窗口。当你点击底部面板上的按钮时,该应用程序就会使用任意颜色,在任意位置,绘出任意长度的直线。这些直线是由图形语境中的drawLine()
方法描绘的。
接下来,我们不直接使用图形语境,而是实现一个类
Line ,用它来描绘直线。
简单类Line的实现
可能你会认为使用图形语境来描绘直线不是非常面向对象的,所以你想使用一个类Line
来封装这个功能。现在就让我们来完成这个功能的封装。
Example 4 列出的就是修改过的应用程序。
Example 4. Drawing lines with a heavyweight line object
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class Test extends JFrame{
public Test() {
...
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Graphics g = panel.getGraphics();
for(int i=0; i < NUMBER_OF_LINES; ++i) {
Color color = getRandomColor();
System.out.println("Creating " + color + " line");
Line line = new Line(color,
getRandomX(), getRandomY(),
getRandomX(), getRandomY());
line.draw(g);
}
}
});
}
...
}
|
我只写出了相关的代码;程序的其余部分与Example 3中的相应部分一样。上述的应用程序创建了10,000
个直线对象并告诉每一个对象自我描绘。Example 5
列表显示了这个类Line 。
Example 5. A heavyweight Line class
import java.awt.*;
public class Line {
private Color color = Color.black;
private int x, y, x2, y2;
public Line(Color color, int x, int y, int x2, int y2) {
this.color = color;
this.x = x; this.y = y;
this.x2 = x2; this.y2 = y2;
}
public void draw(Graphics g) {
g.setColor(color);
g.drawLine(x, y, x2, y2);
}
}
|
上述的类Line
保留了它的颜色和端点,并使用这些端点来描绘直线。
flyweight Line的实现
显然,我们不再按照上述方式创建10,000
直线,现在我们将直线的数量减为6条,一种颜色对应一条直线。Example
6 列表显示了修改后的应用程序。
Example 6. Drawing lines with a flyweight line object
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class Test extends JFrame {
...
public Test() {
...
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Graphics g = panel.getGraphics();
for(int i=0; i < NUMBER_OF_LINES; ++i) {
Line line = LineFactory.getLine(getRandomColor());
line.draw(g, getRandomX(), getRandomY(),
getRandomX(), getRandomY());
}
}
});
}
...
}
|
差不多就这样了。现在直线对象可从直线工厂中获得,直线工厂返回6条共享直线中的一个。Example
7 列出了这个工厂。
Example 7. The line factory
import java.util.HashMap;
import java.awt.Color;
public class LineFactory {
private static final HashMap linesByColor = new HashMap();
public static Line getLine(Color color) {
Line line = (Line)linesByColor.get(color);
if(line == null) {
line = new Line(color);
linesByColor.put(color, line);
System.out.println("Creating " + color + " line");
}
return line;
}
}
|
直线工厂保留了直线的HashMap,HashMap可键入颜色。当你调用getLine()函数向工厂请求一条直线时,工厂首先检查HashMap是否存在这种颜色的直线;若有的话,则返回直线。否则,工厂创建一个新的直线,把它保存在HashMap中,并返回该直线。无论是哪种情况,调用者都能得到一条共享的Line
对象。
Example 8 列出了修改后的类Line。
Example 8. A flyweight Line implementation
import java.awt.*;
public class Line {
private Color color;
public Line(Color color) {
this.color = color;
}
public void draw(Graphics g, int x, int y, int x2, int y2) {
g.setColor(color);
g.drawLine(x, y, x2, y2);
}
}
|
注意上述类Line比 Example 5中的类 Line
要简单。为什么?因为上述的类Line
已经清除了外部状态即直线的端点。由于上述的类Line
不再保留外部状态,也由于外部状态从客户端传递到方法draw(),所以这些类的实例得以共享。
你的应用程序可以翱翔了!
从历史的观点来看,大量的(通常为小型的)对象存在于Java
应用程序中可对其性能产生破坏作用,虽然现代JVMs已经极大的减低了你必须为这种过多的对象所付出的代价。
如果你的应用程序需要显示的许多对象属于同一种类型,你可以考虑使用Flyweight
模式来共享一定数量的这些对象。
【原文出处】http://www.javaworld.com/javaworld/jw-07-2003/jw-0725-designpatterns-p1.html
【关于作者】
David Geary 的著作有: Core JSTL Mastering the JSP Standard Tag
Library (Prentice Hall, 2002; ISBN: 0131001531)、 Advanced
JavaServer Pages (Prentice Hall, 2001; ISBN: 0130307041)、以及
Graphic Java s系列 (Prentice Hall)。David使用各种不同的语言来开发面向对象的软件已经有18年了。自从1994年拜读了GOF
的Design Patterns
一书后,他成为了设计模式的活跃分子,并在Smalltalk、
C++、和 Java语言中使用和实现了设计模式。 在1997年,David成为全职作家,偶尔也举行讲座,偶尔也提供咨询。他现在是定义JSP
标准标记库和JavaServer Faces的专家组成员,也是Apache
Struts JSP 框架的贡献者之一。
来源:塞迪网
|