Programación 3
Universidad de Alicante, 2024–2025
Guía fácil para usar el Java Collection Framework
Java Collection Framework es como se conoce a la librería de
clases contenedoras de Java que podemos encontrar en el paquete estándar
java.util
. Estas clases sirven para almacenar colecciones
de objetos, como listas, conjuntos, mapas, …
Todas estas clases permiten guardar en ellas referencias a objetos. Por ejemplo:
// Lista de enteros. Puede haber enteros repetidos en la lista:
List<Integer> lista_de_manzanas;
// Conjunto de enteros. No puede haber enteros repetidos:
Set<Integer> conjunto_de_naranjas;
// Un mapa que asocia a una cadena un entero, como en una lista de notas de un examen:
// [("Juan Goytisolo", 9.5), ("Pablo Iglesias", 5.0), ...]
Map<String, Integer> mapa_de_notas;
Gracias al boxing/unboxing podemos usarlas también con tipos primitivos (int, double, etc). Sin embargo, debemos definirlos usando las clases que representan estos tipos primitivos (Integer, Double, etc):
List<Integer> lista_de_enteros; // aquí podemos guardar y recuperar directamente valores tipo 'int'.
Vamos a ver como funcionan estas colecciones de objetos. Puedes encontrar la mayor parte del código de está página en el fichero GuiaFacilJCF.java
Listas
Llamamos lista a cualquier colección de objetos ordenados por posición, como en un array. En una lista podemos añadir elementos, acceder a ellos por su posición en la lista, eliminar elementos de la lista y otras operaciones, como vaciar la lista, copiarla, etc. En una lista puede haber objetos repetidos, es decir, objetos que son iguales según el método equals() de su clase.
Crear una lista
Vamos a crear una lista de objetos de tipo Integer:
List<Integer> lista_de_enteros = new ArrayList<Integer>();
Porqué usamos ‘new ArrayList’ y no ‘new List’ quedará claro cuando hablemos de polimorfismo e interfaces.
Desde la versión 1.7 de Java se puede obviar el tipo de elemento en la lista en la instrucción new:
List<Integer> lista_de_enteros = new ArrayList<>();
El compilador se encarga de realizar lo que se llama inferencia
de tipos, deduciendo del tipo en tiempo de compilación de
lista_de_enteros cuál es el tipo de sus elementos y por tanto
qué tipo debe ir entre <>
(a <> se le
conoce como el operador diamante en la jerga de Java).
Añadir elementos a la lista
El método add(·)
añade una referencia a un objeto al
final de la lista
.add(new Integer(4));
lista_de_enteros.add(new Integer(5));
lista_de_enteros.add(new Integer(7));
lista_de_enteros.add(2,new Integer(6)); // lo añade en la posición 2, entre el 5 y el 7
lista_de_enteros.add(4); // añado el 4 otra vez, usando 'boxing'
lista_de_enteros// esta última línea equivale a: lista_de_enteros.add(new Integer(4));
Tamaño de una lista
Usa size()
para saber el tamaño de una lista:
int tam = lista_de_enteros.size();
devuelve 5: la lista contiene 5 elementos.
Obtener elementos de una lista
El método get(int)
sirve para acceder a cualquier
elemento de la lista, por su posición:
Integer primero = lista_de_enteros.get(0);
int primer = lista_de_enteros.get(0); // 'unboxing', obteniendo directamente un 'int'.
devuelve el objeto en la posición 0, es decir, el primero de los que añadí.
Integer ultimo = lista_de_enteros.get(lista_de_enteros.size()-1);
me devuelve el último elemento de la lista.
Saber si un elemento está en la lista
Usa el método contains(·)
para preguntarle a la lista si
contiene el objeto dado:
.contains(new Integer(7)); // me devolverá 'true'
lista_de_enteros.contains(new Integer(8)); // me devolverá 'false' lista_de_enteros
Obtener la posición de un objeto en la lista
El método indexOf(·)
me indica la posición de la primera
ocurrencia del objeto en la lista (recuerda que un mismo objeto puede
estar repetido en diferentes posiciones):
.indexOf(new Integer(6)); // devuelve 2
lista_de_enteros.indexOf(new Integer(10)); // devuelve -1 lista_de_enteros
Recorrer los elementos de la lista
Podemos usar un bucle convencional:
for (int i=0; i < lista_de_enteros.size(); i++) {
System.out.println(lista_de_enteros.get(i));
}
el cual nos permitiría recorrer sólo una parte de la lista si nos interesa (p. ej., los cuatro primeros elementos).
Con un bucle for
como éste:
for (Integer entero : lista_de_enteros) {
System.out.println(entero);
}
donde la variable ‘entero’ de tipo Integer (también puede ser de tipo ‘int’ en este caso) va tomando el valor del siguiente elemento de la lista ‘lista_de_enteros’ en cada iteración. Con este tipo de bucle recorremos TODOS los elementos de la lista.
Una forma algo más sofisticada, pero más flexible de hacer esto es usar iteradores:
Iterator<Integer> iterador = lista_de_enteros.iterator();
while (iterador.hasNext()) {
Integer entero = iterador.next();
System.out.println(entero); // imprime un elemento
}
Fíjate que en el ‘while’ podríamos añadir más condiciones para detener el bucle donde nos interese, lo cual no podemos hacer con el ‘for’ anterior.
Eliminar un objeto de la lista
Para esto usamos el método remove(·)
de dos formas:
boolean quitado = lista_de_enteros.remove(new Integer(7)); // o remove(7)
quita la primera aparicíon del 7 en la lista y devuelve ‘true’. Si no hay ningún 7 devolvería ‘false’.
Integer unEntero = lista_de_enteros.remove(1);
quita el segundo elemento de la lista y lo devuelve.
NOTA: Podrías pensar que en esta segunda llamada a remove()
sobre un List
Saber si una lista está vacía o vaciarla
Esto es fácil:
boolean estaVacia = lista_de_enteros.isEmpty(); // devolverá 'false'
.clear(); // ahora sí que está vacía lista_de_enteros
Conjuntos
Llamamos conjunto a cualquier colección de objetos de la misma clase sin ningún orden en particular. Además, cada elemento sólo aparece una vez, al contrario que en una lista, donde podían repetirse.
Crear un conjunto
Vamos a crear un conjunto de objetos de tipo Integer:
Set<Integer> conjunto_de_enteros = new HashSet<Integer>();
Porqué ‘new HashSet’ y no ‘new Set’ quedará claro cuando hablemos de polimorfismo e interfaces.
Añadir elementos al conjunto
El método add(·)
añade una referencia a un objeto al
conjunto.
.add(new Integer(4));
conjunto_de_enteros.add(new Integer(5));
conjunto_de_enteros.add(new Integer(7));
conjunto_de_enterosboolean repe = conjunto_de_enteros.add(new Integer(4));
// no añade el 4 porque ya está en el conjunto y además devuelve 'false'
(con todas las colecciones del JCF podemos usar el boxing/unboxing, así que no lo comentaremos más.)
Tamaño de un conjunto
.size(); conjunto_de_enteros
me devuelve 3: el conjunto contiene 3 elementos.
Saber si un elemento está en la lista
.contains(new Integer(7)); // me devolverá 'true'
conjunto_de_enteros.contains(new Integer(8)); // me devolverá 'false' conjunto_de_enteros
Un Set
no tiene los métodos get(·)
e
indexOf(·)
, los elementos no están en ninguna posición en
particular. Básicamente, con un conjunto lo que podemos hacer es añadir
elementos, eliminarlos y preguntar si un elemento pertenece al
conjunto.
Recorrer todos los elementos del conjunto
Con un bucle for
como éste:
for (Integer entero : conjunto_de_enteros) {
System.out.println(entero);
}
Este bucle imprime todos los enteros del conjunto, uno en cada línea. El problema es que no recorre el conjunto en ningún orden en particular. No hay ningún orden definido en el conjunto.
Eliminar un objeto del conjunto
boolean quitado = conjunto_de_enteros.remove(new Integer(7));
devuelve ‘true’. Si no hay ningún 7 devolvería ‘false’.
Saber si un conjunto está vacío o vaciarlo
boolean estaVacio = conjunto_de_enteros.isEmpty(); // devolverá 'false'
.clear(); // ahora sí que está vacío conjunto_de_enteros
Listas y conjuntos == colecciones
Como puedes observar, hay operaciones sobre las listas y los
conjuntos que son iguales. Y es que tanto las listas como los conjuntos
son colecciones de objetos y por tanto comparten algunas
operaciones. Esto es así porque ambas clases son a su vez del tipo
Collection
, de manera que podemos hacer lo siguiente:
Collection<Integer> coleccion = lista_de_enteros;
// añado un entero a la lista
.add(new Integer(10));
coleccion
= conjunto_de_enteros;
coleccion
// añado un entero al conjunto
.add(new Integer(11)); coleccion
Date cuenta de que ambos enteros se añaden a colecciones distintas: el primero a la lista, el segundo al conjunto, aunque usamos la misma referencia para referirnos a ambas colecciones. Si no acabas de entender porqué es así, lo verás claro cuando estudiemos la herencia.
Colas
Una cola es una colección de elementos que se ‘encolan’, de manera que el elemento que se insertó primero, es el elemento que está en la ‘cabeza’ de la cola. Hay tres operaciones básicas que se pueden hacer con una cola:
- encolar (o insertar) : añadir un elemento al final de la cola
- examinar : consultar el elemento que está en la cabeza de la cola
- desencolar : eliminar el elemento que está en la cabeza de la cola.
En general, no podemos acceder a elementos en cualquier posición de la cola, como en una lista, si no sólo al que está en cabeza. De la misma forma, no podemos eliminar elementos de la cola que no estén en la cabeza de ésta.
Crear una cola
Queue<Integer> cola = new LinkedList<>(); // LinkedList implementa el interface Queue
Encolar, desencolar y examinar la cabeza de la cola
Hay dos formas de realizar estas operaciones con una cola: usando métodos que lanzan excepciones no verificadas si fallan (por ejemplo, si intentamos desencolar de una cola vacía), o métodos que devuelven un valor especial (null o false) en caso de error. En esta tabla tienes las operaciones de Queue que lanzan excepciones (throw) o devuelven un valor (return) si fallan:
throw | return | |
---|---|---|
encolar | add(e) | offer(e) |
desencolar | remove() | poll() |
consultar | element() | peek() |
Encolar elementos
// usando add()
.add(5);
cola.add(6);
cola
// usando offer()
.offer(5);
cola.offer(6); cola
Si no hay errores (por alguna razón no podemos encolar más elementos), ambos métodos se comportan igual.
Consultar el elemento en cabeza
.offer(5);
cola.offer(6);
colaint a = cola.peek(); // el elemento no se desencola
= cola.element(); // misma operación a
Desencolar un elemento
.offer(5);
cola.offer(6);
colaint a = cola.poll(); // devuelve el elemento en cabeza y lo desencola
= cola.remove(); // misma operación a
Las operaciones habituales que podemos usar con otras coleeciones, como size(), clear(), isEmpty() también funcionan con las colas.
Colas de prioridad
Son un tipo de colas donde los elementos se encolan, pero se ordenan en la cola en base a un criterio. De manera que en la cabeza de la cola se encuentra el elemento con mayor (o menor) prioridad. Hay dos formas de definir este criterio de ordenación: Haciendo que los elementos de la lista implementen el interfaz Comparable, o mediante un objeto de tipo Comparator que se le proporciona a la cola en el momento de su creación y que implementa el orden deseado.
Por ejemplo, la clase Integer implementa el interfaz Comparable y por tanto podemos guardar enteros en una cola de prioridad, de manera que el elemento menor de los que insertemos en la cola será el que ocupe la posición de cabeza:
PriorityQueue<Integer> cola = new PriorityQueue();
.add(5);
cola.add(2);
cola.add(3);
colaint a = cola.peek(); // retorna 2
= cola.poll(); // retorna 2 y lo desencola
a = cola.peek(); // retorna 3 a
Si queremos que los enteros se ordenen de otra manera en la cola (por ejemplo de mayor a menor), debemos proporcionar a la cola un comparador de enteros que los ordene de esa forma.
Esto se hace creando una clase que implemente el interfaz
Comparator<T>
, que nos dice que debemos implementar
el método compare(a,b)
para comparar dos objetos de tipo T
‘a’ y ‘b’, de forma que
- si
a < b
, entoncescompare(a,b)
devuelve un número negativo. - si
a.equals(b)
, entoncescompare(a,b)
devuelve cero. - si
a > b
, entoncescompare(a,b)
devuelve un número positivo.
Siguiendo con el ejemplo de la cola de enteros, si queremos que se ordenen ‘al revés’ de mayor a menor, entonces debemos invertir el orden haciendo que
- si
a < b
, entoncescompare(a,b)
devuelve un número positivo. - si
a.equals(b)
, entoncescompare(a,b)
devuelve cero. - si
a > b
, entoncescompare(a,b)
devuelve un número negativo.
El siguiente código hace precisamente esto con una cola de prioridad para enteros:
Comparator<Integer> comparador = new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return b - a;
}
};
PriorityQueue<Integer> cola = new PriorityQueue(comparador);
.add(5);
cola.add(2);
cola.add(3);
colaint a = cola.peek(); // retorna 5
= cola.poll(); // retorna 5 y lo desencola
a = cola.peek(); // retorna 3 a
Mapas
Los mapas permiten establecer una correspondencia entre pares de objetos: uno que actúa como clave y otro como valor asociado a esa clave. Un diccionario, por ejemplo, es un mapa entre cadenas de texto: la palabra que buscamos en el diccionario actúa como clave y su significado como valor asociado.
Un mapa en Java se define así:
Map<Clave, Valor> = new HashMap<Clave, Valor>();
donde Clave y Valor son dos clases cualesquiera.
Así, un diccionario lo implementaríamos como un mapa entre cadenas:
Map<String, String> diccionario = new HashMap<String, String>();
Otro uso típico de un mapa es aquel en el que queremos ‘indexar’ algo usando como índice no números enteros, sino otro tipo de objeto. Por ejemplo, podríamos definir un damero para jugar al ajedrez como un mapa entre coordenadas de las casillas del damero (A3, B7,…) y piezas del juego:
Map<CoordenadaAjedrez, PiezaAjedrez> damero = new HashMap<>();
La ventaja de hacerlo así es doble:
- podemos usar cualquier tipo de objeto como índice (es decir, como clave).
- no necesitamos guardar en el mapa información sobre aquellas casillas que no están ocupadas.
Si lo implementáramos como un array bidimensional, las casillas vacías ocuparían memoria. Pero en el ajedrez siempre hay más casillas vacías que ocupadas. Además, tendríamos que indexar el array con enteros, por lo que tendríamos que ‘traducir’ las coordenadas de alguna manera (A3 -> [0][2], B7 -> [1][6], …).
En cualquier caso, podríamos incluir en el mapa casillas vacías, si así nos interesa, asociándolas, por ejemplo, con el valor ‘null’.
Veamos como podemos trabajar con mapas:
Añadir una entrada al mapa
El método put(·)
añade una correspondencia entre una
clave y su valor:
.put(new CoordenadaAjedrez('A',3),
dameronew PiezaAjedrez("ALFIL",Color.BLANCO));
.put(new CoordenadaAjedrez('B',7),
dameronew PiezaAjedrez("CABALLO",Color.NEGRO));
.put(new CoordenadaAjedrez('B',7),
dameronew PiezaAjedrez("REINA",Color.BLANCO));
La última instrucción cambia la pieza asociada a la casilla B7 por una reina blanca. Es como si hubíeramos sacado al caballo negro del tablero y puesto a la reina blanca en su lugar. El mapa tiene por tanto 2 elementos (pares clave-valor) y no 3.
Fíjate en que nada nos impide asociar el mismo valor a claves distintas:
.put(new CoordenadaAjedrez('F',4),
dameronew PiezaAjedrez("REINA",Color.BLANCO));
// ahora el mapa sí tiene 3 pares clave-valor
O asociar una clave con un valor ‘null’:
.put(new CoordenadaAjedrez('A', 1), null);
damero// ahora el mapa tiene 4 elementos
Tamaño de un mapa
.size(); // devuelve 4 damero
El mapa contiene 4 entradas: ([A3, ALFIL blanco], [B7, REINA blanca], [F4, REINA blanca], [A1, null]).
Obtener el valor asociado a una clave en el mapa
El método get(·)
nos permite buscar en el mapa usando
una clave:
= damero.get(new CoordenadaAjedrez('A',3)); PiezaAjedrez pieza
devuelve una referencia al alfil. Si la clave no está en el mapa, devolverá ‘null’.
Saber si una clave está en el mapa
Usa el método containsKey(·)
para preguntarle al mapa si
la clave dada tiene algún valor asociado:
.containsKey(new CoordenadaAjedrez('A',3)); // devuelve 'true'
damero.containsKey(new CoordenadaAjedrez('H',3)); // devuelve 'false' damero
Saber si un valor está asociado a alguna clave
Usa el método containsValue(·)
para preguntarle al mapa
si el valor dado está asociado al menos a una clave:
.containsValue(new PiezaAjedrez("ALFIL",Color.BLANCO)); // devuelve 'true'
damero.containsValue(new PiezaAjedrez("REY",Color.NEGRO)); // devuelve 'false' damero
Recorrer un mapa
Los mapas no se pueden recorrer directamente, como hacemos con una lista o un conjunto. Sin embargo, si que podemos obtener el conjunto de claves guardadas en el mapa, recorrer este conjunto y pedirle al mapa el valor asociado a cada clave:
Set<CoordenadaAjedrez> coordenadas = damero.keySet();
for (CoordenadaAjedrez coord : coordenadas) {
= damero.get(coord);
PiezaAjedrez pieza System.out.println(coord.toString() + " -> " + pieza.toString());
}
Eliminar una entrada del mapa
Para esto usamos el método remove(·)
:
= damero.remove(new CoordenadaAjedrez('A',3)); PiezaAjedrez pieza
Elimina la entrada asociada a la casilla A3 y me devuelve la pieza que estaba en esa casilla. Si la casilla no estuviera en el mapa, delvolvería ‘null’.
Saber si un mapa está vacío o vaciarlo
Esto es fácil:
boolean estaVacio = damero.isEmpty(); // devolverá 'false'
.clear(); // ahora sí que está vacía damero
Corolario
Es posible que ya te hayas dado cuenta: cuando necesitan comparar
objetos entre sí, para saber si son iguales, estas clases utilizan el
método equals(·)
de esos objetos. Así pues, un objeto
CoordenadaAjedrez será igual a otro si así lo dice el método
equals(·) de la clase CoordenadaAjedrez.
Por ejemplo, cuando le preguntamos a un conjunto si contiene un
determinada objeto ‘objeto1’, éste usa el método equals(·) de
la clase del objeto para buscar un objeto en el conjunto tal que
objeto1.equals(objeto_del_conjunto)
devuelva ‘true’.
hashCode()
Este método, que se suele implementar en todas las clases, devuelve
un entero que actua como identificador para un objeto. Es un método
compatible con equals(), de manera que si
objeto1.equals(objeto2)
devuelve ‘true’, entonces
objeto1.hasCode()
devuelve el mismo valor que
objeto2.hashCode()
.
Algunas implementaciones de listas, conjuntos y mapas utilizan hashCode(·) en lugar de equals(·) para saber si dos objetos son iguales.
API del JCF
Por último, todo programador Java hace un uso intensivo de la documentación del API (Application Programming Interface) del lenguaje. La documentación completa del Java Collections Framework para la versión 1.8 de Java se puede consultar aquí: