Tutorial de Java

Errores de Programación Más Frecuentes

Anterior | Siguiente
  1. Scheduling de Hilos de Ejecución
  2. Errores en el uso de las características de portabilidad de Java
  3. Uso directo de las clases de bajo nivel del AWT
  4. Uso de directorios definidos
  5. Carga de drivers JDBC
  6. Terminación de líneas
  7. Entrada/Salida por fichero
  8. Tamaño de los elementos del interfaz gráfico
  9. Fuentes de caracteres
  10. El protocolo paint()

Scheduling de Hilos de Ejecución

Quizá piense el lector que es un tanto presuntuoso por parte del autor el permitirse indicar errores de programación que él mismo, probablemente incluso de mayor envergadura que los que cita, cometa. Y tiene razón el lector, por ello quiero pedirle disculpas y lo que aquí se exponen son unos cuantos ejemplos que generarán error, garantizado.

A continuación se presenta una decena de ejemplos de errores, vistos desde el punto de vista de la portabilidad, porque quizá el sine qua non de Java, en última instancia sea perseguir una verdadera independencia de plataforma.

Los errores no tienen ningún orden de dificultad, no son más que una decena de errores en los que puede caer cualquier programador. Hay muchísimas formas cometer fallos a la hora de programar en Java; algunas se deben simplemente a malos hábitos y son muy difíciles de encontrar, mientras que otros saltan a la vista al instante. Los errores de programación más obvios, también son los que con más frecuencia cometen los programadores.

Quizás muchos de los fallos se evitarían si los programadores intentarán aplicar calidad a sus programas desde el momento mismo de concebir el programa, y no la tendencia de aplicar pureza a la aplicación en el último momento.

El scheduling de los hilos de ejecución, es decir, el tiempo que el sistema destina ala ejecución de cada uno de los hilos de ejecución, puede ser distinto en diferentes plataformas. Si no se tienen en cuenta las prioridades o se deja al azar la prevención de que dos hilos de ejecución accedan a un mismo objeto al mismo tiempo, el programa no será portable.

El siguiente programa, por ejemplo, no es portable.

class Contador implements Runnable {
    static long valor = 0;

    public void run() {
        valor += 1;
    }

    public static void main( String args[] ) {
        try {
            Thread hilo1 = new Thread( new Contador() );
            hilo1.setPriority( 1 );
            Thread hilo2 = new Thread( new Contador() );
            hilo2.setPriority( 2 );

            hilo1.start();
            hilo2.start();

            hilo1.join();
            hilo2.join();

            Systtem.out.println( valor );
        } catch( Exception e ) {
            e.printStackTrace();
        }
    }
}

Este programa puede no imprimir "2" en todas las plataformas, porque los dos hilos de ejecución no están sincronizados y, desgraciadamente, este es un problema muy profundo y no hay forma de detectar su presencia ni adivinar el momento en que va a ocurrir.

Una solución simple, y drástica, es hacer todos los métodos sincronizados. Pero esto también tiene problemas porque puede presentar como puntos sin retorno obvios, lo que en realidad es una corrupción de datos. Así que, el scheduling de los hilos de ejecución es uno de los aspectos más problemáticos de la programación Java, porque la naturaleza del problema se vuelve global, al intervenir varios hilos de ejecución. No se puede buscar el problema en una parte del programa, es imprescindible entender y tratar el programa en su globalidad.

Además, hay ejemplos de contención de hilos que no serán detectados. Por ejemplo, en la clase Contador anterior no se detectará el problema ya que la contención está en el acceso al campo, en lugar de en el acceso al método.

Errores en el uso de las características de portabilidad de Java

Hay características de portabilidad en el API de Java. Es posible, pero menos portable, escribir código que no haga uso de estas características. Muchas de las propiedades del sistema proporcionan información sobre la portabilidad; por ejemplo, se pueden utilizar las propiedades del sistema para conocer cuál es el carácter definido como fin de línea o el que se emplea como terminador del fichero, para emplear el adecuado a la plataforma en que se está ejecutando el programa.

Java proporciona dos métodos para facilitar la escritura de programas portables en este sentido. Por un lado, utilizar el método println() en vez de imprimir las cadenas seguidas del terminador de cadena embebido; o también, utilizar la expresión System.getProperty("line.separator") para conocer cuál es el terminado de línea que se utiliza en la plataforma en que se está ejecutando el programa. En general, el uso de las propiedades facilita en gran modo la portabilidad y debería extenderse su uso siempre que fuese aplicable.

De igual modo, AWT abstrae muchas de las operaciones que se hacen con el sistema de ventanas de la plataforma. Usar esta abstracción siempre; por ejemplo, utilizar LayoutManager en vez de posicionar en coordenadas absolutas, utilizar los diferentes métodos getSize(), utilizar los colores disponibles en java.awt.SystemColor.

Uso directo de las clases de bajo nivel del AWT

Los programas que se pretende sean portables y que vayan a implementar interfaces basadas en AWT no deberían utilizar las clases base del AWT directamente. El protocolo para la interacción con estas clases es muy dependiente de la plataforma. La solución es sencilla, basta con mantenerse en el lado cliente del AWT. Quizá esto haya sido un problema de documentación, porque en anteriores versiones del JDK se han descrito estas clases, cuando nunca se ha pretendido que sean de uso público en programas cliente.

Uso de directorios definidos

Un error muy común y fácil de cometer entre los programadores, aunque igual de fácil de corregir es la designación en el código de nombre de ficheros, que pueden dar lugar a problemas de portabilidad, pero cuando se añade el directorio en que se sitúan, seguro que estos problemas aparecerán. Estos fallos son más comunes entre programadores con viejos hábitos, que eran dependientes del sistema operativo, y que son difíciles de olvidar.

La forma más portable de construir un File para un fichero en un directorio es utilizar el constructor File(File,String). Otra forma sería utilizar las propiedades para conocer cuál es el separador de ficheros y el directorio inicial; o también, preguntarle al operador a través de una caja de diálogo.

Otro problema es la noción de camino absoluto, que es dependiente del sistema. En Unix los caminos absolutos empiezan por /, mientras que en Windows pueden empezar por cualquier letra. Por esta razón, el uso de caminos absolutos que no sean dependientes de una entrada por operador o de la consulta de las propiedades del sistema no será portable.

El ejemplo siguiente proporciona una clase útil para la construcción de nombres de ficheros. La última versión del JDK es mucho más exhaustiva, y detecta más fácilmente los errores cometidos en los directorios y nombres de ficheros.

import java.io.File;
import java.util.StringTokenizer;

public class UtilFichero {
  /* Crea un nuevo fichero con el nombre de otros. Si la base inicial es
   * nula, parte del directorio actual
   */
  public static File dirInicial( File base,String path[] ) {
    File valor = base;
	int i=0;
	
	if( valor == null && path.length == 0 ) {
	  valor = new File( path[i++] );
	  }
	  
	for( ; i < path.length; i++ ) {
	  valor = new File( valor,path[i] );
	  }

    return( valor );
	}

  public static File desdeOrigen( String path[] ) {
    return( dirInicial( null,path ) );
	}
  
  public static File desdeProp( String nombrePropiedad ) {
    String pd = System.getProperty( nombrePropiedad );

	return( new File( pd ) );
	}

  // Utilizando la propiedad del sistema "user.dir"
  public static File userDir() {
    return( desdeProp( "user.dir" ) );
	}
  
  // Utilizando la propiedad del sistema "java.home"
  public static File javaHome() {
    return( desdeProp( "java.home" ) );
	}

  // Utilizando la propiedad del sistema "user.home"
  public static File userHome() {
    return( desdeProp( "user.home" ) );
	}

  /* Separa el primer argumento, utilizando el segundo argumetno como
   * carácter separador.
   * Es muy útil a la hora de crear caminos de ficheros portables
   */ 
  public static String[] split( String p,String sep ) {
    StringTokenizer st = new StringTokenizer( p,sep );
    String valor[] = new String[st.countTokens()];

    for( int i=0; i < valor.length; i++ ) {
      valor[i] = st.nextToken();
      }
    
	return( valor );
    }
  }

Carga de drivers JDBC

El interfaz JDBC, definido por el paquete java.sql, proporciona gran flexibilidad a la hora de codificar la carga del driver JDBC a utilizar. Esta flexibilidad permite la sustitución de diferentes drivers sin que haya que modificar el código, a través de la clase DriverManager, que selecciona entre los drivers disponibles en el momento de establecer la conexión. Los drivers se pueden poner a disposición de DriverManager a través de la propiedad del sistema jdbc.drivers o cargándolos explícitamente usando el método java.lang.Class.forName().

También es posible la carga de una selección de drivers, dejando que el mecanismo de selección de DriverManager encuentre el adecuado en el momento de establecer la conexión con la base de datos. Ha y que tener siempre en cuenta los siguientes puntos:

  • La prueba de drivers se intenta siempre en el orden en que se han registrado, por lo que los primeros drivers tienen prioridad sobre los últimos cargados, con la máxima prioridad para los drivers listados en jdbc.drivers.
  • Un driver que incluya código nativo fallará al cargarlo sobre cualquier plataforma diferente de la que fue diseñado; por lo que el programa deberá recoger la excepción ClassNotFoundException.
  • Un driver con código nativo no debe registrarse con DriverManager hasta que no se sepa que la carga ha tenido éxito.
  • Un driver con código nativo no está protegido por la caja negra de Java, así que puede presentar potenciales problemas de seguridad.

Terminación de líneas

Las distintas plataformas de sistemas operativos tienen distintas convenciones para la terminación de líneas en un fichero de texto. Por esto debería utilizarse el método println(), o la propiedad del sistema line.separator, para la salida; y para la entrada utilizar los métodos readLine().

Java internamente utiliza Unicode, que al ser un estándar internacional, soluciona el problema a la hora de codificar; pero el problema persiste al leer o escribir texto en un fichero.

En el JDK 1.1 se utilizan las clases java.io.Reader y java.io.Writer para manejar la conversión del set de caracteres, pero el problema puede surgir cuando se leen o escriben ficheros ASCII planos, porque en el ASCII estándar no hay un carácter específico para la terminación de líneas; algunas máquinas utilizan \n, otras usan \r, y otras emplean la secuencia \r\n.

Enarbolando la bandera de la portabilidad, deberían utilizarse los métodos println() para escribir una línea de texto, o colocar un marcador de fin de línea. También, usar el método readLine() de la clase java.io.BufferedReader para recoger una línea completa de texto. Los otros métodos readLine() son igualmente útiles, pero el de la clase BufferedReader proporciona al código también la traslación.

Entrada/Salida por fichero

Las clases de entrada y salida del JDK 1.0 no son protables a plataformas que no soporten formatos nativos de ficheros no-ASCII. Es fácil para el programador suponer alegremente que todo el mundo es ASCII. Pero la realidad no es esa, los chinos y los japoneses, por ejemplo, no puedes escribir nada con los caracteres ASCII. Hay que tener esto en cuenta si se quiere que los programas viajen fuera del país propio.

Tamaño de los elementos del interfaz gráfico

El tamaño exacto de los elementos del AWT es diferente de una plataforma a otra, como tampoco son iguales la pantalla y los tamaños máximo y mínimo que toma por defecto una ventana. Un botón renderizado para un Mac con un tamaño de pixel fijo, seguro que no será renderizado con ese mismo rango en cualquier otro sistema operativo.

Fuentes de caracteres

El tamaño y disponibilidad de varios tipos de fuentes varía de pantalla a pantalla, incluso en una misma plataforma hardware, dependiendo de la instalación que se haya hecho. Esto es algo que no descalifica totalmente el programa, porque se verá defectuosamente, pero el programa podrá seguir usándose; pero debería prevenisre, porque se presupone que el programador desea que su software aparezca de la mejor manera posible en cualquier plataforma.

El modo mejor de evitar todo esto es no codificar directamente el tamaño de los textos, dejar que los textos asuman su tamaño en relación al layout, y utilizar los métodos de la clase FontMetrics para encontrar el tamaño en que aparecen los caracteres de una cadena sobre un Canvas. Cuando se coloca una fuente que no se encuentra entre las de defecto, hay que asegurarse siempre de colocar alguna de respaldo en el bloque catch.

Cuando se crea un menú para seleccionar fuentes de caracteres, se debería utilizar el método java.awt.Toolkit.getFontList(), en lugar de especificar una lista de fuentes.

Cuando se actualice un programa del JDK 1.0 al JDK 1.1, hay que tener en cuenta que los nombre de las fuentes de caracteres se han actualizado, tal como se indica en la documentación del método getFontList().

El protocolo paint()

Los métodos del AWT Component.paint() y Component.update() tienen como parámetro de entrada un objeto de tipo Graphics. Este objeto no debe ser persistente, sino solamente válido durante la acción del método; la implementación del AWT es libre de destruir este objeto Graphics después de que el método paint() le devuelva el control, lo que hace peligros retener el objeto.

No se debe retener pues el objeto Graphics; en general, no es seguro pintar fuera de los métodos update() o paint(); si se necesita una vida más larga para el objeto Graphics, se debe crear uno a partir del argumento de entrada al método, pero teniendo sumo cuidado, porque esto no funciona en todas las plataformas.

Graphics miGraphics = null;  // retenido

void paint( Graphics g ) {
  if( miGraphics == null ) {
    miGraphics = g.create();
    }
  // El código de pintado iría aquí
}

Navegador

Home | Anterior | Siguiente | Indice | Correo