Return-Path: Received: from fort-point-station.mit.edu by po10.mit.edu (8.9.2/4.7) id NAA15307; Thu, 10 Jan 2002 13:15:29 -0500 (EST) Received: from hermes.sun.com (hermes.sun.com [64.124.140.169]) by fort-point-station.mit.edu (8.9.2/8.9.2) with SMTP id NAA22274 for ; Thu, 10 Jan 2002 13:15:24 -0500 (EST) Date: Thu, 10 Jan 2002 18:15:27 GMT+00:00 From: "JDC Tech Tips" To: alexp@mit.edu Message-Id: <7683238-1547911430@hermes.sun.com> Subject: JDC Tech Tips January 10, 2002 (Using Exceptions, FontMetrics) Precedence: junk Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit X-Mailer: Beyond Email J D C T E C H T I P S TIPS, TECHNIQUES, AND SAMPLE CODE WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips, January 10, 2001. This issue covers: * Using Exceptions * Sizing Text With FontMetrics These tips were developed using Java(tm) 2 SDK, Standard Edition, v 1.3. You can view this issue of the Tech Tips on the Web at http://java.sun.com/jdc/JDCTechTips/2002/tt0110.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - USING EXCEPTIONS Suppose you're writing a method that does some file processing, and one of the parameters to the method is a string filename. The method checks for a valid name, and then opens the file for processing. The code might look something like this: import java.io.*; class BadArgumentException extends RuntimeException { public BadArgumentException() { super(); } public BadArgumentException(String s) { super(s); } } public class ExDemo1 { static void processFile(String fname) throws IOException { if (fname == null || fname.length() == 0) { throw new BadArgumentException(); } FileInputStream fis = new FileInputStream(fname); // ... process file ... fis.close(); } public static void main(String args[]) { try { processFile("badfile"); } catch (IOException e1) { System.out.println("I/O error"); } try { processFile(""); } catch (IOException e1) { System.out.println("I/O error"); } } } The ExDemo1 example works as written. The result of running the program is: I/O error Exception in thread "main" BadArgumentException at ExDemo1.processFile(ExDemo1.java:18) at ExDemo1.main(ExDemo1.java:35) But there are a couple of issues regarding the way exceptions are used in the example. In this tip, you'll get some practical advice on using exceptions to best advantage. The first issue is about using standard exceptions as opposed to using your own exceptions. It's usually preferable to use standard exceptions instead of your own. So instead of using BadArgumentException, it would be better to use IllegalArgumentException. Defining your own exception in the ExDemo1 example doesn't offer any advantage. The second issue regards the clarity of messages, that is, it's a good idea to include a descriptive message as an argument to the exception constructor. The ExDemo1 example fails to do this. Here's an update to the example that incorporates these ideas: import java.io.*; public class ExDemo2 { static void processFile(String fname) throws IOException { if (fname == null || fname.length() == 0) { throw new IllegalArgumentException( "null or empty filename"); } FileInputStream fis = new FileInputStream(fname); // ... process file ... fis.close(); } public static void main(String args[]) { try { processFile("badfile"); } catch (IOException e1) { System.out.println("I/O error"); } try { processFile(""); } catch (IOException e1) { System.out.println("I/O error"); } } } The result of running the program is: I/O error Exception in thread "main" java.lang.IllegalArgumentException: null or empty filename at ExDemo2.processFile(ExDemo2.java:7) at ExDemo2.main(ExDemo2.java:25) There's a third issue that needs to be addressed in this example. The processFile method is called twice, the first time with a nonexistent filename, the second with an empty string. In the first case, an IOException is thrown, and in the second, an IllegalArgumentException. The first exception is caught, and the second is not. An IOException is an instance of what is called a checked exception, while an IllegalArgumentException is a run-time exception. The exception class hierarchy in the java.lang package looks like this: Throwable Exception RuntimeException IllegalArgumentException IOException Error The basic rule is that the caller of a method that throws a checked exception must handle the exception in a catch clause or further propagate it. In other words, processFile calls the FileInputStream constructor and later the FileInputStream.close method. Both the constructor and the close method throw the checked exception IOException or its subclass FileNotFoundException. So processFile must catch this exception or declare that it itself throws the exception. Since it does the latter, its caller, the main method, must catch the exception. Checked exceptions are a mechanism for requiring the programmer to deal with exceptional conditions that are raised. By contrast, this treatment is not required of run-time exceptions. When processFile is called with an empty string, it throws an IllegalArgumentException, which is not caught, and the current thread (and the program) terminates. In general, checked exceptions are used for recoverable errors, such as a nonexistent file. Run-time exceptions, by comparison, are used for programming errors. If you're writing a file browser, for example, it's quite plausible that a user might specify a nonexistent file as part of some operation. But an empty string passed as a filename possibly indicates a non-recoverable programming error, something that is not supposed to happen. A third kind of exception is a subclass of Error and is, by convention, reserved for use by the Java virtual machine*. OutOfMemoryError is an example of this type of exception. Another aspect of using exceptions concerns what is called "failure atomicity", that is, leaving an object in a consistent state when an exception is thrown. Here's an example: class MyList { private static final int MAXSIZE = 3; private final int vec[] = new int[MAXSIZE]; private int ptr = 0; public void addNum(int i) { vec[ptr++] = i; /* if (ptr == MAXSIZE) { throw new ArrayIndexOutOfBoundsException( "ptr == MAXSIZE"); } vec[ptr++] = i; */ } public int getSize() { return ptr; } } public class ExDemo3 { public static void main(String args[]) { MyList list = new MyList(); try { list.addNum(1); list.addNum(2); list.addNum(3); list.addNum(4); } catch (ArrayIndexOutOfBoundsException e) { System.out.println(e); } System.out.println( "size = " + list.getSize()); } } If you run this program, the result is: java.lang.ArrayIndexOutOfBoundsException size = 4 The program unsuccessfully tries to add a fourth element onto a three-long list of integers, and gives an exception. But the reported size of the list is 4. Why is this? The problem is in the "ptr++" expression. The list pointer's value is taken, the pointer is incremented, and then the original value is used to index into the array. This indexing triggers an exception, but the pointer already has the new, incorrect, value. The solution to this problem is illustrated in the commented code in the MyList class: if (ptr == MAXSIZE) { throw new ArrayIndexOutOfBoundsException( "ptr == MAXSIZE"); } vec[ptr++] = i; Here the pointer is first checked, and an exception thrown if it's out of range. The pointer is incremented only if it's safe to do so. A final example builds on the previous one. When an exception is thrown from within a method, you sometimes need to worry about cleaning up. This is true even though the Java programming language has garbage collection, that is, it automatically reclaims dynamic space that is no longer in use. Here's an example that demonstrates this issue: import java.io.*; import java.util.*; public class ExDemo4 { static final int NUMFILES = 2048; static final String FILENAME = "testfile"; static final String BADFILE = ""; static final List stream_list = new ArrayList(); // copy one file to another public static void copyFile( String infile, String outfile) throws IOException { // open the files FileInputStream fis = new FileInputStream(infile); stream_list.add(fis); FileOutputStream fos = new FileOutputStream(outfile); // if an exception, won't get this far // ... copy file ... // close the files fis.close(); fos.close(); /* FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(infile); stream_list.add(fis); fos = new FileOutputStream(outfile); // ... copy file ... } // finally block executed even if // exception occurs finally { if (fis != null) { fis.close(); } if (fos != null) { fos.close(); } } */ } public static void main(String args[]) throws IOException { // create a file new File(FILENAME).createNewFile(); // repeatedly try to copy it to a bad file for (int i = 1; i <= NUMFILES; i++) { try { copyFile(FILENAME, BADFILE); } catch (IOException e) { } } // display the number of successful // FileInputStream constructor calls System.out.println("open count = " + stream_list.size()); // try to open another file FileInputStream fis = new FileInputStream(FILENAME); } } The driver program in the main method creates a file and then repeatedly tries to copy it to another file, a file with a bad filename. The copyFile method opens both files, copies one to the other, and then closes the files. But what happens if the input file is valid, and can be opened, but the output file cannot be? In this case, an exception is thrown when the second file is opened. Most of the time, this approach works, but there is a problem. The first file stream is not closed. This creates a resource leak because the open stream has a file descriptor behind it, representing a hook into the operating system. Normally, you can rely on garbage collection to prevent the resource leak. If garbage collection occurs, it calls the finalize method for FileInputStream. This closes the stream and frees the file descriptor. However in the ExDemo4 example, the effect of garbage collection is blocked by adding the FileInputStream references to a list of references that exists outside of the method. Even if the example did not do this, garbage collection is not guaranteed to work in a timely fashion to solve the problem. This example is contrived, but it illustrates the point about cleaning up when you throw an exception from within a method. If you run the ExDemo4 program, you should see a result that looks something like: open count = 1019 Exception in thread "main" java.io.FileNotFoundException: testfile (Too many open files) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream. (FileInputStream.java:64) at ExDemo4.main(ExDemo4.java:83) The solution to this problem is given in the commented code in copyFile: FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(infile); stream_list.add(fis); fos = new FileOutputStream(outfile); // ... copy file ... } // finally block executed even if // exception occurs finally { if (fis != null) { fis.close(); } if (fos != null) { fos.close(); } } This code forces the input file stream to be closed, and handles the case where the output file cannot be opened. If you uncomment the pertinent code (remember to then comment the previous open file/close file code), you get the result: open count = 2048 Note that even with the fix, there is still some potential for a resource leak. This could happen if the file copy is successful, but the close of the input stream triggers an exception within the finally clause. In this case, the output stream will not be closed. Your results may vary, depending on your local environment. The point is simply that you have to pay attention to cleaning up when an exception is thrown, and resource leaks are a specific example of this issue. For more information about using exceptions, see items 40, 42, 45, and 46, in "Effective Java Programming Language Guide" by Joshua Bloch (http://java.sun.com/docs/books/effective/). Also see Section 12.3, Finalization, in "The Java(tm) Programming Language Third Edition" by Arnold, Gosling, and Holmes http://java.sun.com/docs/books/javaprog/thirdedition/. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SIZING TEXT WITH FONTMETRICS Imagine that you're using a Graphics object in Swing to draw some text. Your program needs to display two text lines. The program calls the Graphics.drawString method to draw the first line, and calls it again to draw the second. The drawString method requires that you specify an X,Y starting location for the text. For the second line, you assume that adding 8 to Y will do the job. That is, you assume that the height of characters is about 8. For example, if the first line starts at 100,100, then the second starts at 100,108. Here's what the code looks like: import java.awt.*; import java.awt.event.*; import javax.swing.*; public class FmDemo1 { public static void main(String args[]) { JFrame frame = new JFrame("FmDemo1"); // handle window closing frame.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e) { System.exit(0); } }); final JPanel panel = new JPanel(); // set up a button and add an // action listener to it JButton button = new JButton("Draw Text"); button.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e) { Graphics g = panel.getGraphics(); // draw two lines of text int BASE1 = 100; int OFFSET1 = 8; g.drawString("LINE 1", 100, BASE1); g.drawString("LINE 2", 100, BASE1 + OFFSET1); // draw two lines of text, // using font metrics FontMetrics fm = g.getFontMetrics(); int BASE2 = 150; int OFFSET2 = fm.getHeight(); g.drawString("LINE 1", 100, BASE2); g.drawString("LINE 2", 100, BASE2 + OFFSET2); } }); panel.add(button); frame.getContentPane().add(panel); frame.setSize(250, 250); frame.setLocation(300, 200); frame.setVisible(true); } } This program works. Select the Draw Text button and you'll see two lines of text followed by another two. Notice that the first two lines run together just a bit. The problem can be fixed by changing the 8 value to some larger number such as 18. But this approach misses the point. When you're drawing text as part of a set of graphics operations, your program needs to account for varying font sizes. In other words, the program should automatically adjust itself based on the sizes of the fonts that it's working with. You could change the value 8 to 18 and fix the problem in the FmDemo1 example, but what if you're working with a really big font? In that case, a height value of 18 won't be enough. A better solution is illustrated in the second group of drawString statements in FmDemo1, which draws the lower two lines of text. The program obtains a FontMetrics object. It then calls the getHeight method on the object to get the height of the font. This is then used instead of a fixed value such as 8 or 18. Typically a program calls Graphics.getFontMetrics to get a FontMetrics object. The object returned is actually of a subclass of FontMetrics, given that FontMetrics is an abstract class. A FontMetrics object contains information about the size of a given font. To see what sort of information is available, let's look at another example: import java.awt.*; import java.awt.event.*; import javax.swing.*; public class FmDemo2 { public static void main(String args[]) { JFrame frame = new JFrame("FmDemo2"); // handle window closing frame.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e) { System.exit(0); } }); // set up a panel and set its font final JPanel panel = new JPanel(); Font f = new Font( "Monospaced", Font.ITALIC, 48); panel.setFont(f); // set up a button and action listener // for it JButton button = new JButton("Draw Text"); button.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e) { int XBASE = 50; int YBASE = 100; String test_string = "hqQWpy`/\'|i\\,{_!^"; Graphics g = panel.getGraphics(); FontMetrics fm = g.getFontMetrics(); int ascent = fm.getAscent(); int descent = fm.getDescent(); int width = fm.stringWidth( test_string); // draw a text string g.drawString( test_string, XBASE, YBASE); // draw the ascent line g.setColor(Color.red); g.drawLine(XBASE, YBASE - ascent, XBASE + width, YBASE - ascent); // draw the base line g.setColor(Color.green); g.drawLine(XBASE, YBASE, XBASE + width, YBASE); // draw the descent line g.setColor(Color.blue); g.drawLine(XBASE, YBASE + descent, XBASE + width, YBASE + descent); } }); panel.add(button); frame.getContentPane().add(panel); frame.setSize(600, 250); frame.setLocation(250, 200); frame.setVisible(true); } } Run this program and select the Draw Text button. You'll see a line of text with three colored lines above, through, and below it. The green line in the middle is the baseline. This is the starting point for calculating the various font measurements. When the Abstract Window Toolkit (AWT) draws a character, the character's X,Y reference point is at the left of the character, at the baseline. The red line at the top is the ascent. This is the distance from the baseline to the top of most characters. The blue line is the descent. This is the distance from the baseline to the bottom of most characters. It is possible that some characters will have a greater ascent or descent. FontMetrics provides getMaxAscent and getMaxDescent methods to get the maximum values for the font. There is also a property called leading that represents the amount of space to be reserved between the descent of one line of text and the ascent of the next. The FmDemo2 program also illustrates the use of the stringWidth method, which computes the graphical width of a string. Each font character has what is called an advance width. This is the position where the AWT should place the next character after drawing the character in question. The advance width of a string is not necessarily the sum of the widths of its characters measured in isolation. That's because the widths of some characters can vary based on context. Let's look at a final example, one that shows how to draw a bounding box around a string: import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import javax.swing.*; public class FmDemo3 { public static void main(String args[]) { JFrame frame = new JFrame("FmDemo3"); // handle window closing frame.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e) { System.exit(0); } }); // set up a panel and set a font for it final JPanel panel = new JPanel(); Font f = new Font( "Monospaced", Font.ITALIC, 48); panel.setFont(f); JButton button = new JButton("Draw Text"); button.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e) { int XBASE = 50; int YBASE = 100; String test_string = "hqQWpy`/\'|i\\,{_!^"; Graphics g = panel.getGraphics(); FontMetrics fm = g.getFontMetrics(); // draw a text string g.drawString( test_string, XBASE, YBASE); // draw a bounding box around it RectangularShape rs = fm.getStringBounds( test_string, g); Rectangle r = rs.getBounds(); g.setColor(Color.red); g.drawRect(XBASE + r.x, YBASE + r.y, r.width, r.height); } }); panel.add(button); frame.getContentPane().add(panel); frame.setSize(600, 250); frame.setLocation(250, 200); frame.setVisible(true); } } In the FmDemo3 program, the getStringBounds method is used to get a RectangularShape object. The program then calls getBounds to get the bounding box, which is drawn around the text. This is useful when you're trying to lay out text and graphics together, and need to know just how much space the text is occupying. For more information about sizing text with FontMetrics, see "Fonts and FontMetrics" in chapter 4 of "Graphic Java: Mastering the JFC, 3rd Edition Volume 1, AWT" by David Geary (http://www.sun.com/books/catalog/Geary3/index.html). . . . . . . . . . . . . . . . . . . . . . . . IMPORTANT: Please read our Terms of Use and Privacy policies: http://www.sun.com/share/text/termsofuse.html http://www.sun.com/privacy/ * FEEDBACK Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com * SUBSCRIBE/UNSUBSCRIBE - To subscribe, go to the subscriptions page, (http://developer.java.sun.com/subscription/), choose the newsletters you want to subscribe to and click "Update". - To unsubscribe, go to the subscriptions page, (http://developer.java.sun.com/subscription/), uncheck the appropriate checkbox, and click "Update". - To use our one-click unsubscribe facility, see the link at the end of this email: - ARCHIVES You'll find the JDC Tech Tips archives at: http://java.sun.com/jdc/TechTips/index.html - COPYRIGHT Copyright 2002 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA. This document is protected by copyright. For more information, see: http://java.sun.com/jdc/copyright.html This issue of the JDC Tech Tips is written by Glen McCluskey. JDC Tech Tips January 10, 2002 * As used in this document, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform. Sun, Sun Microsystems, Java, and Java Developer Connection are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. To use our one-click unsubscribe facility, select the following URL: http://sunmail.sun.com/unsubscribe?7683238-1547911430