Return-Path: <env_7683238-1547911430@hermes.sun.com>
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 <alexp@mit.edu>; Thu, 10 Jan 2002 13:15:24 -0500 (EST)
Date: Thu, 10 Jan 2002 18:15:27 GMT+00:00
From: "JDC Tech Tips" <body_7683238-1547911430@hermes.sun.com>
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.<init>
                             (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
