UML软件工程组织

如何用Flyweight来提高程序性能
作者:David Geary 陈姣姣翻译 
概要

分配太多的对象到你的应用程序中,将有损程序的性能。在这部分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 框架的贡献者之一。

来源:塞迪网

 

版权所有:UML软件工程组织