Justified or not, Java is sometimes perceived as slow. Compared with other languages—such as C++—that are compiled into machine code, Java is interpreted at runtime; and all other things being equal, compiled machine code is faster than interpreted byte code. Of course, you might argue that Java can hold its own, mainly due to advanced technologies such as just-in-time (JIT) compilers and Java HotSpot, and you'd be mostly correct. Today's JVMs are modern marvels that wring amazing performance out of Java byte code.
Regardless of modern JVM efficiency, you should always look for programming techniques that boost performance. In this installment of Java Design Patterns, I discuss one of those techniques that, not surprisingly, takes the form of a design pattern: the Flyweight pattern. Let's see how it works.
The Flyweight pattern
Object-oriented design is sometimes at odds with performance. From a design standpoint, it's best to encapsulate everything in an object so you can treat those objects uniformly, reuse them, and extend them. Unfortunately, modeling everything as an object can be expensive performance-wise. A case-in-point is Smalltalk, which literally models everything as an object, as opposed to Java, which provides a mix of objects and intrinsic types. Today, Java is much more prevalent than Smalltalk, due in no small part to Java's better performance.
Note: Java also provides static type checking, which Smalltalk lacks. Although static type checking is widely regarded as beneficial, most Smalltalk developers argue against it.
Even though Java provides a mix of objects and intrinsic types, Java applications can create a huge number of objects. For example, trees can have an unlimited number of rows, so if you model each row as a distinct Swing component, you're asking for trouble. For fine-grained objects such as tree nodes, you can implement the Flyweight pattern to create a small pool of shared objects, which significantly reduces the number of objects created. Soon enough, you'll learn how to implement flyweights and share them; in the meantime, it's sufficient to understand that flyweights are shared objects and that using them can result in substantial performance gains.
In Design Patterns, the Gang of Four (GOF) authors describe the Flyweight pattern like this:
Use sharing to support large numbers of fine-grained objects efficiently.
Figure 1 shows a class diagram for the Flyweight design pattern.
Figure 1. Flyweight class diagram
Flyweights are typically instantiated by a flyweight factory that creates a limited number of flyweights and doles them out, one at a time, to clients. Those flyweights are instantiated according to some criteria. For example, you might have a pool of line objects that know how to draw lines. In that case, the flyweight factory could create one line object for each line color, such as one object for white lines and another for blue lines. Those lines, which are flyweights, get reused whenever you draw white or blue lines. If you have a drawing with 1,000 white lines and 6,000 blue lines, only two lines—instead of 7,000—are actually instantiated.
Figure 2's sequence diagram depicts runtime interaction between clients and a flyweight factory.
Figure 2. Flyweight sequence diagram
Clients don't directly instantiate flyweights; instead they get them from a factory. The factory first checks to see if it has a flyweight that fits specific criteria (e.g., a blue or white line); if so, the factory returns a reference to the flyweight. If the factory can't locate a flyweight for the specified criteria, it instantiates one, adds it to the pool, and returns it to the client.
You might be wondering how you can reuse flyweights. For example, how can one line flyweight draw all lines of a particular color when those lines presumably have different locations and lengths? You accomplish that reuse by splitting a flyweight's state in two: intrinsicstate (e.g., line color) and extrinsic state (e.g., line location and length). The flyweight maintains intrinsic state, while extrinsic state must be passed to the flyweight at runtime. By externalizing extrinsic state, you can easily reuse flyweights for different variations of that extrinsic state.
Flyweights are abundant in Java, from simple strings to Swing tree nodes and component borders. The rest of this article discusses those flyweights and concludes with an example of how you can implement your own flyweights.
Java strings
Because strings are usually plentiful in most applications, Java goes to great lengths to make sure strings perform well. First, strings are immutable, so they don't have to be thread-safe. Because synchronization is expensive, strings are spared that drag on performance. Second, as you might guess, strings specified at compile-time are flyweights—strings that contain the same character sequence are shared. That sharing can greatly reduce memory footprints, and therefore, increase performance.
Example 1's application demonstrates Java strings' flyweight nature.
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 } }
Here's the application's output:
true true false true
.
In the preceding application, the strings
fly
and fly2
refer to the same string object (recall that the ==
operator evaluates totrue
if the objects it compares are the same). Ditto for weight
and weight2
.
Strings computed at runtime, such as
distinctString
in the preceding application, are not flyweights by default; however, you can force the issue with the String.intern()
method—a poorly named method that brings to mind medical topics, but nonetheless returns flyweights for strings computed at runtime.
Because the Java compiler implements the Flyweight pattern for strings, I can't show you how it's done. So, let's move on to another flyweight implemented in the Java APIs.
Swing borders
Swing comes with an exhaustive set of borders for its components, including line borders, titled borders, bevel borders, etc. Figure 3 shows an application that fits two panels with a bevel border.
Figure 3. Swing borders
You can implement component borders in numerous ways. The GOF book, for example, shows you how to implement them with the Decorator pattern (see page 175), but in Swing, they're implemented as flyweights for maximum performance. Additionally, Swing lets you instantiate a single border per component if you wish, but it also provides a border factory that creates flyweight borders so a single border can be shared among multiple components. Example 2, the listing for Figure 3's application, illustrates the use of that factory.
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"); } }
The preceding application's output is:
bevel borders are shared
, which means the two panels share a single border instance. Notice that BorderFactory.createRaisedBevelBorder()
is somewhat of a misnomer because it doesn't actually create anything if a bevel border already exists in the factory.
Borders have a
paintBorder()
method that the border's component invokes when the component is painted. Here is that method's signature:public void paintBorder(Component c, Graphics g, int x, int y, int width, int height)
The arguments passed to
paintBorder()
represent a border's extrinsic state, namely, a component, a graphics object to draw in, and the border's bounds. Because a border can be a flyweight, and therefore shared by multiple components, the border does not maintain that extrinsic state.Swing tree nodes
Swing tree nodes are excellent candidates for the Flyweight pattern. Consider Figure 4, which shows a Swing tree that acts as a file explorer.
Figure 4. Swing trees
As a testament to Swing's design, a file explorer like the one in Figure 4 is straightforward to implement. In fact, it's a mere 144 lines of code, blank lines and all. As a testament to the Flyweight pattern, that file explorer is fast. If you wish, you can download this article's source code and take the file explorer for a spin. If you do, explore some directories with a lot of files so you have a tree with many nodes like Figure 4 (note the scrollbar thumb's size). Then grab the scrollbar thumb and move it up and down as fast as you can. You'll see that performance is excellent.
Swing trees are blazingly fast because they use a single componentfor all nodes in the tree. That component is created by a tree cell renderer with the overly verbose
TreeCellRenderer.getTreeCellRendererComponent()
method, whose signature is listed below:public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus)
By now, the sheer number of arguments to
getTreeCellRendererComponent()
should indicate to you that the component returned from the method is a flyweight. Like Swing borders, tree cell renderers are passed extrinsic state so the rendering component can be shared. ThegetTreeCellRendererComponent()
method sets the component's properties depending upon the extrinsic state. For instance, for folders, getTreeCellRendererComponent()
fits the component with a folder icon; if not, it uses a document icon.
As an aside, Swing tables render table cells in the same manner as Swing trees. One component represents all the cells for a single table, so Swing tables also have excellent performance.
So far I've detailed three Flyweight pattern examples in Java, but for you to understand the Flyweight pattern completely, let's implement a flyweight from scratch.
Roll your own!
The rest of this article implements a line flyweight similar to the one discussed at the beginning of this article. We'll explore that implementation in three steps. First, I implement an application that draws 10,000 lines without the aid of a line object, by simply painting repeatedly into a graphics context. Second, I implement a naive
Line
class that cannot be shared. Third, I turn that naive implementation into a flyweight. Figure 5 shows the line-drawing application.
Figure 5. Drawing lines with Swing
Drawing without line objects
Example 3 lists Figure 5's application.
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)]; } }
The preceding application creates a window with two panels. The application draws lines with random colors, locations, and lengths into the top panel when you click on the button in the bottom panel. The graphic context's
drawLine()
method draws those lines.
Now let's implement a
Line
class and use line objects to draw lines instead of using the graphics context directly.A naive Line implementation
Perhaps you think using a graphics context to draw lines is not very object-oriented, so you want to implement a
Line
class to encapsulate that functionality. Let's do that. Example 4 lists the revised application.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); } } }); } ... }
I only included pertinent code; the rest of the application is identical to the one listed in Example 3. The preceding application creates 10,000 line objects and tells each one to draw itself. Example 5 lists the
Line
class.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); } }
The preceding
Line
class maintains its color and endpoints, which it uses to draw a line.A flyweight Line implementation
Obviously, we don't want to create 10,000 lines as we did in the preceding application, so let's reduce that number to six, one line for each line color. Example 6 lists the revised application.
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()); } } }); } ... }
That's more like it. Now line objects are obtained from a line factory, which returns one of six shared line instances. Example 7 lists that factory.
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; } }
The line factory maintains a
HashMap
of lines, keyed by color. When you ask the factory for a line by calling getLine()
, the factory first checks to see if a line with that color exists in the HashMap
; if so, it returns it. Otherwise, it creates a line, stores it in the HashMap
, and returns it. Either way, the caller winds up with a shared Line
instance.
Example 8 lists the revised
Line
class.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); } }
Notice the preceding
Line
class is simpler than Example 5's Line
class. Why? Because the preceding Line
class has been purged of extrinsic state, namely the line's endpoints. Because the preceding Line
class does not maintain that extrinsic state, and because it's passed to the draw()
method from the client, those line instances can be shared.Your apps can soar
Historically, allocating a huge number of (typically small) objects can be detrimental to your Java application's performance, although modern JVMs have greatly reduced the penalty you must pay for such excess. If you find that your application is instantiating numerous objects of a particular type, you might consider using the Flyweight pattern to share a limited number of those objects.
No comments:
Post a Comment