Bienvenido a los Consejos Técnicos de la Conexión del Desarrollador Java (JDC), del 4 de Septiembre del 2001. Esta edición cubre:
- Hacer Copias Defensivas de Objetos.
- Usar Iterators.
Estos consejos fueron desarrollados usando Java(tm) 2 SDK, Standard Edition v 1.3
Se puede ver esta edición (en su original en inglés) de los Consejos Técnicos en formato html en http://java.sun.com/jdc/JDCTechTips/2001/tt0904.html
Hacer Copias Defensivas de Objetos
Supongamos que estamos haciendo alguna programación gráfica usando la clase Rectangle del AWT. En nuestro programa un objeto Rectangle es pasado a un constructor de una clase. El constructor chequea la anchura y altura del Rectangle para asegurarse que son valores positivos. Si la anchura y la altura son positivas, entonces el volumen del rectángulo es (anchura * altura) está garantizado que sea positivo. En el programa, también podemos realizar otros pasos para asegurarnos de que los objetos que referencia nuestra clase son Rectangles válidos. Por ejemplo, hacemos la clase final. De esta forma, ninguna subclase puede invalidar el chequeo de error que hacemos en el programa.
El código se parecerá a esto:
import java.awt.Rectangle; final class TestRect { private final Rectangle rect; public TestRect(Rectangle r) { // check for width/height at least 1 if (r.width < 1 || r.height < 1) { throw new IllegalArgumentException(); } rect = r; } public int getVolume() { return rect.width * rect.height; } } public class CopyDemo1 { public static void main(String args[]) { // create a Rectangle and a TestRect Rectangle r = new Rectangle(0, 0, 5, 10); TestRect t = new TestRect(r); // set width to an invalid value and // compute volume //r.width = -59; System.out.println("Volume = " + t.getVolume()); } }
¿Es este código correcto? ¿Puede de alguna forma un mal objeto Rectangle pasar los chequeos y ser referenciado desde dentro del código TestRect? La respeuesta es SI. Podemos quitar el comentario de la línea del programa CopyDemo1 que dice "r.width = -59", y luego ejecutar el programa, el resultado será :
Volume = -590
que probablemente no es lo que teníamos en mente. El problema es que cuando pasamos un objeto como argumento a un método, realmente estamos pasando una referencia (puntero) al objeto. Esto significa que un sólo objeto puede ser compartido en varias partes del programa.
¿Cómo podemos corregir el problema ilustrado arriba? Una forma es hacer una copia del Rectangle pasado a TestRect:
import java.awt.Rectangle; final class TestRect { private final Rectangle rect; public TestRect(Rectangle r) { // use copy constructor to copy Rectangle // object rect = new Rectangle(r); // check for valid width/height if (rect.width < 1 || rect.height < 1) { throw new IllegalArgumentException(); } } public int getVolume() { return rect.width * rect.height; } } public class CopyDemo2 { public static void main(String args[]) { // create Rectangle and TestRect objects Rectangle r = new Rectangle(0, 0, 5, 10); TestRect t = new TestRect(r); // set width to an invalid value r.width = -59; // compute volume System.out.println("Volume = " + t.getVolume()); } }
En esta aproximación, hacemos una copia usando el constructor copia de Rectangle, el constructor que toma como argumento un Rectangle y crea un nuevo objeto con los mismos valores. Hacemos la copia antes del chequeo de validación. El chequeo de validación se hace sobre la copia para evitar problemas en los programas multi-thread. Uno de estos problemas podría ser que otro thread cambie el argumento pasado después de que haya sido comprobado pero antes de que haya sido copiado.
Si ejecutamos el programa CopyDemo2, el resultado es:
Volume = 50
Como hemos hecho una copia del argumento dentro del constructor, cambiar la anchura a -59 fuera del constructor no tiene efecto sobre el resultado. Observa que hacer las copias de esta forma tiene algún coste en velocidad y espacio, especialmente si copiamos grandes objetos.
Otra forma de resolver este problema aparentemente es usar un método clone(). Pero esta aproximación no siempre funciona, como ilustra el siguiente ejemplo:
import java.awt.Rectangle; final class TestRect { private final Rectangle rect; public TestRect(Rectangle r) { // clone the Rectangle object rect = (Rectangle)r.clone(); // check for valid width/height if (rect.width < 1 || rect.height < 1) { throw new IllegalArgumentException(); } } public int getVolume() { return rect.width * rect.height; } } // subclass of Rectangle with bogus clone() method class MyRectangle extends Rectangle { public MyRectangle(int x, int y, int w, int h) { super(x, y, w, h); } public Object clone() { return this; } } public class CopyDemo3 { public static void main(String args[]) { // create MyRectangle and TestRect objects Rectangle r = new MyRectangle(0, 0, 5, 10); TestRect t = new TestRect(r); // set width to an invalid value r.width = -59; // compute volume System.out.println("Volume = " + t.getVolume()); } }
Rectangle no es una clase final, por eso puede ser subclasificada. En este ejemplo, la clase MyRectangle se define con un método clone degenerado, uno que simplemente devuelve una referencia al objeto que está siendo clonado. Un método clone como este podría ser malicioso o simplemente estar mal escrito. En cualquier caso, no obtenemos el resultado deseado. La salida del prorama CopyDemo3 es la misma que la del programa CopyDemo1:
Volume = -590
El problema de los accesos compartidos a objetos puede suceder en otros contextos. Supongamos que añadimos un método accesor a TestRect que devuelve una referencia al Rectangle almacenado dentro del objeto. Un usuario puede cambiar el objeto de una forma arbitraria, y las asumpciones que hacemos sobre la validación del objeto no se mantendrán más.
Otra forma de corregir el problema es pasar los valores de la anchura y la altura como enteros, y no pasarlos en una referencia al objeto Rectangle. En general, podemos corregir el problema haciendo que las clases sean inmutables, es decir, que los objetos de las clases no puedan modificarse después de su creación. Pero como Rectangle es mutable, necesitamos otra forma de resolver el problema.
Otra situación donde podríamos querer crear una copia defensiva se ilustra en el siguiente programa:
class TestNames { public static final String names[] = {"red", "green", "blue"}; } public class CopyDemo4 { public static void main(String args[]) { TestNames.names[0] = "purple"; System.out.println(TestNames.names[0]); } }
El hecho de que el array names sea final significa que no podemos asignarlo. Pero aún así podemos cambiar el valor de una posición particular del array. Cuando ejecutemos este programa, el resultado será:
purple
En otras palabras, efectivamente hemos sobreescrito lo que se suponía que era un array de sólo-lectura.
¿Cómo resolvemos este problema? Una forma es copiar el array usando clone, otra es crear una vista de sólo-lectura del array usando el Collections Framework. Aquí tenemos una ilustración que muestra ambas aproximaciones:
import java.util.*; final class TestNames { private static final String names[] = {"red", "green", "blue"}; // return a copy of the names array public static String[] getNames() { //return names; return (String[])names.clone(); } // return a read-only List view of the array public static List getNamesList() { return Collections.unmodifiableList( Arrays.asList(names)); } } public class CopyDemo5 { public static void main(String args[]) { // attempt to modify names[0] and then // print its value TestNames.getNames()[0] = "purple"; System.out.println(TestNames.getNames()[0]); // get names array as a read-only list and // print the value in the first slot List list = TestNames.getNamesList(); System.out.println(list.get(0)); // attempt to modify the read-only list //list.set(0, "purple"); } }
Observa que este programa hace el array names como private, y define un método accesor para él, getNames(). Sin embargo, haciendo simplemente esto no se resuelve el problema. Todavía podemos modificar la primera posición del array. Por eso el programa clona el array. Después de clonarlo, si cambiamos la primera posición del array, sólo afecta a la copia. Por supuesto, clonar un array grande tiene un coste añadido en espacio y velocidad.
Observa también los mecanismos de Collections Framework usados en el programa. Arrays.asList() crea una lista copiada por un array. Collections.unmodifiableList entonces crea una vista de sólo-lectura de la lista. En este caso no se copia el array, pero en su lugar se construye una lista sobre él.
Esta soluciones implican una copia superficial o ninguna copia del array subyacente. Si el array contiene objetos mutables, entonces todavía tenemos un problema porque un codigo de usuario erróneo puede cambiar el valor de un objeto individual dentro del array. En el programa CopyDemo5 de arriba, se usa un array de strings, y los objetos String son inmutables.
La necesidad de la copia defensiva también se muestra en otros contextos, como después de que un objeto haya sido deserializado, o cuando se añade un objeto a un Set o un Map. Por ejemplo, una de las propiedades de un Set, es que no contiene elementos duplicados. ¿Qué pasa si se añade un objeto mutable a un Set, y luego cambiamos el objeto para que equals() sea true entre ese objeto y otro objeto del Set? En este momento el Set estará corrupto. Para evitar este problema, se podría haber realizado una copia defensiva.
Para más información sobre la realización de copias defensivas de obejtos, puedes ver el ítem 24 "Make defensive copies when needed" en "Effective Java Programming Language Guide" de Joshua Bloch (http://java.sun.com/docs/books/effective/).
Usar Iterators
Un iterator es un mecanismo usado para pasar atavés de los elementos de una estructura de datos. El interface java.util.Iterator es parte del Collections Framework, y especifica tres ,métodos:
- boolean hasNext();
devuelve true si hay más elementos. - Object next();
Devuelve el siguiente elemento. - void remove();
borra el último elemento devuelto.
Este interface reemplaza a la vieja java.util.Enumeration. También hay un interface ListIterator que extiende Iterator, y proporciona facilidades para atravesar listas, tanto hacia adelante como hacia atrás, y modificar la lista durante la iteración.
¿Por qué querríamos usar un iterator? Veamos un sencillo ejemplo:
import java.util.*; public class IterDemo1 { public static void main(String args[]) { List list = new ArrayList(); list.add("test1"); list.add("test2"); list.add("test3"); //for (int i = 1; i <= list.size(); i++) { // System.out.println(list.get(i)); //} Iterator iter = list.iterator(); while (iter.hasNext()) { System.out.println(iter.next()); } } }
En este ejemplo, hay una lista que contiene tres strings, y queremos imprimir el valor de cada string. Podríamos hacerlo utilizando las propiedades de acceso-aleatorio de la lista. En este caso, llamamos a get() para devolver elementos arbitrarios. Si quitamos los comentarios del código de IterDemo1 que hace esto y ejecutamos el programa, obtendremos los siguientes resultados:
test2 test3 Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3
Esto meustra que el programa se saltó el primer sring, y que intenta acceder al índice 3 de la lista cuyos índices válidos están en el rango 0-2. Por eso obtenemos un error "off-by-one".
Este ejemplo trivial ilustra una razón por la que los iterators son importantes -- manejan los detalles de la estructura atravesada. No tenemos que recodar, por ejemplo, que una lista empieza en el índice 0 y no en el 1. Hay incluso estructuras más complejas e importantes como los trees, donde la forma correcta de atravesar la estructura no simpre es obvia.
Otra ventaja de los iteratos es que proporcionan un interface a través de diferentes tipos de datos. Por ejemplo, aquí hay un pequeño programa que muestra los elementos de cualquier tipo de collection o de map:
import java.util.*; public class IterDemo2 { static void dumpElements(Object o) { // if have a Map, pick up the set of // key/value pairs if (o instanceof Map) { o = ((Map)o).entrySet(); } // dump elements of collection if (o instanceof Collection) { Collection c = (Collection)o; Iterator iter = c.iterator(); while (iter.hasNext()) { System.out.println(iter.next()); } } // give error if not a Collection else { System.err.println( "Not a valid collection"); } } public static void main(String args[]) { // Lists List list = new ArrayList(); list.add("test1"); list.add("test2"); dumpElements(list); // Sets Set set = new TreeSet(); set.add("test3"); set.add("test4"); dumpElements(set); // Maps Map map = new HashMap(); map.put("test5key", "test5value"); map.put("test6key", "test6value"); dumpElements(map); } }
List y Set extienden el interface Collection. Sin embargo, Map no lo hace, pero es posible obtener la "entry set" de un Map, y luego iterar sobre ella. Cuando ejecutamos el programa IterDemo2, la salida será:
test1 test2 test3 test4 test6key=test6value test5key=test5value
¿Cómo podemos escribir nuestros propios iterators? Veamos un ejemplo, uno que define un iterator para un BitSet:
import java.util.*; class BitSetIterator implements Iterator { // underlying BitSet private final BitSet bitset; // current index into BitSet private int index; // constructor public BitSetIterator(BitSet bitset) { this.bitset = bitset; } // return true if has more elements public boolean hasNext() { return index < bitset.length(); } // return next element public Object next() { if (index >= bitset.length()) { throw new NoSuchElementException(); } boolean b = bitset.get(index++); return new Boolean(b); } // remove element public void remove() { throw new UnsupportedOperationException(); } } public class IterDemo3 { public static void main(String args[]) { // create BitSet and set some bits BitSet bitset = new BitSet(); bitset.set(1); bitset.set(19); bitset.set(47); // go through and display True/False values Iterator iter = new BitSetIterator(bitset); while (iter.hasNext()) { Boolean b = (Boolean)iter.next(); String tf = ( b.booleanValue() ? "T" : "F"); System.out.print(tf); } System.out.println(); } }
Un objeto BitSetIterator sigue la pista de la referencia al objeto BitSet subyacente, junto con un índice dentro del conjunto de bits. El programa IterDemo3 usa BitSet.length() para obtener el número de bit má alto que está en el set. Entonces el programa itera hasta el indice que indica ese valor. Cada elemento es devuelto como objeto Boolean. Observa que es legal llamar repetidamente a hasNext() sin llamar a next(). También es legal llamar a next() sin llamar a hasNext(). y next() tiene que manejar la situación cuando no hay más elementos adicionales que devolver.
Cuando ejecutamos este programa, la salida es:
FTFFFFFFFFFFFFFFFFFTFFFFFFFFFFFFFFFFFFFFFFFFFFFT
¿Qué sucede si la estructura de datos subyacente sobre la que está trabajando el iterator cambia durante la iteracción? Veamos un ejemplo sencillo:
import java.util.*; class BitSetIterator implements Iterator { private final BitSet bitset; private int index; public BitSetIterator(BitSet bitset) { this.bitset = bitset; } public boolean hasNext() { return index < bitset.length(); } public Object next() { if (index >= bitset.length()) { throw new NoSuchElementException(); } boolean b = bitset.get(index++); return new Boolean(b); } public void remove() { throw new UnsupportedOperationException(); } } public class IterDemo4 { public static void main(String args[]) { // create Bitset and set bit 0 BitSet bitset = new BitSet(); bitset.set(0); // set up iterator Iterator iter = new BitSetIterator(bitset); //BitSet clone = (BitSet)bitset.clone(); //Iterator iter = new BitSetIterator(clone); // display True/False bits while (iter.hasNext()) { bitset.clear(0); Boolean b = (Boolean)iter.next(); String tf = ( b.booleanValue() ? "T" : "F"); System.out.print(tf); } System.out.println(); } }
El programa IterDemo4 crea un BitSet, seleciona un bit, configura un iterator, y llama a hasNext(). Luego, antes de llamar a next(), el programa borra el bit que fue seleccionado. En este punto, no hay bits seleccionados en el BitSet. Por eso, cuando se llama a next(), el resultado es una NoSuchElementException.
Una forma de evitar este tipo de problemas es copiar la estructura de datos, y luego crear el iterator sobre la copia. En el ejemplo IterDemo4, hay otra alternativa usando clone() para copiar el BitSet (El código para esta aproximación está comentado). Recuerda del Truco 1 que puede ser peligroso llamar a clone si tenemos una subclase con un método clone malicioso. Pero en el ejemplo IterDemo4, hay un objeto BitSet real , no un objeto de una subclase.
Aquí tenemos otro ejemeplo de modificación de una lista mientras iteramos sobre ella:
import java.util.*; public class IterDemo5 { public static void main(String args[]) { List list = new ArrayList(); list.add("test1"); list.add("test2"); list.add("test3"); list.add("test4"); Iterator iter = list.iterator(); for (int index = 0; iter.hasNext(); index++) { String next = (String)iter.next(); if (next.equals("test2")) { list.remove(index); //iter.remove(); } } } }
Si ejecutamos este programa, obtendremos una ConcurrentModificationException. Esta es causada por un intento de eliminar un elemento de la lista, mientras se itera sobre ella. Las implementaciones de list e iterator trabajan juntas para detectar este problema. Sin embargo, podemos eliminar con seguridad el elemento llamando al método remove() definido en la implementación de iterator. Este tipo de iterator tiene el nombre "fail-fast".
Un ejemplo final nos muestra cómo podemos usar un iterator para filtrar la salida de otro iterator:
import java.util.*; class FilterNumbers implements Iterator { // underlying iterator private final Iterator iter; // current Number object private Object nextnum; public FilterNumbers(Iterator iter) { this.iter = iter; } public boolean hasNext() { // if already have a Number object, return // true if (nextnum != null) { return true; } // scan for a Number object while (iter.hasNext()) { nextnum = iter.next(); if (nextnum instanceof Number) { return true; } } // didn't find one nextnum = null; return false; } public Object next() { // either already have a Number object, // or must scan for one via hasNext() if (nextnum == null && !hasNext()) { throw new NoSuchElementException(); } // return Number object Object savenum = nextnum; nextnum = null; return savenum; } public void remove() { throw new UnsupportedOperationException(); } } public class IterDemo6 { public static void main(String args[]) { // create a list List list = new ArrayList(); // add various types of elements to list list.add(null); list.add("test1"); list.add(new Integer(37)); list.add(new Double(12.34)); list.add(null); list.add("test2"); list.add(new Long(12345)); list.add(null); list.add(new Byte((byte)125)); // filter and display the Number objects Iterator iter = new FilterNumbers( list.iterator()); while (iter.hasNext()) { System.out.println(iter.next()); } } }
En el programa IterDemo6, hay un iterator de alguna clase, y queremos añadir otro iterator sobre él. El iterator que queremos añadir busca todos los objetos de clases que implementen el interface Number, como Byte y Double.
El iterator filtro escanea y devuelve todos los objetos Number al llamador. Cuando ejecutamos este programa, la salida es:
37 12.34 12345 125
Para más información sobre el uso de iterators, puedes ver la sección 16.2 "Iteration", y la sección 16.10, "Writing Iterator Implementations", en "The Java(tm) Programming Language Third Edition" de Arnold, Gosling, and Holmes http://java.sun.com/docs/books/javaprog/thirdedition/
Copyright y notas de la traducción
Nota respecto a la traducción
El original en inglés de la presente edición de los JDC Tech Tips fue escrita por Glen McCluskey, la traducción no oficial fue hecha por Juan A. Palos (Ozito), cualquier sugerencia o corrección hágala al correo [email protected] , sugerencia respecto a la edición original a mailto:[email protected]
Nota (Respecto a la edición via email)
Sun respeta su tiempo y su privacidad. La lista de correo de la Conexión del desarrollador Java se usa sólo para propósitos internos de Sun Microsystems(tm). Usted ha recibido este email porque se ha suscrito a la lista. Para desuscribirse vaya a la página de suscripciones, desmarque casilla apropiada y haga clic en el botón Update.
Suscripciones
Para suscribirse a la lista de correo de noticias de la JDC vaya a la página de suscripciones, elija los boletines a los que quiera suscribirse, y haga clic en Update.
Realimentación
¿Comentarios?, envie su sugerencias a los Consejos Técnicos de la JDC a mailto:[email protected]
Archivos
Usted encontrará las ediciones de los Consejos Técnicos de la JDC (en su original en inglés) en http://java.sun.com/jdc/TechTips/index.html
Copyright
Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.
Este documento esta protegido por las leyes de autor. Para mayor información vea http://java.sun.com/jdc/copyright.html
Enlaces a sitios fuera de Sun
Los Consejos Técnicos de la JDC puede dar, p terceras pueden dar, enlaces a otros sitios y recursos. ya que Sun no tiene control sobre esos sitios o recursos usted reconoce y acepta que Sun no es responsable por la disponibilidad de tales sitios o recursos, y no se responsabiliza por cualquier contenido, anuncios , productos u otros materiales disponibles en tales sitios o recursos. Sun no será responsable, directa o indirectamente, por cualquier daño o pérdida causada o supuestamente causada por o en relación con el uso de o seguridad sobre cualquier tal contenido, bienes o servicios disponibles en o através de cualquier sitio o recurso.
El original en Ingles de esta edición de los Consejos técnicos fue escrita por Glen McCluskey.
JDC Tech Tips September 04, 2001
Sun, Sun Microsystems, Java y Java Developer Connection (JDC) son marcas registradas de Sun Microsystems Incs. en los Estados Unidos y cualquier otro país.