logotipo

img_google

Consejos Técnicos de la Conexión del Desarrollador Java

Consejos, Técnicas y Código de Ejemplo

Bienvenido a esta edición de los Consejos Técnicos de la Conexión del Desarrollador Java, febrero 5, 2002, esta edición cubre:

Estos consejos se escribieron usando la Java(tm) 2 SDK, Standard Edition, v1.3.

Usted puede esta edición (en su original en inglés), en http://java.sun.com/jdc/JDCTechTips/2002/tt0205.html


Escritura de métodos toString

Uno de los métodos estándar definidos en la clase java.lang.Object es toString. Este método se usa para obtener una representación literal de un objeto. Podemos (y normalmente debemos) sobrecargar este método en las clases que escribamos. Este consejo examina algunas de las cuestiones que rodean el uso de toString

Consideremos el siguiente código de ejemplo: 

class MyPoint {
    private final int x, y;

    public MyPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

public class TSDemo1 {
    public static void main(String args[]) {
        MyPoint mp = new MyPoint(37, 47);

        // se usa el Object.toString() por defecto

        System.out.println(mp);

        // al igual que el anterior, muestra la 
        // funcion del toString() por defecto

        System.out.println(mp.getClass().getName()
                + "@"
                + Integer.toHexString(mp.hashCode()));

        // llamada implicita a toString() sobre un objeto
        // como parte de una concatenación entre cadenas

        String s = mp + " probando";
        System.out.println(s);

        // igual que el anterior excepto que la
        // referencia al objeto es nula

        mp = null;
        s = mp + " probando";
        System.out.println(s);
    }
}

El programa TSDemo1 define una clase MyPoint representando puntos X,Y.  No definimos un método toString() para la clase. El programa crea una instancia de la clase y luego la imprime. Cuando ejecutemos el programa, veremos el siguiente resultado:

MyPoint@111f71
MyPoint@111f71
MyPoint@111f71 probando
null probando

Nos puede sorprender como es posible imprimir un objeto de una clase arbitraria. Los métodos de librería tales como System.out.println conocen nada acerca de la clase MyPoint o sus objetos. Asi que ¿cómo es posible convertir algo como un objeto en una cadena y luego imprimirlo, como lo muestra la primera salida en el ejemplo TSDemo1?

La respuesta es que println llama al método java.oi.PrintStream.print(Object), el cual luego llama al método String.valueOf(). El método String.valueOf es muy simple:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

Cuando se llama a println con un objeto MyPoint, el método String.valueOf convierte al objeto en una cadena. String.valueOf primero se asegura que la referencia no sea nula. Luego llama al método toString para el objeto. Puesto que la clase MyPoint no tiene un método toString, se usa el método por defecto de  java.lang.Object.

¿Qué hace que el método toString por defecto devuelva una cadena? El formato se ilustra en la segunda muestra de la salida de TSDemo1. El nombre de clase, una "@", y la versión hexadecimal del código de dispersón del objeto se concatenan y se retorna. El método por defecto hashCode en la clase Object es aplica generalmente para convertir  las direcciones de memoria del objeto en un entero. Asi que los resultados mostrados pueden variar.

La tercera y cuarta partes del ejemplo TSDemo1 ilustran una idea relacionada: cuando usamos "+" para concatenar una cadena y un objeto, se llama toString para convertir el objeto a una cadena. Necesitamos ver los bytecodes de TSDemo1 para ver eso. Podemos ver en el bytecode de TSDemo1 (una forma legible para las personas) usando elcomando javap:

javap -c TSDemo1

Si vemos el bytecode notaremos que parte de él implica  la creación de un objeto StringBuffer, y luego el uso de StringBuffer.append(Object) para agregar el objeto mp a él. StringBuffer.append(Object) se implementa de manera muy simple:

public synchronized StringBuffer append(Object obj) {
    return append(String.valueOf(obj));
}

Como se menciono antes, String.valueOf llama al toString del objeto para obtener su representación en modo alfanumérico

Bien, hemos nombrado mucho al método toString. ¿Cómo escribir métodos toString? Es en verdad muy simple. He aquí un ejemplo:

class MyPoint {
    private final int x, y;

