100% encontró este documento útil (1 voto)
545 vistas

Collections de Java

El documento introduce las colecciones en Java, explicando primero conceptos generales sobre estructuras de datos y algoritmos. Luego describe las interfaces principales de colecciones en Java, comenzando por Collection, la cual permite iterar y conocer el tamaño de una colección. También presenta List, Set, Map y Queue, que extienden Collection y agregan funcionalidad. Finalmente, brinda ejemplos de uso de iteradores e iteración simplificada desde Java 6.

Cargado por

api-3723951
Derechos de autor
© Attribution Non-Commercial (BY-NC)
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
100% encontró este documento útil (1 voto)
545 vistas

Collections de Java

El documento introduce las colecciones en Java, explicando primero conceptos generales sobre estructuras de datos y algoritmos. Luego describe las interfaces principales de colecciones en Java, comenzando por Collection, la cual permite iterar y conocer el tamaño de una colección. También presenta List, Set, Map y Queue, que extienden Collection y agregan funcionalidad. Finalmente, brinda ejemplos de uso de iteradores e iteración simplificada desde Java 6.

Cargado por

api-3723951
Derechos de autor
© Attribution Non-Commercial (BY-NC)
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 9

Collections de Java

Introducción
Un tema que aparece frecuentemente en la programación es el de organizar una
colección de objetos en memoria. Este tema también es conocido con el título
"estructuras de datos", y se relaciona también a distintos algoritmos para agilizar el
acceso. Antes de hablar específicamente de las colecciones de Java voy a desviarme un
rato hablando un poco sobre el problema en general de almacenar colecciones de datos
en memoria.

Algoritmos y estructuras de datos


Todo programador aprende, apenas empieza, un par de estructuras de datos simples.
Podríamos decir que la primer estructura de datos es la variable. Almacena un solo
objeto, y el tiempo de acceso es ínfimo. La segunda estructura de datos que siempre se
aprende es el arreglo (en inglés "array"). Esta colección nos permite obtener un valor
dado una posición en una tabla. Es muy veloz ya que esta estructura tiene un fuerte
paralelo con cómo se organiza internamente la memoria de una computadora. Los
valores en un arreglo están en orden, y en ese mismo orden están dispuestos en la
memoria, por lo tanto el acceso lo maneja directamente el hardware (porque la memoria
funciona físicamente como un arreglo).

Un arreglo en Java se hace así (pero asumamos que ya se sabe).

String[] nombres = new String[10];

Muchos programadores se detienen aquí. Ayuda a esto el hecho de que en algunos


lenguajes el arreglo era la estructura de datos más compleja soportada (por ejemplo en
BASIC... ¡o incluso C!).

Uno puede usar arreglos para todo. ¿Quiero guardar la lista de helados que me gusta?
¿Las notas que se sacaron los alumnos de una clase? ¿La colección de coordenadas que
forman un mapa? Puede guardarse en un arreglo... ahora... supongamos que quiero
guardar las notas de los alumnos de una clase. Si quiero usar un arreglo podría actuar
así: Creo una clase con el nombre del alumno y su nota:

class Nota{
String alumno;
int nota;

Nota(String alumno, int nota) {


this.alumno = alumno;
this.nota = nota;
}

public int getNota() {


return nota;
}
public String getAlumno() {
return alumno;
}
}

Ahora almaceno las notas de 500 alumnos:

Nota[] notas = new Nota[500];

notas[0] = new Nota("Juan", 7);


notas[1] = new Nota("Pedro", 8);
notas[2] = new Nota("Manuel", 4);
notas[3] = new Nota("David", 6);

// ...

notas[499] = new Nota("Elías", 9);

Ahora... ¿Qué pasa si queremos averiguar la nota de Elías? No sabemos "a priori" en
qué posición del arreglo está guardada la nota. La única manera que tenemos de
proceder es simplemente recorrer el arreglo y preguntar si ya llegamos al alumno que
queremos:

for(Nota n : notas)
if(n.getNombre().equals("Elías"))
return n.getNota();

¡Esto es lentísimo! Está bien, "Elías" es el peor caso, ya que se encuentra último. A
Elías lo encontraremos después de haber efectuado 499 comprobaciones. Pero si
suponemos que todos los alumnos son accedidos más o menos con una misma
probabilidad tenemos que el número de comprobaciones promedio que se necesitarán
por búsqueda de alumno es de 250!

¿Podemos hacerlo mejor? ¿Qué tal si pudiéramos contar con que el orden de las notas
coincide con el orden alfabético del nombre de los alumnos? En ese caso podríamos ir
al alumno 250, y preguntar: ¿Elías es mayor o menor alfabéticamente que vos? Si el
alumno 250 es Luis, Elías claramente está antes qué él. Luego, Elías está entre el 0 y el
249. Entonces vamos a comprobar al alumno 125, que es, por ejemplo, "Carlos". Elías
está después que Carlos, entonces sabemos que su posición es mayor a 125 (y era menor
a 249). Así vamos acotando hasta llegar a Elías en, a lo sumo, 9 comprobaciones. A este
algoritmo se le suele llamar "búsqueda dicotómica".

No hace falta entender del todo el párrafo anterior. El quid de la cuestión es entender
que, mediante algoritmos, redujimos la necesidad de comprobar 250 veces en promedio,
a comprobar a lo sumo nueve veces. Y si en vez de 500 nombres tenemos 100.000
necesitaremos comprobar, a lo sumo, 17 veces. El uso de este algoritmo depende del
hecho de que los datos que ordenamos están ordenados.

Asimismo cada una de las soluciones puede traer problemas secundarios que serán más
o menos importantes de acuerdo a nuestras necesidades. En el ejemplo anterior, por
ejemplo, necesitamos mantener un arreglo ordenado... ¿qué pasa si se agrega un nuevo
alumno? Habrá que insertarlo en la posición correcta de acuerdo al orden alfabético y
desplazar a todos lo que queden a su derecha un lugar. En el peor caso es recorrer todo
el arreglo!
Todo esto quiere decir que el tipo de estructura de datos que se use dependerá de lo que
puede asumir de la colección almacenada y de cómo se espera que esa colección sea
usada (cómo sea leída y cómo sea modificada). Algunas preguntas a hacerse son:

• ¿Me interesa mantener el orden abitrario original? ¿Puedo (como con las notas)
reordenar los ítems?
• ¿Hay duplicados?
• ¿Con qué frecuencia es necesario añadir nuevos elementos?

Colecciones en Java
Java tiene desde la versión 1.2 todo un juego de clases e interfaces para guardar
colecciones de objetos. En él, todas las entidades conceptuales están representadas por
interfaces, y las clases se usan para proveer implementaciones de esas interfaces. Una
introducción conceptual debe entonces enfocarse primero en esas interfaces.

La interfaz nos dice qué podemos hacer con un objeto. Un objeto que cumple
determinada interfaz es “algo con lo que puedo hacer X”. La interfaz no es la
descripción entera del objeto, solamente un mínimo de funcionalidad con la que debe
cumplir.

Como corresponde a un lenguaje tan orientado a objetos, estas clases e interfaces están
estructuradas en una jerarquía. A medida que se va descendiendo a niveles más
específicos aumentan los requerimientos y lo que se le pide a ese objeto que sepa hacer.

Collection

La interfaz más importante es Collection. Una Collection es todo aquello que se puede
recorrer (o “iterar”) y de lo que se puede saber el tamaño. Muchas otras clases
extenderán Collection imponiendo más restricciones y dando más funcionalidades. Es
de notar que el requisito de "que se sepa el tamaño" hace inconveniente utilizar estas
clases con colecciones de objetos de las que no se sepa “a priori” la cantidad (ésto
podría considerarse una limitación de este framework).

Por las dudas vuelvo a aclarar: No puedo construir una Collection. No se puede hacer
“new” de una Collection, sino que todas las clases que realmente manejan colecciones
“son” Collection, y admiten sus operaciones.

Las operaciones básicas de una collection entonces son:

add(T)
Añade un elemento.
iterator()
Obtiene un “iterador” que permite recorrer la colección visitando cada elemento
una vez.
size()
Obtiene la cantidad de elementos que esta colección almacena.
contains(t)
Pregunta si el elemento t ya está dentro de la colección.
iterator()
Obtiene un “iterador” para recorrer la colección.

Si yo tengo un objeto Collection hay muchas cosas que no puedo asumir. No puedo
asumir que el orden en el que lo recorro sea relevante, es decir, que si lo recorro de
nuevo vea los elementos en el mismo orden en el que los vi la primera vez. Tampoco
puedo asumir que no hay duplicados. No puedo asumir características de rendimiento:
Preguntar si existe un objeto en la colección puede tardar desde muy poco hasta...
mucho =).

Una capacidad de un objeto Collection es la de poder ser recorrido. Como a este nivel
no está definido un orden, la única manera es proveyendo un iterador, mediante el
método iterator(). Un iterador es un objeto “paseador” que nos permite ir obteniendo
todos los objetos al ir invocando progresivamente su método next(). También, si la
colección es modificable, podemos remover un objeto durante el recorrido mediante el
método remove() del iterador. El siguiente ejemplo recorre una colección de Integer
borrando todos los ceros:

