Tutorial de Java

Nuevas Colecciones

Anterior | Siguiente
Para el autor, las colecciones son una de las herramientas más poderosas que se pueden poner en manos de un programador. Por ello, las colecciones que incorporaba Java, adolecían de precariedad y de demasiada rapidez en su desarrollo. Por todo ello, para quien escribe esto ha sido una tremenda satisfacción comprobar las nuevas colecciones que incorpora el JDK 1.2, y ver que incluso las antiguas han sido rediseñadas. Probablemente, las colecciones, junto con la librería Swing, son las dos cosas más importantes que aporta la versión 1.2 del JDK, y ayudarán enormemente a llevar a Java a la primera línea de los lenguajes de programación.

Hay cambios de diseño que hacen su uso más simple. Por ejemplo, muchos nombres son más cortos, más claros y más fáciles de entender; e incluso algunos de ellos han sido cambiados totalmente para adaptarse a la terminología habitual.

El rediseño también incluye la funcionalidad, pudiendo encontrar ahora listas enlazadas y colas. El diseño de una librería de colecciones es complicado y difícil. En C++, la Standard Template Library (STL) cubría la base con muchas clases diferentes. Desde luego, esto es mejor que cuando no hay nada, pero es difícil de trasladar a Java porque el resultado llevaría a tal cantidad de clases que podría ser muy confuso. En el otro extremo, hay librerías de colecciones que constan de una sola clase, Collection, que actúa como un Vector y una Hashtable al mismo tiempo. Los diseñadores de las nuevas colecciones han intentado mantener un difícil equilibrio: por un lado disponer de toda la funcionalidad que el programador espera de una buena librería de colecciones, y, por otro, que sea tan fácil de aprender y utilizar como la STL y otras librerías similares. El resultado puede parecer un poco extraño en ocasiones, pero, al contrario que en las librerías anteriores al JDK 1.2, no son decisiones accidentales, sino que están tomadas a conciencia en función de la complejidad. Es posible que se tarde un poco en sentirse cómodo con algunos de los aspectos de la librería, pero de seguro que el programador intentará adoptar rápidamente estos nuevos métodos. Hay que reconocer que Joshua Bloch de Sun, ha hecho un magnífico trabajo en el rediseño de esta librería.

La nueva librería de colecciones parte de la premisa de almacenar objetos, y diferencia dos conceptos en base a ello:

  • Colección (Collection): un grupo de elementos individuales, siempre con alguna regla que se les puede aplicar. Una List almacenará objetos en una secuencia determinada y, un Set no permitirá elementos duplicados.
  • Mapa (Map): un grupo de parejas de objetos clave-valor, como la Hastable ya vista. En principio podría parecer que esto es una Collection de parejas, pero cuando se intenta implementar, este diseño se vuelve confuso, por lo que resulta mucho más claro tomarlo como un concepto separado. Además, es conveniente consultar porciones de un Map creando una Collection que represente a esa porción; de este modo, un Map puede devolver un Set de sus claves, una List de sus valores, o una List de sus parejas clave-valor. Los Mapas, al igual que los arrays, se pueden expandir fácilmente en múltiples dimensiones sin la incorporación de nuevos conceptos: simplemente se monta un Map cuyos valores son Mapas, que a su vez pueden estar constituidos por Mapas, etc.

Las Colecciones y los Mapas pueden ser implementados de muy diversas formas, en función de las necesidades concretas de programación, por lo que puede resultar útil el siguiente diagrama de herencia de las nuevas colecciones que utiliza la notación gráfica propugnada por la metodología OMT (Object Modeling Technique).

El diagrama está hecho a partir de la versión beta del JDK 1.2, así que puede haber cosas cambiadas con respecto a la versión final. Quizás también, un primer vistazo puede abrumar al lector, pero a lo largo de la sección se comprobará que es bastante simple, porque solamente hay tres colecciones: Map, List y Set; y solamente dos o tres implementaciones de cada una de ellas. Las cajas punteadas representan interfaces y las sólidas representan clases normales, excepto aquellas en que el texto interior comienza por Abstract, que representan clases abstractas. Las flechas indican que una clase puede generar objetos de la clase a la que apunta; por ejemplo, cualquier Collection puede producir un Iterator, mientras que una List puede producir un ListIterator (al igual que un Iterator normal, ya que List hereda de Collection).

Los interfaces que tienen que ver con el almacenamiento de datos son: Collection, Set, List y Map. Normalmente, un programador creará casi todo su código para entenderse con estos interfaces y solamente necesitará indicar específicamente el tipo de datos que se están usando en el momento de la creación. Por ejemplo, una Lista se puede crear de la siguiente forma:

    List lista = new LinkedList();

Desde luego, también se puede decidir que lista sea una lista enlazada, en vez de una lista genérica, y precisar más el tipo de información de la lista. Lo bueno, y la intención, del uso de interfaces es que si ahora se decide cambiar la implementación de la lista, solamente es necesario cambiar el punto de creación, por ejemplo:

    List lista = new ArrayList();

el resto del código permanece invariable.

En la jerarquía de clases, se pueden ver algunas clases abstractas que pueden confundir en un principio. Son simplemente herramientas que implementan parcialmente un interfaz. Si el programador quiere hacer su propio Set, por ejemplo, no tendría que empezar con el interfaz Set e implementar todos los métodos, sino que podría derivar directamente de AbstractSet y ya el trabajo para crear la nueva clase es mínimo. Sin embargo, la nueva librería de colecciones contiene suficiente funcionalidad para satisfacer casi cualquier necesidad, así que en este Tutorial se ignorarán las clases abstractas.

Por lo tanto, a la hora de sacar provecho del diagrama es suficiente con lo que respecta a los interfaces y a las clases concretas. Lo normal será construir un objeto correspondiente a una clase concreta, moldearlo al correspondiente interfaz y ya usas ese interfaz en el resto del código. El ejemplo java420.java es muy simple y consiste en una colección de objetos String que se imprimen.

import java.util.*;
        
public class java420 {
    public static void main( String args[] ) {
        Collection c = new ArrayList();
        for( int i=0; i < 10; i++ )
            c.add( Integer.toString( i ) );
        
        Iterator it = c.iterator();
        while( it.hasNext() )
            System.out.println( it.next() );
        }
    } 

Como las nuevas colecciones forman parte del paquete java.util, no es necesario importar ningún paquete adicional para utilizarlas.

A continuación se comentan los trozos interesantes del código del ejemplo. La primera línea del método main() crea un objeto ArrayList y lo moldea a una Collection. Como este ejemplo solamente utiliza métodos de Collection, cualquier objeto de una clase derivada de Collection debería funcionar, pero se ha cogido un ArrayList porque es el caballo de batalla de las colecciones y viene a tomar el relevo al Vector.

El método add(), como su nombre sugiere, coloca un nuevo elemento en la colección. Sin embargo, la documentación indica claramente que add() "asegura que la colección contiene el elemento indicado". Esto es para que un Set tenga significado, ya que solamente añadirá el elemento si no se encuentra en la colección. Con un ArrayList, o cualquier otra lista ordenada, add() significa siempre "colocarlo dentro".

Todas las colecciones pueden producir un Iterator invocando al método iterator(). Un Iterator viene a ser equivalente a una Enumeration, a la cual reemplaza, excepto en los siguientes puntos:

  1. Utiliza un nombre que está históricamente aceptado y es conocido en toda la literatura de programación orientada a objetos
  2. Utiliza nombres de métodos más cortos que la Enumeration: hasNext() en vez de hasMoreElements(), o next() en lugar de nextElement()
  3. Añade un nuevo método, remove(), que permite eliminar el último elemento producido por el Iterator. Solamente se puede llamar a remove() una vez por cada llamada a next()

En el ejemplo se utiliza un Iterator para desplazarse por la colección e ir imprimiendo cada uno de sus elementos.

Colecciones

A continuación se indican los métodos que están disponibles para las colecciones, es decir, lo que se puede hacer con un Set o una List, aunque las listas tengan funcionalidad añadida que ya se verá, y Map no hereda de Collection, así que se tratará aparte.

boolean add( Object )

Asegura que la colección contiene el argumento. Devuelve false si no se puede añadir el argumento a la colección

boolean addAll( Collection )

Añade todos los elementos que se pasan en el argumento. Devuelve true si es capaz de incorporar a la colección cualquiera de los elementos del argumento

void clear()

Elimina todos los elementos que componen la colección

boolean contains( Object )

Verdadero si la colección contiene el argumento que se pasa como parámetro

boolean isEmpty()

Verdadero si la colección está vacía, no contiene elemento alguno

Iterator iterator()

Devuelve un Iterator que se puede utilizar para desplazamientos a través de los elementos que componen la colección

boolean remove( Object )

Si el argumento está en la colección, se elimina una instancia de ese elemento y se devuelve true si se ha conseguido

boolean removeAll( Collection )

Elimina todos los elementos que están contenidos en el argumento. Devuelve true si consigue eliminar cualquiera de ellos

boolean retainAll( Collection )

Mantiene solamente los elementos que están contenidos en el argumento, es lo que sería una intersección en la teoría de conjuntos. Devuelve verdadero en caso de que se produzca algún cambio

int size()

Devuelve el número de elementos que componen la colección

Object[] toArray()

Devuelve un array conteniendo todos los elementos que forman parte de la colección. Este es un método opcional, lo cual significa que no está implementado para una Collection determinada. Si no puede devolver el array, lanzará una excepción de tipo UnsupportedOperationException

El siguiente ejemplo, java421.java, muestra un ejemplo de todos estos métodos. De nuevo, recordar que esto funcionaría con cualquier cosa que derive de Collection, y que se utiliza un ArrayList para mantener un común denominador solamente.

El primer método proporciona una forma se rellenar la colección con datos de prueba, en esta caso enteros convertidos a cadenas. El segundo método será utilizado con bastante frecuencia a partir de ahora.

Las dos versiones de nuevaColeccion() crean ArrayList conteniendo diferente conjunto de datos que devuelven como objetos Collection, está claro que no se utiliza ningún otro interfaz diferente de Collection.

El método print() también se usará a menudo a partir de ahora, y lo que hace es moverse a través de la Colección utilizando un Iterator, que cualquier Collection puede generar, y funciona con Listas, Conjuntos y Mapas.

El método main() se usa simplemente para llamar a los métodos de la Colección.

Listas

Hay varias implementaciones de List, siendo ArrayList la que debería ser la elección por defecto, en caso de no tener que utilizar las características que proporcionan las demás implementaciones.

List (interfaz)

La ordenación es la característica más importante de una Lista, asegurando que los elementos siempre se mantendrán en una secuencia concreta. La Lista incorpora una serie de métodos a la Colección que permiten la inserción y borrar de elementos en medio de la Lista. Además, en la Lista Enlazada se puede generar un ListIterator para moverse a través de las lista en ambas direcciones.

ArrayList

Es una Lista volcada en un Array. Se debe utilizar en lugar de Vector como almacenamiento de objetos de propósito general. Permite un acceso aleatorio muy rápido a los elementos, pero realiza con bastante lentitud las operaciones de insertado y borrado de elementos en medio de la Lista. Se puede utilizar un ListIterator para moverse hacia atrás y hacia delante en la Lista, pero no para insertar y eliminar elementos.

LinkedList

Proporciona un óptimo acceso secuencial, permitiendo inserciones y borrado de elementos de en medio de la Lista muy rápidas. Sin embargo es bastante lento el acceso aleatorio, en comparación con la ArrayList. Dispone además de los métodos addLast(), getFirst(), getLast(), removeFirst() y removeLast(), que no están definidos en ningún interfaz o clase base y que permiten utilizar la Lista Enlazada como una Pila, una Cola o una Cola Doble.

En el ejemplo java422.java, cubren gran parte de las acciones que se realizan en las Listas, como moverse con un Iterator, cambiar elementos, ver los efectos de la manipulación de la Lista y realizar operaciones sólo permitidas a las Listas Enlazadas.

En testBasico() y moverIter() las llamadas se hacen simplemente para mostrar la sintaxis correcta, y aunque se recoge el valor devuelto, no se usa para nada. En otros casos, el valor devuelto no es capturado, porque no se utiliza normalmente. No obstante, el lector debe recurrir a la documentación de las clases para comprobar el uso de cualquier método antes de utilizarlo.

Sets

Set tiene exactamente el mismo interfaz que Collection, y no hay ninguna funcionalidad extra, como en el caso de las Listas. Un Set es exactamente una Colección, pero tiene utilizada en un entorno determinado, que es ideal para el uso de la herencia o el polimorfismo. Un Set sólo permite que exista una instancia de cada objeto.

A continuación se muestran las diferentes implementaciones de Set, debiendo utilizarse HashSet en general, a no ser que se necesiten las características proporcionadas por alguna de las otras implementaciones.

Set (interfaz)

Cada elemento que se añada a un Set debe ser único, ya que el otro caso no se añadirá porque el Set no permite almacenar elementos duplicados. Los elementos incorporados al Conjunto deben tener definido el método equals(), en aras de establecer comparaciones para eliminar duplicados. Set tiene el mismo interfaz que Collection, y no garantiza el orden en que se encuentren almacenados los objetos que contenga.

HashSet

Es la elección más habitual, excepto en Sets que sean muy pequeños. Debe tener definido el método hashCode().

ArraySet

Un Set encajonado en un Array. Esto es útil para Sets muy pequeños, especialmente aquellos que son creados y destruidos con frecuencia. Para estos pequeños Sets, la creación e iteración consume muchos menos recursos que en el caso del HashSet. Sin embargo, el rendimiento es muy malo en el caso de Sets con gran cantidad de elementos.

TreeSet