    public MyPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public String toString() {
        return x + " " + y;
    }
}

public class TSDemo2 {
    public static void main(String args[]) {
        MyPoint mp = new MyPoint(37, 47);

        // llama al método MyPoint.toString
        System.out.println(mp);

        // lama a toString y 
        // le extrae el valor de X
        String s = mp.toString();
        String t = s.substring(0, s.indexOf(' '));
        int x = Integer.parseInt(t);
        System.out.println(t);
    }
}

Cuando corramos el programa TSDemo2, la salida será:

37 47
37

Ciertamente en este ejemplo el método toString trabaja, pero hay un par de problemas con él. Uno es que no hay un texto descriptivo que se muestre en la salida de toString. Todo lo que se ve es un críptico "37 47". El otro problema es que los valores de X, Y en MyPoint son privados. No hay otra manera de obtenerlos excepto deshaciendo la cadena retornada por toString. La segunda parte de TSDemo2 muestra el código requerido para extraer el valor de X de la cadena. Esta manera está propensa a los errores además de ineficiente. 

He aquí otra manera de escribir métodos toString, uno que parcha los problemas del ejemplo previo. 

class MyPoint {
    private final int x, y;

    public MyPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public String toString() {
        return "X=" + x + " " + "Y=" + y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

public class TSDemo3 {
    public static void main(String args[]) {
        MyPoint mp = new MyPoint(37, 47);

        // llama a MyPoint.toString()
        System.out.println(mp);

        // obtiene los valores X, Y mediante métodos accesores
        int x = mp.getX();
        int y = mp.getY();
        System.out.println(x);
        System.out.println(y);
    }
}

La salida es:

X=37 Y=47
37
47

Este ejemplo agrega un poco de texto descriptivo a la salida, y define un par de métodos accesores para obtener los valores de X, Y. En general, cuando escribimos un método toString, el formato de la cadena que se retorna debe contener etiquetas descriptivas para cada campo. Y allí debe estar una forma de llegar a los valores de  los campos del objeto sin desmembrar la cadena. Note que el uso de "+" dentro de toString para construir el valor de retorno no es necesariamente la manera más eficiente. Es posible que prefiramos usar StringBuffer en su lugar. 

Los tipos primitivos en Java, como int, también tienen métodos toString, por ejemplo Integer.toString(int). ¿ Qué pasa con los arreglos? ¿Cómo poder convertir un arreglo en una cadena? Se puede asignar una referencia al arreglo a una referencia a un Object, pero los arreglos no son en realidad clases. Sin embargo, e posible usar la reflección (reflection) para implementar un método toString para los arreglos. El código se vería así:

import java.lang.reflect.*;

public class TSDemo4 {
    public static String toString(Object arr) {

        // si la referencia al objeto es nula o
        // no es un arreglo, llama a String.valueOf()
        if (arr == null ||
                !arr.getClass().isArray()) {
            return String.valueOf(arr);
        }

        // establece una buffer de cadena y
        // obtiene la lontigud de arreglo
        StringBuffer sb = new StringBuffer();
        int len = Array.getLength(arr);

        sb.append('[');

        // itera a traves de los elementos arreglo
        for (int i = 0; i < len; i++) {
            if (i > 0) {
                sb.append(',');
            }

            // obtiene el elemento i-th
            Object obj = Array.get(arr, i);

            // lo convierte a una cadena mediante
            // una llamada recursiva a toString()
            sb.append(toString(obj));
        }
        sb.append(']');

        return sb.toString();
    }

    public static void main(String args[]) {

        // ejemplo #1
        System.out.println(toString("probando"));

        // ejemplo #2
        System.out.println(toString(null));

        // ejemplo #3
        int arr3[] = new int[]{
                1,
                2,
                3
        };
        System.out.println(toString(arr3));

        // ejemplo #4
        long arr4[][] = new long[][]{
                {1, 2, 3},
                {4, 5, 6},
                {7, 8, 9}
        };
        System.out.println(toString(arr4));

        // ejemplo #5
        double arr5[] = new double[0];
        System.out.println(toString(arr5));

        // ejemplo #6
        String arr6[] = new String[]{
                "testing",
                null,
                "123"
        };
        System.out.println(toString(arr6));

        // ejemplo #7
        Object arr7[] = new Object[]{
                new Object[]{null, new Object(), null},
                new int[]{1, 2, 3},
                null
        };
        System.out.println(toString(arr7));
    }
}

El programa TSDemo4 crea un método toString, y luego pasa el método toString a una referencia arbitraria a Object. Si la referencia es nula o no es un arreglo, el programa llama al método String.valueOf(). Sino el objeto referencia a un arreglo. En ese caso, TSDemo4 usa la reflección(reflection) para acceder a los elementos del arreglo. Array.getLength y Array.get son los métodos clave que operan sobre el arreglo. Después de recuperar un elemento, el programa llama a toString recursivamente para obtener la repesentacion en cadena de texto del elemento. Haciendo las cosa de esta manera, nos aseguramos que los arreglos multimensionales se manejen apropiadamente.

La salida del programa TSDemo4 es:

probando
null
[1,2,3]
[[1,2,3],[4,5,6],[7,8,9]]
[]
[testing,null,123]
[[null,java.lang.Object@111f71,null],[1,2,3],null]

Obviamente, si tiene un array realmente grante, y llama a toString, este usará una gran cantidad de memoria, y la cadena resultante puede no ser particularmente útil o legible para un humano.

Para mayor información acerca de los métodos toString, vea la sección 2.6.2. Method Invocations en "The Java(tm) Programming Languaje Third Edition" de Arnold, Gosling y Holmes. También vea el ítem 9, Always override toString, en "Effective Java Programming Languaje Guide" de Joshua Bloch.


Uso de readResolve

El tip del 7 agosto "Usando enumeraciones" mostró un ejemplo de lo que se llama "typesafe enum" he aquí parte de ese ejemplo

class EnumColor {
   // nombre del enumerador
   private final String enum_name;
 
   // constructor privado, llamado solo desde la misma clase
   private EnumColor(String name) {
       enum_name = name;
   }
 
   // retorna el nombre del enumerador
   public String toString() {
       return enum_name;
   }
 
   // crea tres enumeradores
   public static final EnumColor ROJO =
           new EnumColor("rojo");
   public static final EnumColor VERDE =
           new EnumColor("verde");
   public static final EnumColor AZUL =
           new EnumColor("azul");
}

El ejemplo establece una clase con constructor privado, para que no sea posible crear subclases e instancias. Se crean tres instancias dentro de la clase, cada instancia se usa como un enumerador. Usando esta forma es posible comparar enumeradores usando el operador ==. No hay problemas de violación del tipo de dominio como só los hay con los enumeraciones enteras. EnumColor es un ejemplo de una clase con "instancias controladas". Hay la garantía, por ejemplo, de que existe exactamente una instancia de EnumColor representando a EnumColor.VERDE

Supongamos la necesidad de serializar los objetos EnumColor, esto significa, que los convertiremos en un flujo de bytes y luego revertiremos el proceso ¿Cómo hacerlo?, he aqui una forma:

import java.io.*;

class EnumColor implements Serializable {

    // nombre del enumerador
    private final String enum_name;

    // constructor privado
    // llamado solamente desde la clase
    private EnumColor(String name) {
        enum_name = name;
    }

    // retorna el nombre del enumerador
    public String toString() {
        return enum_name;
    }

    // crea tres enumeradores
    public static final EnumColor ROJO =
            new EnumColor("rojo");
    public static final EnumColor VERDE =
            new EnumColor("verde");
    public static final EnumColor AZUL =
            new EnumColor("azul");
}

public class RRDemo1 {
    public static void main(String args[])
            throws IOException, ClassNotFoundException {
        EnumColor e1 = EnumColor.VERDE;

        // serializacion
        FileOutputStream fos =
                new FileOutputStream("test.ser");
        BufferedOutputStream bos =
                new BufferedOutputStream(fos);
        ObjectOutputStream oos =
                new ObjectOutputStream(bos);
        oos.writeObject(e1);
        oos.close();

        // deserializacion
        FileInputStream fis =
                new FileInputStream("test.ser");
        BufferedInputStream bis =
                new BufferedInputStream(fis);
        ObjectInputStream ois =
                new ObjectInputStream(bis);
        EnumColor e2 = (EnumColor)ois.readObject();
        ois.close();

        // imprimir resultados
        System.out.println("e1 = " + e1);
        System.out.println("e2 = " + e2);

        // ve si e1 y e2 son iguales
        System.out.println(e1 == e2 ? "iguales" : "distintos");
    }
}

El programa RRDemo1 serializa un objeto que representa a EnumColor.VERDE, y lo deserializa. esta es la salida que se producirá cuando corramos el programa:

e1 = verde
e2 = verde
distintos

Los objetos e1 y e2 tienen el mismo nombre (los campos estáticos no se serializan). Desafortunadamente, estas dos referencias no se refieren al mismo objeto. Así que no sirve usar == para hacer la comparación de igualdad. El proceso de serialización ha destruido la propiedad ya mencionada -- hay ahora dos instancias de EnumColor.VERDE, y no pueden ser comparadas usando ==.

El problema es que el método de deserialización readObject siempre opera sobre una nueva instancia de clase. Sea un readObject implícito, o uno que nosotros mismos hayamos escrito para la clase EnumColor. Asi que cuando EnumColor.VERDE se deserializa,  se establece un valor apropiado para el campo enum_name, pero se genera un nuevo objeto. Por eso, el esquema completo acerca del control de las instancias de EnumColor, se viene abajo.

¿Cómo solucionar este problema? La respuesta es usar un caracteristica relativamente nueva de la serialización llamada readResolve. Aquí tenemos un ejemplo:

import java.io.*;

class EnumColor implements Serializable {

    // nombre del enumerador
    private final transient String enum_name;

    // constructor privado
    // llamado solo desde la clase
    private EnumColor(String name) {
        enum_name = name;
    }

    // retorna el nombre del enumerador
    public String toString() {
        return enum_name;
    }

    // índice siguiente a asignar por el enumerador
    private static int nextIndex = 0;

    // indice actual del enumerador
    private final int index = nextIndex++;

    // crea tres enumeradores
    public static final EnumColor ROJO =
            new EnumColor("rojo");
    public static final EnumColor VERDE =
            new EnumColor("verde");
    public static final EnumColor AZUL =
            new EnumColor("azul");

    // tabla de los valores del enumerador
    private static final EnumColor VALUES[] = {
            ROJO,
            VERDE,
            AZUL
    };

    // retorna un objeto alternativo
    // como resultado de la deserialización 
    private Object readResolve() throws
            ObjectStreamException {
        return VALUES[index];
    }
}

public class RRDemo2 {
    public static void main(String args[])
            throws IOException, ClassNotFoundException {
        EnumColor e1 = EnumColor.VERDE;

        // serialización
        FileOutputStream fos =
                new FileOutputStream("test.ser");
        BufferedOutputStream bos =
                new BufferedOutputStream(fos);
        ObjectOutputStream oos =
                new ObjectOutputStream(bos);
        oos.writeObject(e1);
        oos.close();

        // deserialización
        FileInputStream fis =
                new FileInputStream("test.ser");
        BufferedInputStream bis =
                new BufferedInputStream(fis);
        ObjectInputStream ois =
                new ObjectInputStream(bis);
        EnumColor e2 = (EnumColor)ois.readObject();
        ois.close();

        // imprime los resultados
        System.out.println("e1 = " + e1);
        System.out.println("e2 = " + e2);

        // ve si e1/e2 se refieren al mismo objeto
        System.out.println(
                e1 == e2 ? "iguales" : "distintos");
    }
}

Si definimos un método readResolve para una clase, este se llama sobre los objetos de esta clase después que se han deserializado. El método readResolve puede elegir retornar algún otro objeto si lo deseamos, dejando el objeto deseralizado para ser recolectado por el recolector de basura.

El programa RRDemo2 hace transitorio al campo enum_name. Esto significa que enum_name no se serializa. El programa luego asigna un índice a cada enumerador. El índice se serializa. readResolve se llama después de que un objeto serializado (conteniendo sólo el índice) se deserializa. El índice se usa luego para buscar en una tabla de valores del enumerador, con el valor apropiado retornado.  Este esquema conserva la propiedad de control de instancias. El resultado de correr el programa es:

e1 = green
e2 = green
equal

La técnica con readResolve es ligeramente frágil dado que no podemos agregar nuevos enumeradores entre los existente. En lugar de ello, tenemos que agregarlos al final. La forma de serializar un enumerador consiste en serializar sus índices. Si cambiamos los índices de EnumColor los objetos serializados serán incorrectos.

La técnica con readResolve es útil cuando tenemos clases con instancias controladas, tales como typesafe enums, semifallo, y clases de símbolo con ligaduras únicas.

Para más información acerca del uso de readResolve, vea el ítem 57, Provide a readResolve methos when necessary, en "Effective Java Programming Languaje Guide" de Joshua Bloch.


Importante

Por favor lea nuestros Términos de Uso, Privacidad, y Políticas de Licencia

http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html

Retroalimentación

¿Comentarios?, envíelos a los JDC Tech Tips a jdc-webmaster@sun.com, si tienes comentarios sobre la traducción o deseas incluirla en tu página web escribe a acortiz@ucsm.edu.pe.

Suscripciones / Desuscripciones

Archivos

Usted puede encontrar los archivos de los JDC Tech Tips (en su original en inglés) en http://java.sun.com/jdc/TechTips/index.html, puede ver algunas traducciones al español el http://ciberia.ya.com/javaplace/techtips/ 

Copyright

Copyright 2002 Sun Microsystems, Inc. Todos los derechos reservados. 901 San Antonio Road, Palo Alto, California 94303 USA.

Este documento(en su original en inglés) está protegido por el copyright, Para mayor información vea:

http://java.sun.com/jdc/copyright.html

Esta edición de los JDC Tech Tips fue escrita por Glen McCluskey

This issue of the JDC Tech Tips is written by Glen McCluskey.

JDC Tech Tips
February 5, 2002

Sun, Sun Microsystems, Java y Java Developer Connection son marcas o marcas registradas de Sun Microsystems, Inc. en los Estados Unidos y otros paises.