void borrarCeros(Collection<Integer> ceros)


{
Iterator<Integer> it = ceros.iterator();
while(it.hasNext())
{
int i = it.next();
if(i == 0)
it.remove();
}
}

En este ejemplo hago uso de la conversión automática entre Integer e int.

A partir de Java 6 hay una manera simplificada de recorrer una collection (que sirve si
no necesitamos borrar elementos). Se hace mediante una nuevo uso del keyword for:

void mostrar(Collection<?> col)


{
for(Object o : col)
System.out.println(o);
}

Internamente este código no hace otra cosa que obtener el iterador, pero queda mucho
más elegante y legible de esta manera. Pero al mismo tiempo, el non disponer del
iterador explícitamente hace imposible usar esta sintaxis para hacer lo que hice en el
ejemplo previo: ir borrando elementos.

Hay cuatro interfaces que extienden Collection: List, Set, Map y Queue. Cada una de
ellas agrega condiciones sobre los datos que almacena y como contrapartida ofrece más
funcionalidad. A continuación cuento de qué se trata cada una de ellas.
List

Un List, o simplemente lista, es una Collection. La diferencia que tiene una lista con
una Collection es que la lista mantiene un orden arbitrario de los elementos y permite
acceder a los elementos por orden. Podríamos decir que en una lista, por lo general, el
orden es dato. Es decir, el orden es información importante que la lista también nos está
almacenando.

No hay ningún método en Collection para obtener el tercer elemento. No lo puede haber
porque, como se dijo, a nivel Collection ni siquiera estamos seguros de que si volvemos
a recorrer la colección los elementos aparecerán en el mismo orden. Una lista sí debe
permitir acceder al tercer elemento, por eso se añaden los siguientes métodos:

get(int i)
Obtiene el elemento en la posición i.
set(int i, T t)
Pone al elemento t en la posición i.

Existen en Java principalmente dos implementaciones de List, las dos útiles en distintos
casos. Una de ellas es casi igual a los arreglos comunes (como el de notas que usaba
más arriba para ejemplificar). Esta implementación es ArrayList. La ventaja de
ArrayList por sobre un array común es que es expansible, es decir que crece a medida
que se le añaden elementos (mienras que el tamaño de un array es fijo desde su
creación). El tiempo de acceso a un elemento en particular es ínfimo, pero si queremos
eliminar un elemento del principio, o del medio, la clase debe mover todos los que le
siguen a la posición anterior, para “tapar” el agujero que deja el elemento removido.
Esto hace que sacar elementos del medio o del principio sea costoso.

La otra implementación es LinkedList (lista enlazada). En ésta, los elementos son


mantenidos en una serie de nodos atados entre sí como eslabones de una cadena. Cada
uno de estos nodos apunta a su antecesor y al elemento que le sigue. Nada más. No hay
nada en cada uno de esos nodos que tenga algo que ver con la posición en la lista. Para
obtener el elemento número “n”, esta implemetación de List necesita entonces empezar
desde el comienzo, desde el primer nodo, e ir avanzando al “siguiente” n veces. Buscar
el elemento 400 entonces implica 400 de esos pasitos. La ventaja es que es posible
eliminar elementos del principio de la lista y del medio de manera muy eficiente. Para
elminar un elemento solamente hay que modificar a sus dos “vecinos” para que se
“conecten” entre sí ignorando al elemento que se está borrando. Como en una cadena, se
retira un eslabón abriéndo los eslabones adyacentes al que se eliminá y cerrándolos de
modo que lo excluyan. No es necesario hacerle ningún cambio al resto de los elementos
de la lista.

En otros lenguajes lidiar con listas enlazadas puede ser un poco más trabajoso. En Java,
LinkedList se usa exactamente igual que otros tipos de List, por lo que no hay que saber
nada adicional para empezar a usarla. Bueno, esto no es del todo cierto... hay que tener
muy en claro sus particularidades en cuanto a rendimiento. Su método get(int) es
particularmente lento porque, como dije, necesita recorrer para llegar al elemento
pedido. Esto hace que recorrer la ista con un simple for(int i = 0 ; i <
lista.size(); i++) sea tremendamente lento! La complejidad pasa de ser lineal a
cuadrática, es decir: Si se recorre así una lista de 300 elementos, se tarda como si tuviera
44.850 elementos! Una LinkedList sólo debe recorrerse mediante iteradores.