Es un Set ordenado, almacenado en un árbol balanceado. En este caso es muy fácil extraer una secuencia ordenada a partir de un Set de este tipo.

El ejemplo java423.java, no muestra todo lo que se puede hacer con un Set, sino que como Set es una Collection, y las posibilidades de las Colecciones ya se han visto, pues el ejemplo se limita a mostrar aquellas cosas que son particulares de los Sets.

import java.util.*;
    
public class java423 {
    public static void testVisual( Set a ) {
        java421.fill( a );
        java421.fill( a );
        java421.fill( a );
        java421.print( a ); // No permite Duplicados!
    
        // Se añade otro Set al anterior
        a.addAll( a );
        a.add( "uno" ); 
        a.add( "uno" ); 
        a.add( "uno" );
        java421.print( a );
    
        // Buscamos ese elemento
        System.out.println( "a.contains(\"uno\"): "+a.contains( "uno" ) );
    }
    
    public static void main( String args[] ) {
        testVisual( new HashSet() );
        testVisual( new ArraySet() );
        }
    }

Aunque se añaden valores duplicados al Set, a la hora de imprimirlos, se puede observar que solamente se acepta una instancia de cada valor. Cuando se ejecuta el ejemplo, se observa también que el orden que mantiene el HashSet es diferente del que presenta el ArraySet, ya que cada uno tiene una forma de almacenar los elementos para la recuperación posterior. El ArraySet mantiene los elementos ordenados, mientras que el HashSet utiliza sus propias funciones para que las búsquedas sean muy rápidas. Cuando el lector cree sus propios tipos de estructuras de datos, deberá prestar atención porque un Set necesita alguna forma de poder mantener el orden de los elementos que lo integran, como se muestra en el ejemplo siguiente, java424.java.

Las definiciones de los métodos equals() y hashCode() son semejantes a las de ejemplos anteriores. Se debe definir equals() en ambos casos, mientras que hashCode() solamente es necesario si la clase corresponde a un HashSet, que debería ser la primera elección a la hora de implementar un Set.

Mapas

Los Mapas almacenan información en base a parejas de valores, formados por una clave y el valor que corresponde a esa clave.

Map (interfaz)

Mantiene las asociaciones de pares clave-valor, de forma que se puede encontrar cualquier valor a partir de la clave correspondiente.

HashMap

Es una implementación basada en una tabla hash. Proporciona un rendimiento muy constante a la hora de insertar y localizar cualquier pareja de valores; aunque este rendimiento se puede ajustar a través de los constructores que permite fijar la capacidad y el factor de carga de la tabla hash.

ArrayMap

Es un Mapa circunscrito en un Array. Proporciona un control muy preciso sobre el orden de iteración. Está diseñado para su utilización con Mapas muy pequeños, especialmente con aquellos que se crean y destruyen muy frecuentemente. En este caso de Mapas muy pequeños, la creación e iteración consume muy pocos recursos del sistema, y muchos menos que el HashMap. El rendimiento cae estrepitosamente cuando se intentan manejar Mapas grandes.

TreeMap

Es una implementación basada en un árbol balanceado. Cuando se observan las claves o los valores, se comprueba que están colocados en un orden concreto, determinado por Comparable o Comparator, que ya se verán. Lo importante de un TreeMap es que se pueden recuperar los elementos en un determinado orden. TreeMap es el único mapa que define el método subMap(), que permite recuperar una parte del árbol solamente.

El ejemplo java425.java contiene dos grupos de datos de prueba y un método rellena() que permite llenar cualquier mapa con cualquier array de dos dimensiones de Objects.

Los métodos printClaves(), printValores() y print() no son solamente unas cuantas utilidades, sino que demuestran como se pueden generar Colecciones que son vistas de un Mapa. El método keySet() genera un Set que contiene las claves que componen el Mapa; en este caso, es tratado como una Colección. Tratamiento similar se da a values(), que genera una List conteniendo todos los valores que se encuentran almacenados en el Mapa. Observar que las claves deben ser únicas, mientras que los valores pueden contener elementos duplicados. Debido a que las Colecciones son dependientes del Mapa, al representar solamente una vista concreta de parte de los datos del Mapa, cualquier cambio en una Colección se reflejará inmediatamente en el Mapa asociado.

El método print() recoge el Iterator producido por entries() y lo utiliza para imprimir las parejas de elementos clave-valor. El resto del ejemplo proporciona ejemplos muy simples de cada una de las operaciones permitidas en un Mapa y prueba cada tipo de Mapa.

A la hora de crear Mapas propios, el lector debe tener en cuenta las mismas recomendaciones que anteriormente se proporcionaban en el caso de los Sets.

Navegador

Home | Anterior | Siguiente | Indice | Correo