A: Pasando y Retornando Objetos A estas alturas Usted debería estar razonablemente cómodo con la idea de que cuando usted “ pasa ” un objeto, usted pasa realmente una referencia. En muchos lenguajes de programac ión usted puede usar la forma “regular” de estos para pasar objetos, y la mayoría de las veces todas funciona bien. Pero siempre llega el momento en el cual usted debe hacer algo fuera de lo común, y repentinamente las cosas se ponen un poco más complicadas (o en caso de C ++, muy complicado). Java no es una excepción, y es importante que usted entienda exactamente qué es lo que sucede cuando desplaza objetos de un lado a otro y los manipula. Este apéndice proveerá esa visión. Otra forma para plantear la pregunta de este apéndice, ¿si usted viene de un lenguaje de programación equipado con punteros, es “Los tiene Java?”. Algunos afirman que los punteros son difíciles y peligrosos y por consiguiente malos, y ya que Java es todo bondad y luz y le liberará de sus cargas terrenales de programación, no e s posible que tenga tales cosas. Sin embargo, es más preciso decir que Java tiene punteros; ciertamente, cada identificador de un objeto en Java (excepto para los tipos primitivos de datos) es uno de estos punteros, pero su uso está restringido y vigilado no sólo por el compilador sino también por el sistema de tiempo de ejecución. O para ponerlo en otra forma, Java tiene punteros, pero no tiene aritmética de punteros. Estos son lo que he estado llamando “referencias”, y usted puede pensar en ellos como “punteros seguros”, no muy diferentes de las tijeras de seguridad de la escuela elemental - que no son afiladas, para que Usted no pueda lastimarse sin gran esfuerzo, pero algunas veces pueden ser lentos y tediosos. Pasando referencias Cuando usted pasa una referencia a un método, usted está todavía apuntando hacia el mismo objeto. Un experimento simple demuestra esto: //: apendicea:PassReferences.java // Pasando referencias. import com.bruceeckel.simpletest.*; public class PassReferences { private static Test monitor = new Test(); public static void f(PassReferences h) { System.out.println("h inside f(): " + h); } public static void main(String[] args) { PassReferences p = new PassReferences(); System.out.println("p inside main(): " + p); f(p); monitor.expect(new String[] { "%% p inside main\\(\\): PassReferences@[a-z0-9]+", "%% h inside f \\(\\): PassReferences@[a -z0-9]+" }); } } ///:~ El método toString () es automáticamente invocado en las instrucciones de impresión, y PassReferences hereda directamente de Object sin redefinición de toString (). Así, la versión de toString () de Object es usada, la cual escribe la clase del objeto seguida por la dirección donde ese objeto está ubicado (no la referencia, sino el almacenamiento real del objeto). La salida de impresión tiene el siguiente aspecto: p adentro de main() PassReferences@ad3ba4 h adentro de f(): PassReferences@ad3ba4 Usted puede ver que p y h se refieren al mismo objeto. Esto es mucho más eficiente que duplicar un objeto nuevo PassReferences solo para que Usted pueda enviar un argumento a un método. Pero trae a colación un asunto importante. Aliasing Aliasing quiere decir que más de una referencia está atada al mismo objeto, como en el ejemplo precedente. El problema con el aliasing ocurre cuando alguien escribe a ese objeto. Si los dueños de las otras referencias no esperan que ese objeto cambie, se llevarán entonces una sorpresa. Esto puede ser demostrado con un ejemplo simple: //: appendixa:Alias1.java // Aliando dos referencias a un objeto. import com.bruceeckel.simpletest.*; public class Alias1 { private static Test monitor = new Test(); private int i; public Alias1(int ii) { i = ii; } public static void main(String[] args) { Alias1 x = new Alias1(7); Alias1 y = x; // Asigne la referencia System.out.println("x: " + x.i); System.out.println("y: " + y.i); System.out.println("Incrementing x"); x.i++; System.out.println("x: " + x.i); System.out.println("y: " + y.i); monitor.expect(new String[] { "x: 7" , "y: 7" , "Incrementing x" , "x: 8" , "y: 8" }); } } ///:~ En la línea: Alias1 y = x; // Asigne la referencia una referencia nueva Alias1 es creada, pero en lugar de ser asignada a un objeto nuevo creado con new, es asignada a una referencia existente. De esta manera los contenidos de la referencia x, la cual es la dirección a la que el objeto x está apuntando, es asignada a y, y así tanto x como y están pegadas al mismo objeto. Por ello, cuando la variable i de x es incrementada en la instrucción: x.i++; la variable i de y será igualmente afectada. Esto puede verse en la salida: x: 7 y: 7 Incrementando x X: 8 y: 8 Una buena solución en este caso es simplemente no hacerlo. No direccione a propósito más de una referencia a un objeto en el mismo ámbito. Su código será mucho más fácil de entender y depurar. Sin embargo, cuando usted está pasando una referencia como un argumento - la que se supone es la forma como Java trabaja- Usted automáticamente está llevando a cabo el aliasing, porque la re ferencia local que es creada puede modificar el “ objeto exterior ” (el objeto que fue creado fuera del ámbito del método). Aquí hay un ejemplo: //: apéndicea:Alias2.java // Las llamadas a métodos pueden hacer aliasing implícitamente sobre sus argumentos. import com.bruceeckel.simpletest.*; public class Alias2 { private static Test monitor = new Test(); private int i; public Alias2(int ii) { i = ii; } public static void f(Alias2 reference) { reference.i++; } public static void main(String[] args) { Alias2 x = new Alias2(7); System.out.println("x: " + x.i); System.out.println("Calling f(x)" ); f(x); System.out.println("x: " + x.i); monitor.expect(new String[] { "x: 7" , "Calling f(x)" , "x: 8" }); } } ///:~ El método está cambiando su argumento, el objeto exterior. Cuando este tipo de situación se presenta, usted debe decidir si tiene sentido, si el usuario lo espera, y si va a causar problemas. En general, usted llama a un método para producir un valor de retorno y / o un cambio de estado en el objeto para el cual el método ha sido llamado. Es mucho menos común llamar a un método para manipular sus argumentos; esto se denomina “llamando un método por sus efectos secundarios.” Así, cuando usted crea un método que modifica sus argumentos, el usuario debe ser claramente instruido y advertido acerca del uso de ese método y de sus sorpresas potenciales. Por la confusión y escollos que se pueden generar, es mucho mejor evitar afectar el argumento. Si Usted necesita modificar un argumento durante una llamada a un método y no tiene la intención de modificar el argumento externo, lo que debe hacer es proteger éste último haciendo una copia de él dentro de su método. Ese es el tema de buena parte de este apéndice. Haciendo copias locales Para repasar: Todo paso de argumentos en Java es llevado a cabo a través de referencia s. Esto es, cuando usted pasa “ un objeto, ” usted está realmente pasando solo una referencia a un objeto que vive fuera del método, así es que si usted realiza cualquier modificación a esa referencia, entonces usted está modificando el objeto exterior. Además: • • • • • El aliasing ocurre automáticamente durante el paso de argumentos. No hay objetos locales, sólo referencias locales. Las referencias tienen ámbito, los objetos no. El tiempo de vida de un objeto no es nunca un asunto en Java. No hay soporte de lenguaje (e.g., “const ”) para impedir la modificación de objetos y detener los efectos negativos del aliasing. Usted simplemente no puede usar la palabra clave final en la lista de argumentos; eso simplemente impide que vuelva a atar la referencia a un objeto diferente. Si usted sólo está leyendo información de un objeto y no modificándola, el paso de una referencia es la forma más eficiente de paso de argumentos. Esto es bueno; el modo por defecto de hacer las cosas es también el más eficiente. Sin embargo, algunas veces hay que poder tratar el objeto como si fuera “local” a fin de que los cambios que usted haga afecten sólo la copia local y no modifiquen el objeto exterior. Muchos lenguajes de programación dan soporte a la posibilidad de hacer automáticamente dentro del método, una copia local del objeto externo.116 Java no lo hace, pero le permite producir este efecto. Paso por valor Esto trae a colación el asunto de la terminología, lo cual siempre parece bueno para una discusión. El término es “ paso por valor, ” y el significado depende de cómo percibe usted la operación del programa. El significado general es que usted obtiene una copia local de lo que sea que usted está pasando, pero la pregunta real es cómo piensa usted sobre lo que está pasando. En lo concerniente al significado de “paso por valor,” hay dos campo s relativamente distintos: 1. Java pasa todo por valor. Cuando usted está pasando valores primitivos a un método, usted obtiene una copia distinta del valor primitivo. Cuando usted pasando una referencia a un método, usted obtiene una copia de la referencia. Por ende, todo es pasado por valor. Por supuesto, la suposición es que usted siempre está pensando (y preocupándose de) que son referencias las que están siendo pasadas, pero parece que el diseño de Java ha hecho todo lo posible para permitirle ignorar (la mayoría de las veces) que usted está trabajando con una referencia. Esto es, parece darle permiso de pensar en la referencia como “ el objeto, ” ya que implícitamente dereferencia a este cuandoquiera que usted hace una llamada a un método. 2. Java pasa los valores primitivos por valor (no hay argumento alguno allí), pero los objetos son pasados por referencia. Ésta es la visión mundial de que la referencia es un alias para el objeto, así que usted no piensa en pasar referencias, pero en lugar de eso dice “ Estoy pasando el objeto.” Ya que usted no obtiene una copia local del objeto cuando lo pasa a un método, los objetos claramente no son pasados por valor. Parece haber algo de soporte para este punto de vista dentro de Sun, ya que en algún momento, uno de las palabras claves “reservadas pero no implementadas” fue byvalue (la cual probablemente nunca será implementada). Habiendo dado a ambos campos una buena exposición, y después de decir “Depende de cómo piense usted de una referencia,” intentaré obviar el asunto. Al fin y al cabo, no es tan importante - lo que es importante es que usted entienda que pasar una referencia permite que el objeto llamante sea cambiado inesperadamente. [ 1] En C, el cual generalmente maneja pequeños bits de información, el paso por valor es la forma predefinida de hacerlo. C++ tenía que seguir esta forma, pero el paso por valor de objetos no es usualmente el camino más eficiente. Adicionalmente, la codificación de las clases para que soporten el paso por valor es un gran dolor de cabeza en C++. Clonación de objetos La razón más probable para hacer una copia local de un objeto es si usted va a modificar el objeto y no quiere modificar el objeto llamante. Si usted decide que quiere hacer una copia local, una alternativa es usar el método clone () para realizar la operación. Éste es un método que es definido como protected en la clase base Object, y que usted debe redefinir como public en cualquier clase derivada que quiera clonar. Por ejemplo, la clase ArrayList de la librería estándar redefine clone (), así es que podemos llamar clone () para ArrayList: //: apéndicea:Cloning.java // La operación de clone() trabaja solo para algunos pocos // items en la librería estándar de Java. import com.bruceeckel.simpletest.*; import java.util.*; class Int { private int i; public Int(int ii) { i = ii; } public void increment() { i++; } public String toString() { return Integer.toString(i); } } public class Cloning { private static Test monitor = new Test(); public static void main(String[] args) { ArrayList v = new ArrayList(); for (int i = 0; i < 10; i++ ) v.add( new Int(i)); System.out.println("v: " + v); ArrayList v2 = (ArrayList)v.clone(); // Incremente todos los elementos de v2: for (Iterator e = v2.iterator(); e.hasNext(); ) ((Int)e.next()).increment(); // Vea si cambió los elementos de v: System.out.println("v: " + v); monitor.expect(new String[] { "v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", "v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" }); } } ///:~ El método clone () produce un Objeto, al cual se le debe hacer un cast al tipo correcto . Este ejemplo muestra cómo el método clone() de ArrayList no trata automáticamente de clonar cada uno de los objetos que el ArrayList contiene - el viejo ArrayList y el ArrayList clonado son aliased a los mismos objetos. Esto es a menudo llamado una copia superficial, ya que es copiar solo la porción "superficial" de un objeto. El objeto real consta de esta " superficie, " más todos los objetos a los que las referencias apuntan, más todos los objetos a los que esos objetos apuntan, etc. Esto es a menudo llamado " la red de objetos." Copiar toda la malla es llamado una copia profunda. Usted puede ver el efecto de la copia superficial en la salida, donde las acciones realizadas sobre v2 afectan a v: v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] No tratar de aplicar clone() a los objetos contenidos en el ArrayList es probablemente una suposición justa, porque no hay seguridad de que esos objetos sean clonables.117 Añadiendo clonabilidad a una clase Si bien el método de clonación está definido en Object, la-base-de-todas -lasclases, la clonación no está automáticamente disponible en cada clase. 118 Esto parecería ser contra -intuitivo a la idea de que los métodos de la clase base están siempre disponibles en las clases derivadas. La clonación en Java ciertamente va en contra de esta idea; si quiere que exista en una clase, usted tiene que específicamente añadir código para hacer que la clonación funcione. Usando un truco con protected Para impedir la clonabilidad por defecto en cada clase que usted cree, el método clone () es protected en la clase base Object. Esto no solo significa que la clonabilidad no está disponible por defecto para el programador cliente que simplemente usa la clase (no generando clases derivadas), pero también [ 2] Este no es el deletreo que se encuentra en el diccionario para esta palabra, pero es el utilizado en la biblioteca de Java, por lo cual yo lo he usado aquí también, esperando reducir la confusión. [3] Usted puede aparentemente crear un contra -ejemplo simple contra esta afirmación, como este: public class Cloneit implements Cloneable { public static void main (String[] args) throws CloneNotSupportedException { Cloneit a = new Cloneit(); Cloneit b = (Cloneit)a.clone(); } } Sin embargo, esto solo funciona porque main( ) es un método de Cloneit y así tiene permiso para llamar el método protected clone( ) de la clase base. Si lo llama desde una clase diferente, no compilará. quiere decir que usted no puede llamar clone () por medio de una referencia a la clase base (aunque eso podría parecer ser útil en algunas situaciones, como para clonar polimórficamente un montón de Objetos.) Es, en efecto, una forma para darle a usted, en el tiempo de compilación, la información de que su objeto no es clonable - y por raro que parezca, la mayoría de las clases en la biblioteca estándar de Java no lo son. Así, si Usted dice: Integer x = new Integer(1); x = x.clone(); Usted obtendrá, en la fase de compilación, un mensaje de error que dice que clone () no es accesible (ya que Integer no le invalida y y este revierte a la versión protected). Sin embargo, si usted está en un méto do de una clase derivada de Object (como lo son todas las clases), entonces usted está autorizado para llamar a Object.clone () porque es protected y usted está heredando. El método clone() de la clase base tiene una útil funcionalidad; lleva a cabo la duplicación real bit a bit del objeto de la clase derivada, actuando así como la operación común de clonación. Sin embargo, usted luego necesita convertir en public su operación de clonación para que sea accesible. En consecuencia, dos asuntos claves cuando u sted lleva a cabo una clonación son: • • Llamar a super.clone () Convertir su clon en public Usted probablemente querrá anular clone () en cualquier clase derivada adicional; de otra manera, su clone (),(ahora public) será usado, y eso podría no hacer lo correcto (aunque, ya que Object.clone () hace una copia del objeto mismo, también podría ser que sí). El truco con protected surte efecto sólo una vez: la primera vez que usted hereda de una clase que no tiene clonabilidad y usted quiere hacer una clase que es clonable. En cualquier clase derivada de la suya, el método clone() está disponible ya que durante la derivación no es posible limitar en Java el acceso a un método. Es decir, una vez que una clase es clonable, cualquier cosa derivada de ella lo es también a menos que usted use los mecanismos provistos para "desactivar" la clonación (los cuales se describen posteriormente). Implementando la interfaz C loneable Hay una cosa más que usted necesita hacer para completar la clonabilidad de un objeto: Implementar la interfaz Cloneable. Esta interfaz es un poco extraña, porque está vacía! interface Cloneable {} La razón para implementar esta interfaz vacía es obviamente no porque usted vaya a hacer casting hacia arriba a Cloneable y vaya a llamar a uno de sus métodos. El uso de interface de este modo es llamado una interfaz de etiqueta porque actúa como un tipo de bandera, embebido en el tipo de la clase. Hay dos razones para la existencia de la interfaz Cloneable. Primero, usted podría tener una referencia a la que se le ha hecho casting hacia arriba a un tipo base y no sabe si es posible clonar ese objeto. En este caso, usted puede usar la palabra clave instanceof (descrita en el Capítulo 10) para averiguar si la referencia está conectada a un objeto que puede ser clonado: if(myReference instanceof Cloneable) // ... La segunda razón es que mezclado en este diseño sobre clonabilidad estaba el pensamiento de que tal vez usted no quería que todos los tipos de objetos fueran clonables. Así, Object.clone () comprueba que una clase implementa la interfaz Cloneable. En caso de que no, lanza una excepción CloneNotSupportedException. En general entonces, usted se ve forzado a implementar a Cloneable como parte del soporte para la clonación. La clonación exitosa Una vez que usted entiende los detalles de implementar el método clone (), usted puede crear clases que pueden ser fácilmente duplicadas para proveer una copia local: //: appendixa:LocalCopy.java // Creando copias locales con clone(). import com.bruceeckel.simpletest.*; import java.util.*; class MyObject implements Cloneable { private int n; public MyObject(int n) { this.n = n; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println( "MyObject can't clone" ); } return o; } public int getValue() { return n; } public void setValue(int n) { this.n = n; } public void increment() { n++; } public String toString() { return Integer.toString(n); } } public class LocalCopy { private static Test monitor = new Test(); public static MyObject g(MyObject v) { // Pasando una referencia, modifica un objeto externo: v.increment(); return v; } public static MyObject f(MyObject v) { v = (MyObject)v.clone (); // Copia local v.increment(); return v; } public static void main(String[] args) { MyObject a = new MyObject(11); MyObject b = g(a); // Equivalencia de referencias, no de objetos: System.out.println("a == b: " + (a == b) + "\na = " + a + "\nb = " + b); MyObject c = new MyObject(47); MyObject d = f(c); System.out.println("c == d: " + (c == d) + "\nc = " + c + "\nd = " + d); monitor.expect(new String[] { "a == b: true" , "a = 12", "b = 12", "c == d: false", "c = 47", "d = 48" }); } } ///:~ Primero que todo, para que clone () pueda ser accesible, lo tiene que hacer public. En segundo lugar, para la parte inicial de la operación de su clone(), usted debería llamar la versión de clone() de la clase base . El clone() que se llama aquí es el que está predefinido dentro de Object, y usted lo puede llamar porque es protected y por consiguiente accesible en las clases derivadas. Object.clone () calcula qué tan grande es el objeto, crea suficiente memoria para una nuevo, y copia todos los bits del viejo al nuevo. Ésta es llamado una copia bit a bit, y es típicamente lo que usted esperaría que haga un método clone(). Pero antes de que Object.clone () realice sus operaciones, primero inspecciona para ver si una clase es Cloneable -es decir, si implementa la interfaz Cloneable. Si no lo hace, Object.clone () lanza una excepción CloneNotSupportedException para señalar que usted no le puede clonar. Así, usted tiene que envolver su llamada a super.clone () con un bloque try para capturar una excepción que nunca debería ocurrir (porque usted ha implementado la interfaz Cloneable). En LocalCopy, los dos métodos g() y f () demuestran la diferencia entre las dos maneras de pasar argumentos. El método g() muestra el paso por refere ncia en el cual el método modifica el objeto exterior y devuelve una referencia a ese objeto exterior, mientras que f() clona el argumento, por consiguiente desacoplándolo y dejando sin tocar el objeto original. Luego puede proceder a hacer lo que quiera -aun retornar una referencia a este objeto nuevo sin afectar para nada el original. Note la declaración de aspecto algo curioso: v = (MyObject)v.clone(); Aquí es donde se crea la copia local. Para prevenir la confusión que tal declaración podría causar, recuerde que esta más bien extraña forma de codificar es perfectamente factible en Java porque cada identificador de un objeto es de hecho una referencia. Así es que la referencia v se usa para clone() una copia de a lo que ella se está refiriendo, y esta devuelve una referencia al tipo base Object (porque está definido de ese modo en Object.clone () ) al que luego hay que hacerle un casting al tipo correcto. En main() se prueba la diferencia entre los efectos de los dos diferentes formas de paso de argumentos. Es importante tener en cuenta que las pruebas de equivalencia en Java no miran en el interior de los objetos que están siendo comparados para ver si sus valores son lo mismos. Los operadores == y ! = simplemente comparan las referencias. Si las direcciones dentro de las referencias son las mismas, ellas apuntan hacia el mismo objeto y por consiguiente son “iguales.” Por ende, lo que los operadores realmente prueban es si las referencias están aliased al mismo objeto. El efecto de Object.clone () ¿Qué ocurre realmente cuando Object.clone () es llamado que hace tan esencial llamar a super.clone () cuando usted anula clone () en su clase? El método clone() en la clase raíz es responsable de crear la cantidad correcta de almacenamiento y hacer la copia bit a bit de los bits del objeto original en el espacio de almacenamiento del objeto nuevo. Esto es,no hace solo almacenamiento y copia un Object sino que realmente calcula el tamaño del objeto real (no solo el objeto de clase base, sino el objeto derivado) que está siendo copiado y duplica eso. Ya todo esto está ocurriendo desde el código en el método clone() definido en la clase raíz (la cual no tiene idea de qué está siendo heredado desde ella ), usted puede adivinar que el proceso exige a RTTI determinar el objeto que está siendo realmente clonado. Así, el método clone() puede crear la cantidad correcta de almacenamiento y puede hacer la copia correcta bit a bit para ese tipo. No importando lo que usted haga, la primera parte del proceso de clonación normalmente debería ser una llamada a super.clone (). Esto establece el trabajo de base para la operación de clonación mediante la elaboración de un duplicado exacto. En este punto usted puede realizar otras operaciones necesarias para completar la clonación. Para saber co n seguridad cuáles son esas otras operaciones, usted necesidad entender exactamente qué hace exactamente Object.clone (). ¿En particular, clona automáticamente el destino de todas las referencias? El siguiente ejemplo prueba esto: //: apéndicea:Snake.java // Prueba la clonación para ver si el destino // de las referencias también es clonado. import com.bruceeckel.simpletest.*; public class Snake implements Cloneable { private static Test monitor = new Test(); private Snake next; private char c; // Valor de i == número de segmentos public Snake(int i, char x) { c = x; if(--i > 0) next = new Snake(i, ( char)(x + 1)); } public void increment() { c++; if(next != null) next.increment(); } public String toString() { String s = ":" + c; if(next != null) s += next.toString(); return s; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println( "Snake can't cl one"); } return o; } public static void main(String[] args) { Snake s = new Snake(5, 'a'); System.out.println("s = " + s); Snake s2 = (Snake)s.clone(); System.out.println("s2 = " + s2); s.increment(); System.out.println("after s.increment, s2 = " + s2); monitor.expect(new String[] { "s = :a:b:c:d:e" , "s2 = :a:b:c:d:e", "after s.increment, s2 = :a:c:d:e:f" }); } } ///:~ Una Snake está hecha de un montón de segmentos, cada uno de tipo Snake. Así, es una lista enlazada simple. Los segmentos son creados recursivamente, decrementando el argumento del primer constructor para cada segmento hasta que se alcanza el cero. Para darle a cada segmento una etiqueta única, el segundo argumento, un char, es incrementado en cada llamada recursiva del constructor. El método increment() aumenta recursivamente cada etiqueta para que usted pueda ver el cambio, y el método toString () imprime recursivamente cada etiqueta. De la salida, usted puede ver que sólo el primer segmento es duplicado por Object.clone (), por consiguiente hace una copia superficial. Si usted quiere que la serpiente entera sea duplicado - una copia a fondo - usted debe realizar las operaciones adicionales dentro de su método clone() anulado . Típicamente usted llamará a super.clone () en cualquier clase derivada de una clase clonable para asegurarse de que todas las operaciones de la clase base (incluyendo a Object.clone ()) tengan lugar. Esto es seguido por una llamada explícita a clone() para cada referencia en su objeto; de otra manera esas referencias serán aliased a aquellas del objeto original. Es análogo a la forma cómo los constructores son llamados: primero el constructor de la clase base, luego el siguiente constructor derivado, y así sucesivamente, hasta el último constructor derivado. La diferencia es que clone() no es un constructor, así que no hay nada para que suceda automáticamente. Usted debe asegurarse de hacerlo usted mismo. Clonando un objeto compuesto Hay un problema que usted encontrará cuándo intente copiar a fondo un objeto compuesto. Usted debe asumir que el método clone() en los objetos miembro a su vez realizarán una copia a fondo sobre sus referencias, y así sucesivamente. Esto es una gran obligación. Significa efectivamente que para que una copia a fondo funcione, usted debe ya sea controlar todo el código en todas las clases, o al menos tener suficiente conocimiento de todas las clases involucradas en la copia a fondo para saber que están llevando a cabo correctamente su propia copia a fondo. Este ejemplo muestra lo que usted debe hacer para lograr una copia a fondo cuando esté trabajando con un objeto compuesto: //: apéndicea:DeepCopy.java // Clonando un objeto compuesto. // {Depende de: junit.jar} import junit.framework.*; class DepthReading implements Cloneable { private double depth; public DepthReading( double depth) { this.depth = depth; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } return o; } public double getDepth() { return depth; } public void setDepth(double depth){ this.depth = depth; } public String toString() { return String.valueOf(depth);} } class TemperatureReading implements Cloneable { private long time; private double temperature; public TemperatureReading(double tempera ture) { time = System.currentTimeMillis(); this.temperature = temperature; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } return o; } public double getTemperature() { return temperature; } public void setTemperature(double temperature) { this.temperature = temperature; } public String toString() { return String.valueOf(temperature); } } class OceanReading implements Cloneable { private DepthReading depth; private TemperatureReading temperature; public OceanReading( double tdata, double ddata) { temperature = new TemperatureReading(tdata); depth = new DepthReading(ddata); } public Object clone() { OceanReading o = null ; try { o = (OceanReading)super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(); } // Debe clonar las referencias: o.depth = (DepthReading)o.depth.clone(); o.temperature = (TemperatureReading)o.temperature.clone(); return o; // Casting inverso a Object } public TemperatureReading getTemperatureReading() { return temperature; } public void setTemperatureReading(TemperatureReading tr){ temperature = tr; } public DepthReading getDepthReading() { return depth; } public void setDepthReading(DepthReading dr) { this.depth = dr; } public String toString() { return "temperature: " + temperature + ", depth: " + depth; } } public class DeepCopy extends TestCase { public DeepCopy(String name) { super(name); } public void testClone() { OceanReading reading = new OceanReading(33.9, 100.5); // Ahora, clónelo: OceanReading clone = (OceanReading)reading.clone(); TemperatureReading tr = clone.getTemperatureReading(); tr.setTemperature(tr.getTemperature() + 1); clone.setTemperatureReading(tr); DepthReading dr = clone.getDepthReading(); dr.setDepth(dr.getDepth() + 1); clone.setDepthReading(dr); assertEquals(reading.toString(), "temperature: 33.9, depth: 100.5"); assertEquals(clone.toString(), "temperature: 34.9, depth: 101.5"); } public static void main(String[] args) { junit.textui.TestRunner.run(DeepCopy.class); } } ///:~ DepthReading y TemperatureReading son muy similares; Ambos contienen sólo primitivas. Por consiguiente, el método clone() puede ser muy simple: llama a super.clone () y devuelve el resultado. Note que el código de clone() para ambas clases es idéntico. OceanReading está compuesto de los objetos DepthReading y TemperatureReading y por consiguiente, para producir una copia a fondo, su método clone() deba clonar las referencias dentro de OceanReading. Para lograrlo, a l resultado de super.clone () se le debe hacer casting a un objeto OceanReading (para que usted puede ganar acceso a las referencias depth y temperature ). Una copia a fondo con ArrayList Volvamos a visitar a Cloning.java que vimos antes en esta apéndice. Esta vez la clase Int2 es clonable, para que se pueda hacer una copia a fondo de ArrayList: //: apèndicea:AddingClone.java // Usted debe pasar por una serie de giros // para añadir la clonación a su propia clase. import com.bruceeckel.simpletest.*; import java.util.*; class Int2 implements Cloneable { private int i; public Int2(int ii) { i = ii; } public void increment() { i++; } public String toString() { return Integer.toString(i); } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println( "Int2 can't clone"); } return o; } } // La herencia no remueve la clonabilidad: class Int3 extends Int2 { private int j; // Duplicado automáticamente public Int3(int i) { super(i); } } public class AddingClone { private static Test monitor = new Test(); public static void main(String[] args) { Int2 x = new Int2(10); Int2 x2 = (Int2)x.clone(); x2.increment(); System.out.println("x = " + x + ", x2 = " + x2); // Cualquier cosa heredada también es clonable: Int3 x3 = new Int3(7); x3 = (Int3)x3.clone(); ArrayList v = new ArrayList(); for (int i = 0; i < 10; i++ ) v.add( new Int2(i)); System.out.println("v: " + v); ArrayList v2 = (ArrayList)v.clone(); // Ahora clone cada elemento: for (int i = 0; i < v.size(); i++) v2.set(i, ((Int2)v2.get(i)).clone()); // Incremente todos loe elementos de v2: for (Iterator e = v2.iterator(); e.hasNext(); ) ((Int2)e.next()).increment(); System.out.println("v2: " + v2); // Vea si cambió los elementos de v: System.out.println("v: " + v); monitor.expect(new String[] { "x = 10, x2 = 11", "v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", "v2: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", "v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" }); } } ///:~ Int3 es heredado de Int2, y se agrega un miembro primitivo nuevo, int j, . Usted podría pensar que usted necesitaría anular clone() otra vez para asegurar que j sea copiado, pero ese no es el caso. Cuando se llama a clone() de Int2() como el clone() de Int3(), aquel llama a Object.clone(), lo cual determina que está trabajando con un Int3 y duplica todos los bits en Int3. Mientras que usted no agrege referencias que necesiten ser clonadas, la llamada a Object.clone () realiza todas la duplicación necesaria no importa qué tan profundo en la jerarquía esté definido clone(). Usted puede ver qué es lo que se necesita para hacer una copia a fondo de un ArrayList: Después de que el ArrayList es clonado, usted tiene que avanzar y clonar cada uno de los objetos a los que ArrayList apunta. Usted tendría que hacer algo parecido a esto para hacer una copia a fondo de un HashMap. El resto del ejemplo comprueba que la clonación ocurrió mostrando que, una vez que un objeto es clonado, usted lo puede cambiar y sin embargo, el objeto original no sufre modificación alguna. Copia a fondo mediante la serialización Cuando usted considera la serialización de objetos de Java (presentado en el Capítulo 12), usted podría observar que un objeto que es serializado y luego deserializado, efectivamente ha sido clonado. ¿Así es que por qué no usar la serialización para llevar a cabo la copia a fondo? Aquí hay un ejemplo que compara los dos métodos cronometrándolos: //: apéndicea:Compete.java import java.io.*; class Thing1 implements Serializable {} class Thing2 implements Serializable { Thing1 o1 = new Thing1(); } class Thing3 implements Cloneable { public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println( "Thing3 can't clone" ); } return o; } } class Thing4 implements Cloneable { private Thing3 o3 = new Thing3(); public Object clone() { Thing4 o = null; try { o = (Thing4)super.clone(); } catch(CloneNotSupportedException e) { System.err.println( "Thing4 can't clone" ); } // Clone el campo también: o.o3 = (Thing3)o3.clone(); return o; } } public class Compete { public static final int SIZE = 25000; public static void main(String[] args) throws Exception { Thing2[] a = new Thing2[SIZE]; for (int i = 0; i < a.length; i++) a[i] = new Thing2(); Thing4[] b = new Thing4[SIZE]; for (int i = 0; i < b.length; i++) b[i] = new Thing4(); long t1 = System.currentTimeMillis(); ByteArrayOutputStream buf= new ByteArrayOutputStream(); ObjectOutputStream o = new ObjectOutputStream(buf); for (int i = 0; i < a.length; i++) o.writeObject(a[i]); // Ahora obtenga copias: ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream(buf.toByteArray())); Thing2[] c = new Thing2[SIZE]; for (int i = 0; i < c.length; i++) c[i] = (Thing2)in.readObject(); long t2 = System.currentTimeMillis(); System.out.println("Duplication via serialization: " + (t2 - t1) + " Milliseconds"); // Ahora intente la clonación: t1 = System.currentTimeMillis(); Thing4[] d = new Thing4[SIZE]; for (int i = 0; i < d.length; i++) d[i] = (Thing4)b[i].clone(); t2 = System.currentTimeMillis(); System.out.println("Duplication via cloning: " + (t2 - t1) + " Millisecon ds"); } } ///:~ Thing2 y Thing4 contienen objetos miembro de tal manera que hay alguna copia a fondo tomando lugar. Es interesante notar que mientras es fácil armar clases Serializable , es mucho más laborioso duplicarlas. La clonación implica un montón de trabajo para armar la clase, pero la duplicación en sí de los objetos es relativamente simple. Los resultados son interesantes. Aquí está la salida de tres corridas diferentes: Duplicación mediante serialización: 547 Milisegundos Duplicación mediante c lonación: 110 Milisegundos Duplicación mediante serialización: 547 Milisegundos Duplicación mediante clonación: 109 Milisegundos Duplicación mediante serialización: 547 Milisegundos Duplicación mediante clonación: 125 Milisegundos En versiones previas del JDK, e l tiempo requerido para la serialización era mucho más largo que para la clonación (aproximadamente 15 veces más lento), y el tiempo de serialización tendía a variar bastante. Versiones más recientes han acelerado la serialización y aparentemente también han hecho el tiempo más consistente. Aquí, es aproximadamente cuatro veces más lento, lo que lo hace razonable para usar como una alternativa de clonación. Añadiendo jerarquía clonabilidad más abajo en la Si usted crea una clase nueva, su clase base se revierte a Object, y por consiguiente a la no clonabilidad (como se verá en la siguiente sección). Mientras usted explícitamente no adicione clonabilidad, usted no la tendrá. Pero la puede añadir en cualquier nivel y entonces será clonable de ese nivel hacia abajo, como esto: //: apéndicea:HorrorFlick.java // Usted puede insertar clonabilidad herencia package appendixa; import java.util.*; class Person {} en cualquier nivel de class Hero extends Person {} class Scientist extends Person implements Cloneable { public Object clone() { try { return super.clone(); } catch(CloneNotSupportedException e) { // Esto nunca debería suceder. Ya es clonable ! throw new RuntimeException(e); } } } class MadScientist extends Scientist {} public class HorrorFlick { public static void main(String[] args) { Person p = new Person(); Hero h = new Hero(); Scientist s = new Scientist(); MadScientist m = new MadScientist(); //! p = (Person)p.clone(); // Error de compilación //! h = (Hero)h.clone(); // Error de compilación s = (Scientist)s.clone(); m = (MadScientist)m.clone(); } } ///:~ Antes de que se agregase la clonabilidad en la jerarquía, e l compilador le impidió intentar copiar cosas. Cuando se añade la clonabilidad en Scientist, entonces éste y todos sus descendientes son clonables ¿Por qué este diseño extraño? Si todo esto parece ser un esquema extraño, es que efectivamente lo es. Usted podría preguntarse por qué resultó así. ¿Cuál es la idea detrás de este diseño? Originalmente, Java fue diseñado como un lenguaje para monitorear elementos de hardware, y definitivamente no con Internet en mente. En un lenguaje de propósito general como éste, tiene sentido que el programador pueda clonar cualquier objeto. Por ello, clone() fue colocado en la clase raíz Object, pero era un método public así es que usted siempre podría clonar cualquier objeto. Éste parecía ser el acercamiento más flexible, y después de todo, ¿ qué daño podría hacer? Bien, cuando se vio a Java como el lenguaje de programación más adecuado para Internet, las cosas cambiaron. Repentinamente, hay preocupaciones de seguridad, y por supuesto, estos asuntos se manejan usando objetos, y usted necesariamente no quiere que cualquiera pueda clonar sus objetos que manejan la seguridad. Por tanto, lo que usted está viendo es una cantidad de parches aplicados sobre el original esquema simple y franco: clone() es ahora protected en Object. Usted debe anularlo e implementar Cloneable y, debe ocuparse de las excepciones. Vale notar que usted debe implementar la interfaz Cloneable sólo si usted va a llamar el método clone() de Object, ya que este método inspecciona durante el tiempo de ejecución si su clase implementa Cloneable. Pero por consistencia (y ya que Cloneable de cualquier manera está vacío), usted lo debería implementar. Monitoreando la clonabilidad Usted podría sugerir que para quitar la clonabilidad, el método clone() simplemente debería hacerse private, pero esto no funcionará, porque no puede tomar un método de una clase base y hacerlo menos accesible en una clase derivada. Y sin embargo, es necesario poder controlar si un objeto puede ser clonado. Hay un número de actitudes que usted puede tomar para lograr esto en sus clases: 1. La indiferencia. Usted no hace nada sobre la clonación, lo cual quiere decir que su clase no puede ser clonada, pero una clase que herede de usted puede añadir clonación si lo quiere. Esto trabaja únicamente si el método Object.clone() predeterminado hace algo razonable con todos los campos en su clase. 2. Soporte clone(). Siga la práctica estándar de implementar Cloneable y sobrescribir clone(). En el clone() sobrescrito , usted llama a super.clone () y captura todas las excepciones (para que su clone() sobrescrito no lance excepción alguna). 3. Soporte condicionalmente la clonación. Si su clase tiene referencias a otros objetos que podrían o no ser clonables (una clase contenedora, por ejemplo), su clone() puede tratar de clonar todos los objetos para los cuales usted tiene referencias, y si lanzan excepciones, simplemente pasarlas al programador. Por ejemplo, considere un tipo especial de ArrayList que trata clonar todos los objetos que tiene. Cuando usted escribe un ArrayList de este tipo, usted no conoce qué tipo de objetos el programador cliente podría poner en su ArrayList, así que usted no sabe si pueden o no ser clonados. 4. No implemente a Cloneable pero sobrescriba clone() como protected, produciendo así la conducta correcta de copiado para cualquiera de los campos. De esta forma, cualquiera que herede de esta clase puede sobrescribir clone() y llamar a super.clone () para producir la conducta de copiado correcta. Note que su implementación puede y debería invocar a super.clone () si bien ese método espera un objeto Cloneable (de otra manera lanzará una excepción), porque nadie lo invocará directamente sobre un objeto de su tipo. Será invocado sólo a través de una clase derivada, la cual, si se quiere que trabaje exitosamente, implementa a Cloneable. 5. Trate de impedir la clonación no implementando a Cloneable y sobrescribiendo clone() para lanzar una excepción. Esto tiene éxito sólo si cualquier clase derivada llama a super.clone () en su redefinición de clone(). De otra manera, un programador puede encontrar la man era de eludir esta situación. 6. Impida la clonación haciendo que su clase sea final. Si clone() no ha sido sobrescrito por alguna de sus clases ancestrales, ya no lo puede ser. Si lo ha sido, entonces sobrescríbalo otra vez y lance una excepción CloneNotSupportedException. Hacer la clase final es la única forma para garantizar que se impida la clonación. Además, cuando esté manejando objetos de seguridad u otras situaciones en las cuales usted quiere controlar el número de objetos creados, usted debería hacer a todos los constructores private y debería proveer uno o más métodos especiales para la creación de objetos. De ese modo, estos métodos pueden restringir el número de objetos creados y las condiciones en las cuales son creados. (Un caso particular de esto es el patrón singleton mostrado en Thinking in Patterns (with Java) en www.BruceEckel.com.) Aquí hay un ejemplo que muestra las formas diversas como se puede implementar la clonación y luego, más adentro en la jerarquía, cómo "cancelarla": //: apéndicea:CheckCloneable.java // Verificando para ver si una referencia puede ser clonada. import com.bruceeckel.simpletest.*; // Esto no se puede clonar porque no anula a clone(): class Ordinary {} // Sobreescribe clone, pero no implementa Cloneable: class WrongClone extends Ordinary { public Object clone() throws CloneNotSupportedException { return super.clone(); // Lanza una excepción } } // Hace todo correcto para la clonación: class IsCloneable extends Ordinary implements Cloneable { public Object clone() throws CloneNotSupportedException { return super.clone(); } } // Cancela la clonación mediante el lanzamiento de la excepción: class NoMore extends IsCloneable { public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } } class TryMore extends NoMore { public Object clone() throws CloneNotSupportedException { // Llama a NoMore.clone(), lanza una excepción: return super.clone(); } } class BackOn extends NoMore { private BackOn duplicate(BackOn b) { // De alguna manera haga una copia de b y retórnela // Esta es una copia falsa, solo para marcar el punto: return new BackOn(); } public Object clone() { // No llama a NoMore.clone(): return duplicate(this ); } } // Usted no puede heredar de esto, por lo tanto sobreescribir // el método clone como sí lo puede hacer en BackOn: final class ReallyNoMore extends NoMore {} no public class CheckCloneable { private static Test monitor = new Test(); public static Ordinary tryToClone(Ordinary ord) { String id = ord.getClass().getName(); System.out.println("Attempting " + id); Ordinary x = null; if(ord instanceof Cloneable) { try { x = (Ordinary)((IsCloneable)ord).clone(); System.out.println( "Cloned " + id); } catch(CloneNotSupportedException e) { System.err.println( "Could not clone " + id); } } else { System.out.println("Doesn't implement Clo neable"); } return x; } public static void main(String[] args) { // Casting inverso: Ordinary[] ord = { new IsCloneable(), new WrongClone(), new NoMore(), new TryMore(), new BackOn(), new ReallyNoMore (), }; Ordinary x = new Ordinary(); // Esto no compilará; clone() es protected en Object: //! x = (Ordinary)x.clone(); // Primero verifica si una clase implementa Cloneable: for (int i = 0; i < ord.length; i++) tryToClone(ord[i ]); monitor.expect(new String[] { "Attempting IsCloneable", puede "Cloned IsCloneable", "Attempting WrongClone", "Doesn't implement Cloneable", "Attempting NoMore", "Could not clone NoMore", "Attempting TryMore", "Could not clone TryMore", "Attempting BackOn", "Cloned BackOn", "Attempting ReallyNoMore", "Could not clone ReallyNoMore" }); } } ///:~ La primera clase, Ordinary, representa los tipos de clases que hemos visto a todo lo largo de este libro: ningún soporte para la clonación, pero al mismo tiempo, tampoco ninguna prevención de ella. Pero si usted tiene una referencia a un objeto Ordinary con casting inverso desde una clase más derivada, usted no puede establecer si puede o no ser clonado. La clase WrongClone muestra una forma incorrecta de implementar la clonación. Sobreescribe Object.clone () y convierte a ese método en public, pero no implementa Cloneable, así que cuando super.clone () es llamado ( lo que redunda en una llamada a Object.clone ()), se lanza la excepción CloneNotSupportedException, con lo que la clonación no surte efecto. IsCloneable realiza todas las acciones correctas para llevar a cabo la clonación; se sobrescribe clone() y se implementa Cloneable. Sin embargo, este método clone() y varios otros que siguen en este ejemplo no captura la excepción CloneNotSupportedException. En lugar de eso lo pasa a quien llama, quien tiene entonces que colocar un bloque try -catch alrededor de él. En sus propios métodos clone() Usted típicamente capturará la excepción CloneNotSupportedException() en el interior de clone() en vez de pasarla. Como usted verá, en este ejemplo es más informativo pasar las excepciones hasta el final. La clase NoMore intenta “ poner fuera de servicio” la clonación de la manera que los diseñadores Java originalmente pensaron: en la clase derivada clone(), usted lanza la excepción CloneNotSupportedException. El método clone() en la clase TryMore llama correctamente a super.clone (), y este recurre a NoMore.clone (), el cual lanza una excepción e impide la clonación. ¿Pero qué ocurre si el programador no sigue el camino “ correcto ” de llamar a super.clone () dentro del método clone() sobrescrito ? En BackOn, usted puede ver cómo puede ocurrir esto. Esta clase usa un método duplicate() separado para hacer una copia del objeto actual y llama este método dentro de clone() en lugar de llamar a super.clone (). La excepción nunca se lanza y la clase nueva es clonable. Usted no puede confiar en lanzar una excepción para impedir hacer una clase clonable. La única solución de éxito asegurado se ejemplariza en ReallyNoMore, la cual es final y por ende no puede ser here radal. Eso significa que si clone() lanza una excepción en la clase final, esta no puede ser modificado con herencia, y la prevención de la clonación es asegurada. (Usted no puede llamar explícitamente a Object.clone () desde una clase que tiene un nivel arbitrario de herencia; usted está limitado a llamar a super.clone (), el cual tiene acceso sólo a la clase base directa.) Así, si usted hace cualesquiera objetos que involucren asuntos de seguridad, usted querrá hacer esas clases final. El primer método que usted ve en la clase CheckCloneable es tryToClone (), el cual toma cualquier objeto Ordinary y con instanceof verifica si es o no clonable. Si es así, le hace un casting a un IsCloneable, llama a clone(), y el resultado lo devuelve mediante casting a Ordinary, capturando cualesquiera excepciones que se lancen. Note el uso de identificación de tipo en tiempo de ejecución (RTTI; vea el Capítulo 10) para imprimir el nombre de clase con el fin de que pueda ver qué está sucediendo. En main() se crean diferentes tipos de objetos Ordinary y en la definición del arreglo se les hace casting a Ordinary . Las primeras dos líneas de código después de eso crean un objeto Ordinary simple e intentan clonarlo. Sin embargo, este código no compilará porque clone() es un mé todo protected en Object. El resto del código procesa el array y trata de clonar cada objeto, dando cuenta del éxito o el fracaso en cada uno. Así para resumir, si usted quiere que una clase sea clonable: 1. Implemente la interfaz Cloneable. 2. Sobreescriba clone(). 3. Llame a super.clone() dentro de su clone(). 4. Capture las excepciones dentro de su clone(). Esto producirá los efectos más convenientes. El constructor de la copia Puede parecer que organizar la clonación sea un proceso complicado. Podría parecer que debería haber una alternativa. Una posibilidad es usar serialización, como se mostró anteriormente. Otro posibilidad que se le podría ocurrir a usted (especialmente si es un programador de C++) es hacer un constructor especial cuyo trabajo sea duplicar un objeto. En C++, esto se llama el constructor copia. Al principio, esto parece la solución obvia, pero en realidad no funciona. Aquí hay un ejemplo: //: apéndicea:CopyConstructor.java // Un constructor para copiar un objeto del mismo // tipo, como un intento de crear una copia local. import com.bruceeckel.simpletest.*; import java.lang.reflect.*; class FruitQualities { private int weight; private int color; private int firmness; private int ripeness; private int smell; // etc. public FruitQualities() { // Constructor por defecto // Haga algo significativo... } // Otros constructores: // ... // Constructor Copia: public FruitQualities(FruitQualities f) { weight = f.weight; color = f.color; firmness = f.firmness; ripeness = f.ripeness; smell = f.smell; // etc. } } class Seed { // Miembros... public Seed() { /* Constructor por defecto */ } public Seed(Seed s) { /* Constructor Copia*/ } } class Fruit { private FruitQualities fq; private int seeds; private Seed[] s; public Fruit(FruitQualities q, int seedCount) { fq = q; seeds = seedCount; s = new Seed[seeds]; for (int i = 0; i < seeds; i++) s[i] = new Seed(); } // Otros constructores: // ... // Constructor Copia: public Fruit(Fruit f) { fq = new FruitQualities(f.fq); seeds = f.seeds; s = new Seed[seeds]; // Llame a todos los constructores copia Semilla: for (int i = 0; i < seeds; i++) s[i] = new Seed(f.s[i]); // Otras actividades de construcción de copias... } // Para permitir a los constructores derivados (o a otros // métodos) establecer cualidades diferentes: protected void addQualities(FruitQualities q) { fq = q; } protected FruitQualities getQualities() { return fq; } } class Tomato extends Fruit { public Tomato() { super(new FruitQualities(), 100); } public Tomato(Tomato t) { // Constructor Copia super(t); // Casting al constructor copia base // Otras actividades de construcción de copias... } } class ZebraQualities extends Fru itQualities { private int stripedness; public ZebraQualities() { // Constructor por defecto super(); // haga algo significativo... } public ZebraQualities(ZebraQualities z) { super(z); stripedness = z.stripedness; } } class GreenZebra extends Tomato { public GreenZebra() { addQualities( new ZebraQualities()); } public GreenZebra(GreenZebra g) { super(g); // Llama a Tomato(Tomato) // Reinstale las cualidades correctas: addQualities( new ZebraQualities()); } public void evaluate() { ZebraQualities zq = (ZebraQualities)getQualities(); // Haga algo con las cualidades // ... } } public class CopyConstructor { private static Test monitor = new Test(); public static void ripen(Tomato t) { // Utilice el "Constructor Copia": t = new Tomato(t); System.out.println("In ripen, t is a " + t.getClass().getName()); } public static void slice(Fruit f) { f = new Fruit(f); // Hmmm... funcionará esto? System.out.println("In slice, f is a " + f.getClass().getName()); } public static void ripen2(Tomato t) { try { Class c = t.getClass(); // Utilice el "constructor-copia": Constructor ct = c.getConstructor( new Class[] { c }); Object obj = ct.newIn stance(new Object[] { t }); System.out.println( "In ripen2, t is a " + obj.getClass().getName()); } catch(Exception e) { System.out.println(e); } } public static void slice2(Fruit f) { try { Class c = f.getClass(); Constructor ct = c.getConstructor( new Class[] { c }); Object obj = ct.newInstance(new Object[] { f }); System.out.println( "In slice2, f is a " + obj.getClass().getName()); } catch(Exception e) { System.out.println(e); } } public static void main(String[] args) { Tomato tomato = new Tomato(); ripen(tomato); // OK slice(tomato); // OOPS! ripen2(tomato); // OK slice2(tomato); // OK GreenZebra g = new GreenZebra(); ripen(g); // OOPS! slice(g); // OOPS! ripen2(g); // OK slice2(g); // OK g.evaluate(); monitor.expect(new String[] { "In ripen, t is a Tomato", "In slice, f is a Fruit", "In ripen2, t is a Tomato", "In slice2, f is a Tomato", "In ripen, t is a Tomato", "In slice, f is a Fruit", "In ripen2, t is a GreenZebra", "In slice2, f is a GreenZebra" }); } } ///:~ Esto parece un poco extraño al principio. ¿Seguro, la fruta tiene calidades, pero por qué no simplemente colocar campos representando esas calidades directamente en la clase Fruit? Hay dos razones potenciales. La primera es que Usted podría querer insertar o cambiar las calidades fácilmente. Note que Fruit tiene un método protected addQualities( ) para permitir a clases derivadas hacer esto. (Usted podría pensar que lo lógico a hacer es tener un constructor protected en Fruit que tome un argumento FruitQualities, pero los constructores no heredan, por lo que no estaría disponible en clases de segundo nivel o de niveles más altos.) Al tener las calidades de la fruta en una clase separada y usar composición, Usted tiene mayor flexibilidad, incluyendo la habilidad de cambiar las calidades en la mitad del tiempo de vida de un objeto Fruit particular. La segunda razón para hacer FruitQualities un objeto separado es para el caso en que Usted quiera añadir nuevas calidades o cambiar el comportamiento usando herencia y polimorfismo. Note que para GreenZebra (el cual realmente es un tipo de tomate – yo los he cultivado y son fabulosos), el constructor llama a addQualities( ) y le pasa un objeto ZebraQualities, el cual es derivado de FruitQualities, para que pueda ser fijado a la re ferencia FruitQualities en la clase base. Naturalmente, cuando GreenZebra usa FruitQualities, tiene que hacerle casting hacia abajo al tipo correcto (como se vió en evaluate( )), pero siempre sabe que el tipo es ZebraQualities. También verá Usted que hay una clase Seed, y que Fruit (que por definición tiene sus propias semillas)119 contiene un arreglo de Seeds. 119 Finalmente, note que cada clase tiene un constructor copia, y que cada uno debe llamar correctamente los constructores copia para la clase base y los objetos miembro con el fin de lograr una copia profunda. El constructor copia es evaluado dentro de la clase CopyConstructor. El método ripen( ) toma un argumento Tomato y realiza una construcción-copia sobre éste con el fin de duplicar el objeto: t = new Tomato(t); mientras slice( ) toma un objeto Fruit más genérico y también lo duplica: f = new Fruit(f); Estos son evaluados con diferentes clases de Fruit en main( ). Examinando el resultado, Usted puede ver el problema. Después de la construcción-copia que le ocurre a Tomato dentro de slice( ), el resultado ya no es más un objeto Tomato , es solo un Fruit. Ha perdido todas sus características de tomate. Adicionalmente, cuando toma un GreenZebra, ambos méto dos ripen( ) y slice( ) lo convierten en un Tomato y un Fruit, respectivamente. [ 3] Excepto por el pobre a guacate, el cual ha sido reclasificado a simplemente “grasa.” Así, desafortunadamente, la estrategia del constructor copia no nos es útil en Java cuando queremos hacer una copia local de un objeto. Por qué sí funciona en C++ y no en Java? El constructor copia es una parte fundamental de C++ ya que este hace automáticamente una copia local de un objeto. Sin embargo, el ejemplo precedente prueba que no funciona para Java. Por qué?. En Java todo lo que manipulamos es una referencia, pero en C++, Usted puede tener entidades tipo referencia y Usted también puede mover los objetos directamente. Para eso es para lo que el constructor copia sirve: cuando Usted quiere tomar un objeto y pasarlo por valor, duplicando así el objeto. Así que trabaja bien en C++, pero no debe olvidar que esta estrategia falla en Java, por lo tanto, no la use. Clases de solo-lectura Aunque la copia local producida por clone( ) da los resultados deseados en los casos apropiados, es un ejemplo de querer forzar al programador (al autor del método) a ser responsable de prevenir los efectos no deseados del aliasing. Qué pasaría si Usted estuviera haciendo una biblioteca que es de propósito tan general y tan comúnmente usada que Usted no puede asumir que siempre será clonada en los lugares apropiados? O más factiblemente, qué pasaría si Usted quiere permitir aliasing por cuestiones de eficiencia – para prevenir la innecesaria duplicación de objetos – pero Usted no desea sus efectos colaterales negativos? Una solución es crear objetos inmutables que pertenezcan a clases de solo lectura. Usted puede definir una clase de tal manera que ningún método en la clase ocasione cambios al estado interno del objeto. En tal clase, el aliasing no tiene impacto alguno ya que Usted puede leer solo el estado interno, con lo que si muchas secciones de código están leyendo el mismo objeto, no se presenta problema alguno. Como un ejemplo sencillo de objetos inmutables, la biblioteca estándar de Java contiene clases “envoltorio” para todos los tipos primitivos. Usted puede haber descubierto ya que, si quiere almacenar un int en un contenedor como por ejemplo un ArrayList (que solo toma referencias de Objetos), Usted puede envolver su int dentro de la clase Integer de la biblioteca estándar: //: apéndicea:ImmutableInteger.java // La clase Integer no puede ser cambiada. import java.util.*; public class ImmutableInteger { public static void main(String[] args) { List v = new ArrayList(); for (int i = 0; i < 10; i++) v.add( new Integer(i)); // Pero, cómo cambia Usted el int dentro de Integer? } } ///:~ La clase Integer (así como todas las clases “envoltorio” primitivas) implementan la inmutabilidad de una forma sencilla: No tiene método alguno que le permitan cambiar el objeto. Si Usted necesita un objeto que tenga un tipo primitivo que pueda ser modificado, debe crearlo Usted mismo. Afortunadamente, esto es trivial. La siguiente clase usa las convenciones de nombres de los JavaBeans: //: apéndicea:MutableInteger.java // Una clase envoltorio modificable. import com.bruceeckel.simpletest.*; import java.util.*; class IntValue { private int n; public IntValue(int x) { n = x; } public int getValue() { return n; } public void setValue(int n) { this.n = n; } public void increment() { n++; } public String toString() { return Integer.toString(n); } } public class MutableInteger { private static Test monitor = new Test(); public static void main(String[] args) { List v = new ArrayList(); for (int i = 0; i < 10; i++) v.add( new IntValue(i)); System.out.println(v); for (int i = 0; i < v.size(); i++) ((IntValue)v.get(i)).increment(); System.out.println(v); monitor.expect(new String[] { "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" }); } } ///:~ IntValue puede ser aún más simple si no hay problemas de privacidad, la inicialización por defecto a cero es adecuada (con lo que no necesita entonces el constructor), y no está interesado en imprimirlo (con lo que no necesita toString( ) ): class IntValue { int n; } Extraer el elemento y hacerle casting es un poco torpe, pero eso es una característica de ArrayList, no de IntValue . Creación de clases de solo lectura Es posible crear sus propias clases de solo ejemplo: lectura. A continuación un //: apéndicea:Immutable1.java // Objetos que no pueden ser modificados son inmunes al aliasing. import com.bruceeckel.simpletest.*; public class Immutable1 { private static Test monitor = new Test(); private int data; public Immutable1( int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable1 multiply(int multiplier) { return new Immutable1(data * multiplier); } public static void f(Immutable1 i1) { Immutable1 quad = i1.multiply(4); System.out.println("i1 = " + i1.read()); System.out.println("quad = " + quad.read()); } public static void main(String[] args) { Immutable1 x = new Immutable1(47); System.out.println("x = " + x.read()); f(x); System.out.println ("x = " + x.read()); monitor.expect(new String[] { "x = 47", "i1 = 47", "quad = 188", "x = 47" }); } } ///:~ Toda la información es private, y Usted verá que ninguno de los métodos public modifican la información. De hecho, el método que sí parece modificar un objeto es mulitply( ), pero este crea un nuevo objeto Immutable1 y deja el original incolumne. El método f( ) toma un objeto Immutable1 y lleva a cabo varias operaciones sobre el, y l salida de main( ) demuestra que no hay cambio alguno en x. Así, el objeto de x podría ser aliased muchas veces sin peligro ya que la clase Immutable1 está diseñada para garantizar que los objetos no puedan ser cambiados. Los inconvenientes de la inmutabilidad A primera vista crear una clase inmutable parece ser una solución elegante. Sin embargo, cuando quiera que necesite un objeto modificado de ese nuevo tipo, tiene que aguantarse la carga de la creación de un nuevo objeto así como generar potencialmente más frecuentes corridas de recolección de basura. Esto no es problema para algunas clases, pero para otras (tales como la clase String), esto es prohibitivamente costoso. La solución es crear una clase compañera que pueda ser modificada. Luego, cuando Usted esté haciendo una gran cantidad de cambios, puede cambiarse a usar la clase compañera modificable y regresar a la inmodificable cuanto haya terminado. El ejemplo previo puede ser cambiado para ejemplarizar esto: //: apéndicea:Immutable2.java // Una clase compañera para modificar objetos inmutables. import com.bruceeckel.simpletest.*; class Mutable { private int data; public Mutable(int initVal) { data = initVal; } public Mutable add(int x) { data += x; return this; } public Mutable multiply(int x) { data *= x; return this; } public Immutable2 makeImmutable2() { return new Immutable2(data); } } public class Immutable2 { private static Test monitor = new Test(); private int data; public Immutable2( int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable2 add(int x) { return new Immutable2(data + x); } public Immutable2 multiply(int x) { return new Immutable2(data * x); } public Mutable makeMutable() { return new Mutable(data); } public static Immutable2 modify1(Immutable2 y) { Immutable2 val = y.add(12); val = val.multiply(3); val = val.add(11); val = val.multiply(2); return val; } // Esto produce el mismo resultado: public static Immutable2 modify2(Immutable2 y) { Mutable m = y.makeMutable(); m.add(12).multiply(3).add(11).multiply(2); return m.makeImmutable2(); } public static void main(String[] args) { Immutable2 i2 = new Immutable2(47); Immutable2 r1 = modify1(i2); Immutable2 r2 = modify2(i2); System.out.println("i2 = " + i2.read()); System.out.println("r1 = " + r1.read()); System.out.println("r2 = " + r2.read()); monitor.expect(new String[] { "i2 = 47", "r1 = 376", "r2 = 376" }); } } ///:~ Immutable2 contiene métodos que, como antes, preservan la inmutabilidad de los objetos al producir nuevos cada vez que se desee una modificación. Estos son los métodos add( ) y mulitply( ). La clase compañera se llama Mutable , y tiene también métodos add( ) y multiply( ) , pero estos modifican el objeto Mutable en vez de crear uno nuevo. Adicionalmente, Mutable tiene un método para usar su información para producir un objeto Immutable2 y viceversa. Los dos métodos estáticos modify1( ) y modify2( ) muestran dos estrategias diferentes para producir el mismo resultado. En modify1( ), todo se hace dentro de la clase Immutable2 y Usted puede ver que en el proceso se crean cuatro nuevos objetos Immutable2. (Y en cada ocasión que val es reasignada, el objeto previo se convierte en basura.). En el método modify2( ), Usted puede observar que la primera acción es tomar a y de Immutable2 y producir de esta un Mutable . (Esto es como llamar a clone( ) tal como lo vio Usted antes, pero esta vez se crea un diferente tipo de objeto). A continuación el objeto Mutable se usa para realizar una gran cantidad de operaciones de cambio sin que se requiera la creación de muchos nuevos objetos. Finalmente, se revierte a un Immutable2. Aquí se crean dos objetos nuevos (el Mutable y el resultado, Immutable2) en vez de cuatro. En consecuencia, esta estrategia tiene sentido cuando: 1. Usted necesita objetos inmutables y 2. Usted necesita a menudo hacer una gran cantidad de modificaciones o 3. Es costosos crear nuevos objetos inmutables. Cadenas inmutab les Considere el código siguiente: //: apéndicea:Stringer .java import com.bruceeckel.simpletest.*; public class Stringer { private static Test monitor = new Test(); public static String upcase(String s) { return s.toUpperCase(); } public static void main(String[] args) { String q = new String("howdy"); System.out.println(q); // howdy String qq = upcase(q); System.out.println(qq); // HOWDY System.out.println(q); // howdy monitor.expect(new String[] { "howdy", "HOWDY", "howdy" }); } } ///:~ Cuando q se pasa a upcase( ) en realidad es una copia de la referencia a q. El objeto al cual está conectada esta referencia permanece en una única localización física. Las referencias son copiadas a medida que se requieren y se manipulan. Al mirar la definición de upcase( ), Usted puede ver que el nombre de la referencia que se pasa es s, y esta existe solo mientras se ejecuta el cuerpo de upcase( ). Cuando upcase( ) termina, la referencia local s desaparece. upcase( ) retorna el resultado el cual consiste en la cadena original con todos los caracteres en mayúsculas. Naturalmente, en realidad retorna una referencia al resultado. Pero resulta que la referencia que retorna es para un nuevo objeto, y el objeto q original se deja incolumne. Cómo sucede esto?. Constantes implícitas Si Usted dice: String s = "asdf" ; String x = Stringer.upcase(s); realmente quiere que el método upcase( ) modifique el argumento? En general, no, ya que un argumento usualmente parece al lector del código como un pedazo de información proporcionada al método, no algo para ser modificado. Esto es una garantía importante, ya que hace al código más fácil de escribir y entender. En C++, la disponibilidad de esta garantía fue tan importante que ameritó una palabra clave especial, const, que permitiera al programador asegurar que una referencia (puntero o referencia en C++) no podría ser usado para modificar el objeto original. Pero sin embargo, se requería que el programador en C++ fuera diligente y recordara usar const en todo lado. Esto puede causar confusión y además ser fácil de olvidar. Sobrecargando ‘+’ y el StringBuffer Los objetos de la clase String están diseñados para ser inmutables usando la técnica de la clase acompañante mostrada previamente. Si Usted examina la documentación del JDK para la clase String (que se encuentra sumarizada un poco más adelante en este apéndice), verá que cada método en la clase que aparentemente modifica un String en realidad crea y retorna un objeto String completamente nuevo que contiene la modificación. El String original no se toca para nada. Así, no hay una característica en Java como la de const en C++ para hacer que el compilador soporte la inmutabilidad de sus objetos. Si lo desea, Usted tiene que construirla Usted mismo, como lo hace String. Ya que los objetos String son inmutables, Usted puede hacer alias a un String particular cuantas veces quiera. Ya que es de solo lectura, no hay posibilidad alguna de que una referencia cambie algo que afecte a otras. Por lo tanto, un objeto de solo lectura resuelve bien el problema de aliasing También parece posible manejar todos los casos en los que Usted necesite un objeto modificado mediante la creación de una completamente nueva versión del objeto con las modificaciones, tal como String lo hace. Sin embargo , para algunas operaciones esto no es eficiente. Un caso en particular es el operador `+´ que ha sido sobrecargado para los objetos String. Sobrecargar significa que se le ha dado un significado adicional cuando se use con una clase particular. (Los ope radores ‘+’ and ‘+=’ para String son los únicos operadores sobrecargados en Java, y Java no le permite al programador sobrecargar ningún otro). 120 Cuando se usa con objetos String , el operador `+´ permite concatenar Strings : [ 4] C++ le permite al programador la sobrecarga de operadores cuando lo desee. Ya que esto puede ser a menudo un proceso complicado (ver el Capítulo 10 de Thinking in C++, 2a edición, Prentice H all, 2000),los creadores de Java consideraron esto como una “mala”característica que no debería ser incluida en Java. No era tan malo sin embargo ya que terminaron haciéndolo ellos mismos, e irónicamente, la sobrecarga de operadores sería mucho más fácil de usar en Java que en C++. Esto se puede apreciar en Python (ver www.Python.org) el cual tiene recolección de basura y una sobrecarga de operadores bastante sencilla.. String s = "abc" + foo + "def" + Integer.toString(47); Usted puede imaginar como podría esto suceder. La String “abc” podría tener un método append( ) que crea un nuevo objeto String conteniendo a “abc” concatenada con el contenido de foo. El nuevo objeto String creará entonces un nuevo String que añada “def” y así sucesivamente. Esto ciertamente funcionaría pero requeriría la creación de una gran cantidad de objetos String solo para conformar esta nueva String y luego Usted tendrá un puñado de objetos String intermedios a los que es necesario hacerles el proceso de recolección de basura. Sospecho que los creadores de Java intentaron primero esta estrategia (lo cual es una lección en diseño de software – Usted realmente no sabe nada sobre un sistema hasta que ensaya su código y logra algo que funcione). Sospecho también que descubrieron que su desempeño era inaceptable. La solución es una clase compañera mutable similar a la mostrada previamente. Para String, esta clase compañera se llama StringBuffer, y el compilador automáticamente crea un StringBuffer para evaluar ciertas expresiones, en particular cuando se usan los operadores sobrecargados ‘+’ and ‘+=’ con objetos String. El siguiente ejemplo muestra lo que sucede: //: apéndicea:ImmutableStrings.java // Demostración de StringBuffer. import com.bruceeckel.simpletest.*; public class ImmutableStrings { private static Test monitor = new Test(); public static void main(String[] args) { String foo = "foo" ; String s = "abc" + foo + "def" + Integer.toString(47); System.out.println(s); // El "equivalente” usando StringBuffer: StringBuffer sb = new StringBuffer("abc"); // Crea el String! sb.append(foo); sb.append("def"); // Crea el String! sb.append(Integer.toString(47)); System.out.println(sb); monitor.expect(new String[] { "abcfoodef47", "abcfoodef47" }); } } ///:~ En la creación del String s, el compilador está haciendo el equivalente aproximado del código subsiguiente que usa sb: se crea un StringBuffer, y se usa append( ) para añadir nuevos caracteres directamente en el objeto StringBuffer (en vez de hacer nuevas copias cada vez). Mientras que esto es más eficiente, vale la pena anotar que cada vez que Usted crea una cadena de caracteres enmarcada en comillas como “abc” y “def” , el compilador las convierte en objetos String. Por ende puede hacer más objetos creados de los que Usted espera, a pesar de la eficiencia aportada por StringBuffer. Las clases String y StringBuffer A continuación un vistazo general de los métodos disponibles tanto para String como para StringBuffer, con el fin de que pueda identificar la manera como interactúan. Estas tablas no contienen todos y cada uno de los métodos disponibles sino aquellos que son importantes para la discusión. Los métodos que están sobrecargados están resumidos en una sola línea. Primero, la clase String: Método Argumentos, Sobrecarga Uso Constructor Sobrecarga: por defecto, String , StringBuffer, arreglos char, arreglos byte . Creación de objetos String. length( ) Número de caracteres en el String. charAt( ) int Indice El caracter en una localización dentro del String . getChars( ), getBytes( ) El principio y final desde donde copiasr, el arreglo dentro del cual copiar, un índice al interior del arreglo destino Copia de chars o bytes hacia un arreglo externo. toCharArray( ) Genera un char[] que contiene los caracteres en el String. equals( ), equals - Una String con la IgnoreCase( ) cual comparar Una verificacion de igualidad sobre los contenidos de las dos Strings . Método Argumentos, Sobrecarga Uso compareTo( ) Una String con la cual comparar. El resultado es negativo, cero o positivo dependiendo del orden lexicográfico del String y del argumento. Las letras mayúsculas y minúsculas no son iguales ! regionMatches( ) Desplazamiento dentro de esta String, la otra String y su desplazamiento y longitud de comparación. La sobrecarga añade “ignore case.” El resultado booleano indica si las regiones coinciden. startsWith( ) String con la que podría empezar. La sobrecarga añade desplazamiento dentro del argumento. El resultado booleano indica si la String empieza con el argumento. endsWith( ) String que puede ser un sufijo de esta String El resultado booleano indica si el argumento es un sufijo. indexOf( ), lastIndexOf( ) Sobrecargado: char, char e índice de inicio, String, String, e índice de inicio. Retorna -1 si no se encuentra el argumento dentro de esta String, de otra manera retorna el índice donde empieza el argumento. lastIndexOf( ) busca hacia atrás empezando en el final. Método Argumentos, Sobrecarga Uso substring( ) Sobrecargado: índice de inicio, índice de inicio e índice de final. Retorna un nuevo objeto String que contiene el conjunto de caracteres especificado. concat( ) La String a concatenar. Retorna un nuevo objeto String que contiene los caracteres del String original seguidos por lo s caracteres en el argumento. replace( ) El viejo caracter a buscar y el nuevo con el que se reemplazará Retorna un nuevo objeto String con los reemplazos hechos. Si no se encuentra concordancia alguna, usa el viejo objeto String. toLowerCase( ) toUpperC ase( ) Retorna un nuevo objeto String con todas las letras cambiadas a mayúsculas o a minúsculas. Si no es necesario hacer cambio alguno, usa el viejo objeto String. trim( ) Retorna un nuevo objeto String con los espacios en blanco removidos del inicio y del final. Si no es necesario hacer cambio alguno, usa el viejo objeto String. Método Argumentos, Sobrecarga Uso valueOf( ) Sobrecargado: Object, char[], char[] y desplazamiento y conteo, boolean, char, int, long, float, double. Retorna un String conteniendo la representación en caracteres del argumento. intern( ) Produce una y sola una referencia String por cada secuencia de caracteres única. Como puede ver, cada método en String cuidadosamente retorna un nuevo objeto String cuando es necesario cambiar los contenidos originales. Note también que si esto no es necesario, el método retornará únicamente una referencia a la String original. Esto ahorra espacio de almacenamiento y sobrecarga de trabajo. Ahora, la clase StringBuffer: Método Argumentos, sobrecarga Uso Constructor Sobrecargado: por defecto, longitud del buffer a crear, String desde la cual crear. Crear un nuevo objeto StringBuffer. toString( ) Crear un String a partir de este StringBuffer. length( ) Número de caracteres en el StringBuffer. capacity( ) Retorna el número actual de espacios asignados. ensureCapacity( ) Entero que indica la capacidad deseada Hace que el StringBuffer tenga por lo menos el número deseado de espacios. setLength( ) Entero que indica la nueva longitud de la Trunca o expande la cadena de cadena de caracteres en caracters previa. Si el buffer. expande, completa el tamaño con nulls. charAt( ) Entero que indica la localización del elemento deseado. Retorna el char en esa localización en el buffer. setCharAt( ) Entero que indica la localización del elemento deseado y el nuevo valor char para el elemento. Modifica el valor en esa localización. getChars( ) El inicio y final de donde se va a a copiar, el arreglo donde se va a copiar, un índice dentro del arreglo de destino. Copiar chars en un arreglo externo. No hay getBytes( ) como en String . append( ) Sobrecargado: Object, String, char[], char[] con desplazamiento y longitud, boolean, char, int, long, float, double . El argumento es convertido en una cadena y añadido el final del buffer actual, aumentando este si es necesario insert( ) Sobrecargado, cada uno con un primer argumento del desplazamiento a partir del cual comenzar a insertar: Object, String, char[], boolean, char, int, long, float, double . El segundo argumento se convierte en una cadena y se inserta en el buffer actual comenzando en el desplazamiento. Si se requiere, se aumenta el buffer. reverse( ) Se invierte el orden de los caracteres en el buffer. El método más comúnmente usado es append( ), el cual es utilizado por el compilador al evaluar expresiones de tipo String que contengan los operadores ‘+’ and ‘+=’. El método insert( ) tiene un formato similar y ambos llevan a cabo manipulaciones significativas al buffer en vez de crear nuevos objetos. Las cadenas son especiales A este momento Usted ya ha visto que la clase String no es solo una clase más en Java. Hay muchos casos especiales en String, no siendo el menos importante el que sea una clase original y fundamental para Java. Luego está el hecho de que una cadena de caracteres enmarcada en comillas es convertida a un objeto String por el compilador y por los operadores especiales sobrecargados ‘+’ and ‘+=’. En este apéndice Usted ha conocido los restantes casos especiales: la cuidadosamente construida inmutabilidad usando la clase compañera StringBuffer y alguna magia extra en el compilador. Resúmen Debido a que en Java todos los identificadores de objetos son referencias y a que cada objeto es creado sobre la marcha y recolectado como basura solo cuando ya no es usado más, la manera de manipular objetos cambia, especialmente al pasarlos y retornarlos. Por ejemplo, en C o C++, si Usted quiere inicializar un trozo de almacenamiento en un método, probablemente solicitaría que el usuario pase al método la dirección de ese trozo de almacenamiento. De otra manera, Usted tendría que preocuparse acerca de quién sería responsable de destruir ese almacenamiento. Así, la interfaz y el entendimiento de tales métodos es más complicado. Pero en Java, Usted nunca tiene que preocuparse sobre la responsabilidad o sí un objeto todavía existirá cuando sea necesitado. Esto ya ha sido resuelto para Usted. Usted puede crear un objeto en el momento en que se necesita (y no antes) y nunca preocuparse sobre la mecánica de pasar la responsabilidad por él; Usted simplemente pasa la referencia. Algunas veces la simplificación que esto da pasa desapercibida. En otras ocasiones, es asombrosa. Las desventajas de esta magia subyacente son dos: 1. Siempre tiene que aceptar la desmejora en la eficiencia debido a la administración extra de memoria (aunque la desmejora puede ser bastante pequeña), y siempre hay una ligera cantidad de incertidumbre sobre el tiempo que algo puede requerir para correr (ya que el colector de basura puede ser forzado a actuar cuando quiera que Usted esté bajo de memoria). En la mayoría de las aplicaciones, los beneficies superan las desventajas y las tecnologías de mejoramiento en particular han acelerado las cosas hasta el punto de que este asunto ya no es importante. 2. Aliasing: algunas veces Usted puede terminar con dos referencias al mismo objeto, lo cual es un problema solo si las dos referencias se supone que están apuntando a objetos distintos. Aquí es cuando Usted debe prestar mayor atención y, si es necesario, clone( ) o de otra forma duplicar un objeto para prevenir que la otra referencia se vea sorprendida por un cambio inesperado. Alternativamente, Usted puede apoyar al aliasing por asuntos de eficiencia al crear objetos inmutables cuyas operaciones pueden retornar un nuevo objeto del mismo u otro tipo diferente, pero nunca cambiar el objeto original de tal manera que cualquiera que esté aliased a ese objeto no vea cambio alguno. Algunas personas dicen que la clonació n en Java es un diseño chapucero que no debería ser usado, así que implementan su propia versión de clonación121 y nunca llaman el método Object.clone( ), eliminando así la necesidad de implementar Cloneable y de capturar la excepción CloneNotSupportedException. Esto es ciertamente una estrategia razonable, y ya que clone( ) es soportado tan raras veces en la librería estándar de Java, aparentemente es también una estrategia segura. Ejercicios Las soluciones a ejercicios seleccionados se pueden encontrar en el documento electrónico The Thinking in Java Annotated Solution Guide, disponible por una módica tarifa en www.BruceEckel.com. 1. Demostrar un segundo nivel de aliasing. Crear un método que tome una referencia a un objeto pero que no modifique al objeto de esa referencia. Sin embargo, el método debe llamar a un segundo método pasando a éste la referencia, y este segundo método debe modificar el objeto. 2. Crear una clase MyString que contenga un objeto String el que Usted inicializa en el constructor usando el argumento del constructor. Añada un método toString( ) y un método concatenate( ) que adiciones un objeto String a su cadena interna. Implemente clone( ) en MyString. Cree dos métodos static donde cada uno tome una referencia MyString x como argumento y llame a x.concatenate("test") , pero en el segundo método llame primero a clone( ) . Ensaye los dos métodos y muestre los diferentes resultados. 3. Cree una clase llamada Battery conteniendo un int el cual es un número de batería (como un identificador único). Hagala clonable y déle un método toString( ) . Ahora cree una clase llamada Toy que contenga un arreglo de Battery y un toString( ) que imprima todas las baterías. Escriba un clone( ) para Toy que clone automáticamente todos sus objetos Battery . Pruebe esto clonando a Toy e imprimiento el resultado. 4. Cambie CheckCloneable.java de tal forma que todos los métodos clone( ) capturen la excepción CloneNotSupportedException en vez de pasarla al llamador. 5. Usando la técnica de la clase-acompañante -mutable, haga una clase inmutable que contenga un int, un double , y un arreglo de char. [5] Doug Lea, quien fue de ayuda resolviendo este asunto, me lo sugirió, diciendo que el simplemente crea en cada clase una función llamada duplicate( ) . 6. Modifique Compete.java para añadir más objetos miembro a las clases Thing2 y Thing4 y vea si puede determinar cómo cambian los tiempos con la complejidad—si es una simple relación lineal o si parece más complicada. 7. Comenzando con Snake.java, cree una versión de copia profunda de la serpiente. 8. Implemente la interfaz Collection en una clase llamada CloningCollection usando un private ArrayList para proveer la funcionalidad del contenedor. Anule el método clone( ) de tal forma que CloningCollection lleve a cabo una “copia profunda condicional”; intenta hacer clone( ) a todos los elementos que contiene, pero si no puede, deja la(s) referencia(s) aliased.