Un uso ideal de LinkedList es para la creación de “colas”, en las que los elementos se
añaden al final, y se eliminal del comienzo. Para este uso se puede usar, en vez de List,
la interfaz Queue (también implementada por LinkedList) que es más específica para
esta tarea.

Set

Un Set es una Collection. Set es la palabra inglesa para “conjunto” y los desarrolladores
de Java estaban pensando en lo que matemáticamente se conoce como conjunto. Por
sobre lo que es una collection, un set agrega una sola restricción: No puede haber
duplicados.

Por lo general en un set el orden no es dato. Si bien es posible que existan sets que nos
aseguren un orden determinado cuando los recorremos (por ejemplo obtener strings en
orden alfabético), ese orden no es arbitrario y decidido por nosotros, ya que la interfaz
Set no tienen ninguna funcionalidad para manipularlo (como si lo admite la interfaz
List).

La ventaja de utilizar Sets es que preguntar si un elemento ya está contenido mediante


“contains()” suele ser muy eficiente. Entonces es conveniente utilizarlos cada vez que
necesitemos una colección en la que no importe el orden, pero que necesitemos
preguntar si un elemento está o no.

Como, a diferencia de Collection, el orden no necesariamente es preservado, no existen


métodos para “obtener el primer elemento”.

HashSet

Existen varias implementaciones de Set. La más comunmente usada es HashSet.

Los Sets (y los Maps) aprovechan cierta cosa característica de Java: Todos los objetos
heredan de Object, por lo tanto todos los métodos de la clase Object están presentes en
todos los objetos. Hay ciertas cosas que todo objeto en Java sabe hacer. Dos de estas
cosas son:

• Saber si es igual a otro, con su método equals().


• Devolver un número entero de modo tal que si dos objetos son iguales ese
número también lo será (se conoce esto como un hash). Esto todo objeto lo sabe
hacer con su método hashCode().

La clase HashSet aprovecha la segunda de las funciones. A cada objeto que se añade a
la colección se le pide que calcule su “hash”. Este valor será un número entre -
2147483647 y 2147483648. Basado en ese valor se lo guarda en una tabla. Más tarde,
cuando se pregunta con contains() si un objeto x ya está, habrá que saber si está en esa
tabla. ¿En qué posición de la tabla está? HashSet puede saberlo, ya que para un objeto
determinado, el hash siempre va a tener el mismo valor. Entonces la función contains de
HashSet saca el hash del objeto que le pasan y va con eso a la tabla. En la posición de la
tabla hay una lista de objetos que tienen ese valor de hash, y si uno de esos es el
buscado contains devuelve true.

Un efecto de este algoritmo es que el orden en el que aparecen los objetos al recorrer el
set es impredecible. También es importante darse cuenta de que es crítico que la función
hashCode() tiene que devolver siempre el mismo valor para los objetos que se
consideran iguales (o sea que equals() da true). Si esto no es así, HashSet pondrá al
objeto en una posición distinta en la tabla que la que más adelante consultará cuando se
llame a contains, y entonces contains dará siempre falso, por más que se haya hecho
correctamente el add. Esto mismo puede suceder si se usan como claves objetos que
varíen.

TreeSet
Antes de entrar en la descripción de TreeSet vaya una breve explicación. Otra cosa que
pueden saber hacer los objetos con independencia de cómo y dónde son usados es saber
ordenarse. A diferencia de “equals” y “hashCode”, que están en todos los objetos, la
capacidad de “ordernarse” está sólo en aquellos que implementan la interfaz
Comparable. Cuando un objeto implementa esta interfaz promete saber compararse con
otros (con el método compareTo()), y responder con este método si él está antes,
después o es igual al objeto que se le pasa como parámetro. Al orden resultante de usar
este método se le llama en Java “orden natural”. Muchas de las clases de Java
implementan Comparable, por ejemplo String lo hace, definiendo un orden natural de
los strings que es el obvio, el alfabético. También implementan esta interfaz Date,
Number, etc. y los “órdenes naturales” que definen estas implementaciones también los
los que uno esperaría.

Si yo creo una clase mía llamada Alumno, queda a mi cargo, si así lo quiero, la
definición de un orden natural para los alumnos. Puedo elegir usar el apellido, el
nombre, el número de matrícula, etc. De acuerdo al atributo que elija para definir el
orden natural codificaré el método compareTo(). Lo que es importante es que la
definición de este método sea compatible con el equals(); esto es que a.equals(b) si
y sólo si a.compareTo(b) == 0.

TreeSet usa una técnica completamente diferente a la explicada para HashSet.


