JDC Tech Tips (4 de Septiembre del 2001)

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP