Programación 3

Universidad de Alicante, 2022–2023

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 (no podemos usarlas a priori con tipos primitivos como int o double). 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;

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é ‘new ArrayList’ y no ‘new List’ quedará claro cuando hablemos de polimorfismo e interfaces.

Añadir elementos a la lista

El método add(·) añade una referencia a un objeto al final de la lista

lista_de_enteros.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

Tamaño de una lista

Usa size() para saber el tamaño de una lista:

lista_de_enteros.size();

devuelve 4: la lista contiene 4 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);

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:

lista_de_enteros.contains(new Integer(7)); // me devolverá 'true'
lista_de_enteros.contains(new Integer(8)); // me devolverá 'false'

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):

lista_de_enteros.indexOf(new Integer(6)); // devuelve 2
lista_de_enteros.indexOf(new Integer(10)); // devuelve -1

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 va tomando el valor del siguiente elemento de la lista ‘lista_de_enteros’ en cada iteración, recorremos TODOS los elementos de la lista, imprimiendo uno en cada línea.

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));

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 (el 5).

Saber si una lista está vacía o vaciarla

Esto es fácil:

boolean estaVacia = lista_de_enteros.isEmpty(); // devolverá 'false'
lista_de_enteros.clear(); // ahora sí que está vacía

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.

conjunto_de_enteros.add(new Integer(4));
conjunto_de_enteros.add(new Integer(5));
conjunto_de_enteros.add(new Integer(7));
boolean repe = conjunto_de_enteros.add(new Integer(4));
// no añade el 4 porque ya está en el conjunto y además devuelve 'false'

Tamaño de un conjunto

conjunto_de_enteros.size();

me devuelve 3: el conjunto contiene 3 elementos.

Saber si un elemento está en la lista

conjunto_de_enteros.contains(new Integer(7)); // me devolverá 'true'
conjunto_de_enteros.contains(new Integer(8)); // me devolverá 'false'

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'
conjunto_de_enteros.clear(); // ahora sí que está vacío

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
coleccion.add(new Integer(10));

coleccion = conjunto_de_enteros;

// añado un entero al conjunto
coleccion.add(new Integer(11));

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.

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 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;

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:

damero.put(new CoordenadaAjedrez('A',3),
           new PiezaAjedrez("ALFIL",Color.BLANCO));

damero.put(new CoordenadaAjedrez('B',7),
           new PiezaAjedrez("CABALLO",Color.NEGRO));

damero.put(new CoordenadaAjedrez('B',7),
           new 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.

Fíjate en que nada nos impide asociar el mismo valor a claves distintas:

damero.put(new CoordenadaAjedrez('F',4),
           new PiezaAjedrez("REINA",Color.BLANCO));

O asociar una clave con un valor ‘null’:

damero.put(new Coordenada('A', 1), null);

Tamaño de un mapa

damero.size(); // devuelve 3

El mapa contiene 3 entradas: ([A3, ALFIL blanco], [B7, 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:

PiezaAjedrez pieza = damero.get(new CoordenadaAjedrez('A',3));  

devuelve una referencia al alfil. Si la clave no tiene valor asociado 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:

damero.containsKey(new CoordenadaAjedrez('A',3)); // devuelve 'true'
damero.containsKey(new CoordenadaAjedrez('H',3)); // devuelve 'false'

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:

damero.containsValue(new PiezaAjedrez("ALFIL",Color.BLANCO)); // devuelve 'true'
damero.containsValue(new PiezaAjedrez("REY",Color.NEGRO)); // devuelve 'false'

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) {
  PiezaAjedrez pieza = damero.get(coord);
  System.out.println(coord.toString() + " -> " + pieza.toString());
}

Eliminar una entrada del mapa

Para esto usamos el método remove(·):

PiezaAjedrez pieza = damero.remove(new CoordenadaAjedrez('A',3));

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'
damero.clear(); // ahora sí que está vacía

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éotod 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í:

Java Collections Framework