Download A - Pasando y retornando Objetos

Document related concepts
no text concepts found
Transcript
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.