Construye un árbol con los objetos que se van agregando al conjunto. Un árbol es una
forma en computación de tener un conjunto de cosas todo el tiempo en orden, y permitir
que se agreguen más cosas y que el orden se mantenfga. Al tener todo en orden TreeSet
puede fácilmente saber si un objeto está, por medio de una técnica similar a la explicada
en el comienzo de este artículo para buscar los nombres de las notas.

Una ventaja de TreeSet es que el orden en el que aparecen los elementos al recorrerlos
es el orden natural de ellos (los objetos deberán implementar Comparable, como lo
explico arriba; si no lo hacen se deberá especificar una función de comparación
manualmente). Una desventaja es que mantener todo ordenado tiene un costo, y esta
clase es un poquito menos eficiente que HashSet.
Map

Un Map representa lo que en otros lenguajes se conoce como “diccionario” y que se


suele asociar a la idea de “tabla hash” (aunque no se implemente necesariamente con
esa técnica). Un Map es un conjunto de valores, con el detalle de que cada uno de estos
valores tiene un objeto extra asociado. A los primeros se los llama “claves” o “keys”, ya
que nos permiten acceder a los segundos.

Cuando digo que las claves forman un conjunto, lo digo en el sentido Java: Son un
“Set”, no puede haber duplicados. En otros lenguajes existen estructuras parecidas que
admiten la existencia de claves duplicadas (a veces conocidos como “multimap”). La
única manera de lograr esto en Java es haciendo que el map guarde listas de valores en
vez de los valores sueltos.

Un Map no es una Collection ya que esa interfaz le queda demasiado chica. Podríamos
decir que Collection es unidimensional, mientras que un Map es bidimensional. No hay
una manera trivial de expresar un Map en una simple serie de objetos que podemos
recorrer. Sí podríamos recorrer una serie de objetos si cada uno de ellos representase un
par {clave, valor} (y de hecho eso se puede hacer). Pero esta forma de recorrer un Map
no es la forma primaria en que se usa.

Algunos de los métodos más importantes de un Map son:

get(Object clave)
Obtiene el valor correspondiente a una clave. Devuelve null si no existe esa
clave en el map.
put(K clave, V valor)
Añade un par clave-valor al map. Si ya había un valor para esa clave se lo
reemplaza.

Además hay algunas funciones que son útiles a la hora de recorrer eficientemente el
contenido de un Map:

keySet()
Todas las claves (devuelve un Set, es decir, sin duplicados).
values()
Todos los valores (los valores sí pueden estar duplicados, por lo tanto esta
función devuelve una Collection).
entrySet()
Todos los pares clave-valor (devuelve un conjunto de objetos Map.Entry, cada
uno delos cuales devuelve la clave y el valor con los métodos getKey() y
getValue() respectivamente).

Queue y Deque

Se conoce como cola a una colección especialmente diseñada para ser usada como
almacenamiento temporario de objetos a procesar. Las operaciones que suelen admitir
las colas son “encolar”, “obtener siguiente”, etc. Por lo general las colas siguen un
patrón que en computación se conoce como FIFO (por la sigla en inglés de “First In -
First Out” - “lo que entra primero, sale primero”), lo que no quiere decir otra cosa que
lo obvio: El orden en que se van obteniendo los “siguientes” objetos coincide con el
orden en que fueron introducidos en la cola. Esto análogo a su tocaya del supermercado:
La gente que hace la cola va siendo atendida en el orden en que llegó a ésta.

Hasta hace poco, para implementar una cola FIFO en Java la única opción provista por
la biblioteca de colecciones era LinkedList. Como ya se dijo más arriba, esta
implementación ofrece una implementación eficiente de las operaciones “poner
primero” y “sacar último”. Sin embargo, aunque la implentación es la correcta, a nivel
de interfaz dejaba algo que desear. Los métodos necesarios para usar una LinkedList
como una cola eran parte solamente de la clase LinkedList, no existía ninguna interfaz
que abstraiga el concepto de “cola”. Esto hacía imposible crear código genérico que use
indistintamente diferentes implementaciones de colas (que por ejemplo no sean FIFO
sino que tengan algún mecanismo de prioridades). Esta situación cambió recientemente
a partir del agregado a Java de dos interfaces expresamente diseñadas para el manejo de
colas: La interfaz Queue tiene las operaciones que se esperan en una cola. También se
creó Deque, que representa a una “double-ended queue”, es decir, una cola en la que los
elementos pueden añadirse no solo al final, sino también “empujarse” al principio.

Jerarquía de las clases del framework

Tomado de
http://www.reloco.com.ar/prog/java/collections.html
Autor: Nicolás Lichtmaier.

También podría gustarte