JDC Tech Tips (10 de Enero de 2002)

Bienvenido a los Consejos Técnicos de la Conexión del Desarrollador Java (JDC), del 10 de Enero de 2002. Esta edición cubre:

  • Usar Excepciones.
  • Dimensionar Texto con FontMetrics.

Estos consejos fueron desarrollados usando Java(tm) 2 SDK, Standard Edition v 1.3

Se puede ver esta edición (en su original en inglés) de los Consejos Técnicos en formato html en http://java.sun.com/jdc/JDCTechTips/2002/tt0110.html

Usar Excepciones

Supongamos que estamos escribiendo un método que hace algún procesamiento de fichero, y uno de los parámetros del método es un String de un nombre de fichero. El método chequea un nombre válido, y luego abre el fichero para procesarlo. El código se podría parecer a algo como esto:

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");
        }
    }
}

El ejemplo ExDemo1 funciona tal y como está escrito. El resultado de ejecutar el programa es:

I/O error
Exception in thread "main" BadArgumentException
    at ExDemo1.processFile(ExDemo1.java:18)
    at ExDemo1.main(ExDemo1.java:35)

Pero hay un par de problemas en la forma en que se usan las excepciones en este ejemplo. En este página, obtendremos algún consejo práctico sobre el uso de excepciones para aprocecharlas mejor.

El primer problema es sobre el uso de excepciones estándard en oposición al uso de nuestras propias excepciones. Normalmente es preferible usar excepciones estándards en lugar de las propias. Por eso, en lugar de usar BadArgumentException, sería mejor usar IllegalArgumentException. Definir nuestra propia excepción en el ejemplo ExDemo1 no nos ofrece ninguna ventaja.

El segundo problema trata con la claridad de los mensajes, es decir, es una buena idea incluir un mensaje descriptivo como un argumento al constructor de la excepción. El ejemplo ExDemo1 falla al no hacer esto. Aquí tenemos una actualización del ejemplo que incorpora estas 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");
        }
    }
}

El resultado de ejecutar el programa es:

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)

Hay un tercer problema que necesita corrección en este ejemplo. El método processFile se llama dos veces, la pimera con nombre de fichero que no existe, y la segunda con un string vació. En el primer caso, se lanza una IOException, y en el segundo caso una IllegalArgumentException. La primera excepción es capturada y la segunda no.

Una IOException es un ejemplar que es llamado una excepción de chequeo, mientras que una IllegalArgumentException es una excepción de tiempo de ejecución. El árbol de herencia de la clase Excepcion del paquete java.lang se parece a este:

Throwable
    Exception            
        RuntimeException
            IllegalArgumentException
        IOException 
    Error

La regla básica es que el llamador de un método que lanza una excepción chequeada debe manejar la excepción en un clausula catch o propagarla más arriba. En otras palabras, processFile llama al constructor FileInputStream y luego al método FileInputStream.close. El constructor y el método close lanzan una excepción chequeada IOException o su subclase FileNotFoundException. Por eso, processFile debe capturar esta excepción o declararse a sí mismo como que lanza la excepción. Como hace esto, su llamador, el método main, debe capturar la excepción.

Las excepciones chequeadas son un mecanismo para requerir que el programador trate con condiciones excpecionales que aparecen. Por contraste, las excepciones de tiempo de ejecución no requieren este tratamiento. Cuando se llama a processFile con un string vacío, lanza una IllegalArgumentException, que no es capturada, y el thread actual (y el programa) se termina.

En general, las excepciones chequeadas se usan para errores recuperables, como que un fichero no existente. Las excepciones de tiempo de ejecución, en comparación, se usan para errores de programación. Si estamos escribiendo una navegador de ficheros, por ejemplo, es bastante fácil que un usuario pudiera especificar un fichero no existente como parte de alguna operación. Pero un string vacío pasado como un nombre de fichero, posiblemente indica un error de programación no recuperable, algo que no se espera que suceda.

Un tercer tipo de excepción es una subclase de Error y, por convención, está reservada para su uso por la Máquina Virtual Java. OutOfMemoryError es un ejemplo de este tipo de excepción.

Otro aspecto del uso de excepciones concierne all llamado "failure atomicity", es decir, dejar un objeto en un estado consistente cuando se lanza una excepción. Aquí tenemos un ejemplo:

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());
    }
}

Si ejecutamos este programa, el resultado es:

java.lang.ArrayIndexOutOfBoundsException
size = 4

El programa intenta sin éxito añadir un cuarto elemento a una lista de tres enteros, y da una excepción. Pero el tamaño reportado de la lista 4. ¿Por qué es esto?

Es problema está en la expresion "ptr++". Se toma el valor del puntero de la lísta, el puntero es incrementado, y luego se usa el valor original para indexar dentro del array. Este indexado dispara una excepción, pero el puntero ya tiene el nuevo e incorrecto valor.

La solución a este problema se ilustra en el código comentado de la clase MyList:

if (ptr == MAXSIZE) {
 throw new ArrayIndexOutOfBoundsException(
     "ptr == MAXSIZE");
}
vec[ptr++] = i;

Aquí primero se chequea el puntero, y se la lanza una excepción de fuera de rango. El puntero sólo es incremetnado si es seguro hacerlo.

Un ejemplo final construido sobre el anterior. Cuando una excepción se lanza desde dentro de un método, algunas veces necesitamos preocuparnos de limpiar. Esto es cierto incluso aunque el lenguaje Java tiene recolector de basura, es decir, automáticamente reclama espacio dinámico que no es utilizado más.

Aquí hay un ejemplo que demuestra este problema:

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);
    }
}

El conductor del programa en el método main crea un fichero y luego intenta copiarlo repetidamente en otro fichero, un fichero con un nombre de fichero no válido.

El método copyFile abre ambos ficheros, copia uno en el otro, y luego cierra los ficheros. Pero ¿qué sucede si el fichero de entrada es válido, y puede abrirse pero el de salida no? En este caso, se lanza una excepción cuando se abre el segundo fichero.

La mayoría de las veces, esta aproximación funciona, pero hay un problema. El primer stream de ficheo no se cierra. Esto crea un recurso bloqueado poque el stream abierto tiene un descriptor de fichero detrás de él, repsentando un bloqueo en el sistema operativo. Normalmente, podemos confiar en el recolector de basura para evitar el recurso bloqueado. Si ocurre la recolección de basura, llama al método finalize para FileInputStream. Esto cierra el stream y libera el descriptor del fichero. Sin embargo en el ejemplo ExDemo4, el efecto del recolector de basura se ve bloqueado al añadir las referencias FileInputStream a la lista de referencias que salen fuera del método. Incluso si el ejemplo no hiciera esto, no está garantizado que el recolector de basura funcione de una forma temporal para resolver el problema. Este ejemplo, es imaginario, pero ilustra el punto sobre la limpieza cuando lanzamos una excepción dentro de un método.

Si ejecutamos el programa ExDemo4, deberíamos ver un resultado parecido a este:

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)

La solución a este problema se en el texto comentado en 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();
    }
}

Este código fuerza a cerrar el stream del fichero de entrada, y maneja el caso donde el fichero de salida no puede ser abierto. Si quitamos los comentarios al código pertinente (debemos recordar comentar el código anterior que abre/cierra el fichero), obtenemos el resultado:

open count = 2048

Observa que incluso con la corrección, todavía hay un riesgo potencial para bloquear un recurso. Esto podría suceder si la copía del fichero tiene éxito, pero al cerrar el steam de entrada se lanza una excepción dentro de la clausula finally. En este caso, el stream de salida no se cerrará.

Nuestro resultados podrían variar, dependiendo de nuestro entorno local. La clave es simplemente que tenemos que prestar atención a la limpieza cuando se lanza una excepción, y un recurso bloqueado es un ejemplo específico de este problema.

Para más información sobre el uso de excepciones puedes ver los puntos 40, 42, 45, y 46, de "Effective Java Programming Language Guide" de Joshua Bloch. También puedes verla sección 12.3, Finalization, de "The Java Programming Language Third Edition" de Arnold, Gosling, y Holmes.

Dimensionar Textos con FontMetrics

Imagina que estamos usando un objeto Graphics para dibujar algún texto en Swing. Nuestro programa necesita mostrar dos líneas de texto. El programa llama al método Graphics.drawString para dibujar la primera línea, y luego lo llama de nuevo para dibujar la segunda. El método drawString requiere que especifiquemos la localización X,Y inicial del texto. Para la segunda línea, asumimos que añadiendo 8 a Y funcionará. Es decir, aumimos que la altura de los caracteres es alrededor de 8. Por ejemplo, si la primera línea empieza en 100, 100, entonces la segunda empieza en 100, 108. Aquí podemos ver como sería el código:

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);
    }
}

Este programa funciona. Seleccionamos el botón Draw Text y veremos dos líneas de texto seguidas por otras dos. Observa que las primeras líneas estan un poco juntas. El problema se puede corregir cambiando el valor 8 por uno un poco mayor como 18. Pero esta aproximación se pierde la clave. Cuando estamos dibujando texto como parte de una conjunto de operaciones gráficas, nuestro programa necesita tener en cuenta los tamaños de las fuentes. En otras palabras, el programa debería ajustarse automáticamente basándose en el tamaño de las fuentes con las que está trabajando. Podríamos cambiar el valor 8 por 18 y corregir el problema en el ejemplo FmDemo1, pero ¿qué pasaría si estuvieramos trabajando con una fuente realmente grande? En ese caso, un valor de altura de 18 no sería suficiente.

Una mejor solución se ilustra en el segundo griupo de sentencias drawString de FmDemo1, donde se dibujan las dos líneas de texto inferiores. El programa obtiene un objeto FontMetrics. Luego llama al método getHeight del objeto para obtener la altura de la fuente. Y luego la usa en lugar de un valor fijo como 8 ó 18.

Típicamente un programa llama a Graphics.getFontMetrics para obtener un objeto FontMetrics. El objeto devuelto es realmente una subclase de FontMetrics, dado que FontMetrics es una subclase abtracta. Un objeto FontMetrics contiene información sobre el tamaño de una fuente dada.

Para ver que clase de información está disponible, veamos otro ejemplo:

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);
    }
}

Ejecutamos este progama y seleccionamos el botón Draw Text. vermeos una línea de texto con tres líneas coloreadas, una arriba, una en el centro y otra abajo. La línea verde en el centro es la línea base. Este es el punto de entrada para el cálculo de las distintas medidas. Cuando el Abstract Window Toolkit (AWT) dibuja un caracter, el punto de referencia X,Y del caracter está a la izquierda, en la línea base.

La línea roja de la parte superior es la línea ascendente. Esta es la distancia desde la línea base hasta la parte superior del más alto de los caracteres. La línea azul es la línea descendente. Esta es la distancia desde la línea base hasta la parte inferior del carácter que más baja. Es posible que algunos caracteres tengan un ascendente o descendente mayor. FontMetrics proporciona los métodos getMaxAscent y getMaxDescent para obtener los valores máximos de la fuente. También hay una propiedad llamada leading que representa la cantidad de espacio a reservar entre la línea descendente de una línea de texto y la línea ascendente de la siguiente.

El programa de ejemplo FmDemo2 también ilustra el uso del método stringWidth, que calcula la anchura gráfica de un string. Cada fuente de caracteres tiene lo que llamamos anchura de avance. Esta es la posición donde el AWT debería situar el siguiente caracter después de dibujar el caracter en cuestión. La anchura de avance de un string no es ne necesariamente la suma de las anchuras de sus caracteres medidas aisladamente. Esto es porque la anchura de algunos caracteres puede variar con el contexto.

Veamos un ejemplo final, uno que muestra cómo dibujar una caja alrededor de un 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);
    }
}

En el programa de ejemplo FmDemo3, se usa el método getStringBounds para obtener un objeto RectangularShape. Luego el programa llama a getBounds para obtener los límites de la caja, que se dibuja alrededor del texto. Esto es útil cuando estámos intentando distribuir texto y gráficos juntos, y necesitamos conocer cuánto espacio está ocupando el texto.

Para más información sobre el dimensionado de texto con FontMetrics, puedes ver "Fonts and FontMetrics" en el capítulo 4 de "Graphic Java: Mastering the JFC, 3rd Edition Volume 1, AWT" de David Geary.

Copyright y notas de la traducción

Nota respecto a la traducción

El original en inglés de la presente edición de los JDC Tech Tips fue escrita por Glen McCluskey, la traducción no oficial fue hecha por Juan A. Palos (Ozito), cualquier sugerencia o corrección hágala al correo [email protected] , sugerencia respecto a la edición original a mailto:[email protected]

Nota (Respecto a la edición via email)

Sun respeta su tiempo y su privacidad. La lista de correo de la Conexión del desarrollador Java se usa sólo para propósitos internos de Sun Microsystems(tm). Usted ha recibido este email porque se ha suscrito a la lista. Para desuscribirse vaya a la página de suscripciones, desmarque casilla apropiada y haga clic en el botón Update.

Suscripciones

Para suscribirse a la lista de correo de noticias de la JDC vaya a la página de suscripciones, elija los boletines a los que quiera suscribirse, y haga clic en Update.

Realimentación

¿Comentarios?, envie su sugerencias a los Consejos Técnicos de la JDC a mailto:[email protected]

Archivos

Usted encontrará las ediciones de los Consejos Técnicos de la JDC (en su original en inglés) en http://java.sun.com/jdc/TechTips/index.html

Copyright

Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.

Este documento esta protegido por las leyes de autor. Para mayor información vea http://java.sun.com/jdc/copyright.html

Enlaces a sitios fuera de Sun

Los Consejos Técnicos de la JDC pueden dar enlaces a otros sitios y recursos. Ya que Sun no tiene control sobre esos sitios o recursos usted reconoce y acepta que Sun no es responsable por la disponibilidad de tales sitios o recursos, y no se responsabiliza por cualquier contenido, anuncios , productos u otros materiales disponibles en tales sitios o recursos. Sun no será responsable, directa o indirectamente, por cualquier daño o pérdida causada o supuestamente causada por o en relación con el uso de o seguridad sobre cualquier tal contenido, bienes o servicios disponibles en o através de cualquier sitio o recurso.

El original en Ingles de esta edición de los Consejos técnicos fue escrita por Glen McCluskey.

JDC Tech Tips January 10, 2002

Sun, Sun Microsystems, Java y Java Developer Connection (JDC) son marcas registradas de Sun Microsystems Incs. en los Estados Unidos y cualquier otro país.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE ARTÍCULO