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
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.
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.
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
¿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.
Para suscribirse vaya a la página de suscripciones:
(http://developer.java.sun.com/subscription/),
escoja los boletines a los que quiere suscribirse y haga clic en
"Update".
Para desuscribirse vaya a la página de suscripciones:
(http://developer.java.sun.com/subscription/)
Quite las marcas de las casillas apropiadas y haga clic en
"Update".
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 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.