Tabla de Contenidos

04 - Estructura de datos dinámica: Colecciones y Maps

1. Colecciones

Las colecciones son estructuras de datos avanzadas que pueden cambiar de tamaño dinámicamente. Todas las colecciones principales derivan de la interfaz base java.util.Collection.

Principales métodos comunes de la interfaz Collection:

1.1 Genéricos y restricción de tipos primitivos

Las colecciones en Java hacen uso intensivo de los genéricos (definidos entre <>), los cuales permiten especificar el tipo de datos que almacenará la colección en tiempo de compilación.

Las colecciones solo pueden almacenar objetos (tipos por referencia). Está estrictamente prohibido utilizar tipos primitivos (int, double, char, boolean, etc.) como parámetros de tipo genérico.

Para trabajar con valores primitivos dentro de una colección, es obligatorio utilizar sus correspondientes clases de envoltorio (Integer, Double, Character, Boolean, etc.).

Gracias al Autoboxing y Unboxing automático de Java, la conversión entre el tipo primitivo y el objeto envoltorio se realiza de forma transparente:

List<Integer> numeros = new ArrayList<>();

numeros.add(10); // Autoboxing: el int '10' se convierte automáticamente en Integer
numeros.add(20);

int primerNumero = numeros.get(0); // Unboxing: el Integer se convierte a int primitivo

1.2 Polimorfismo y programación orientada a interfaces

Una de las mejores prácticas de diseño en Java es el principio de programar hacia una interfaz, no hacia una implementación.

Al crear una colección, se debe declarar la variable utilizando el tipo de la interfaz (List, Set), e instanciar la clase concreta deseada (ArrayList, HashSet).

// No recomendado: Acopla el código a una implementación específica
ArrayList nombres = new ArrayList<>();

Declarar las variables utilizando la interfaz permite cambiar la implementación subyacente en una sola línea de código sin modificar el resto de la aplicación que consume la colección:

List nombres = new ArrayList<>();

nombres.add("Ana");
nombres.add("Carlos");
IO.println(nombres.get(0));

1.3 Conjuntos (Set)

Un Set es una colección que NO permite elementos duplicados.

Implementaciones más comunes:

Set<Integer> numbers = new HashSet<Integer>(); //Una práctica habitual es definir el tipo de datos conjunto como la interfaz genérica (Set)

numbers.add(10);
numbers.add(5);
numbers.add(10); // Devuelve false y NO se añade porque está duplicado

A partir de Java 9, se pueden crear conjuntos de forma estática mediante el método factory Set.of():

Set<Integer> numbers = Set.of(2, 10, 3, 23, 99);

Si inicializamos un conjunto con el método of(), el conjunto que se crea es inmutable, es decir, no podemos cambiarlo, con lo que no podremos añadir, eliminar ni modificar elementos.

Para recorrer un conjunto se utiliza habitualmente un bucle for-each:

for (Integer number : numbers) {
    IO.println(number);
}

1.4 Listas (List)

Un List es una colección ordenada que SÍ permite elementos duplicados y cuenta con acceso posicional mediante índice (comenzando siempre desde $0$).

Implementaciones principales:

Métodos clave de la interfaz List:

Las listas se utilizan de forma muy parecida a los conjuntos:

        List<Integer> numbers = new ArrayList<>();

        numbers.add(1); // Añade un elemento al final de la lista.
        numbers.add(3); // Añade otro elemento al final de la lista.
        numbers.add(1,2); // Añade en la posición 1 el elemento 2.
        numbers.add(numbers.get(1)+numbers.get(2)); // Suma los valores contenidos en la posición 1 y 2, y lo agrega al final.
        numbers.remove(0); // Elimina el primer elementos de la lista.
        
        for (Integer number: numbers)
            IO.println("Elemento:" + number); // Muestra la lista.
        }    
        

Al igual que con los conjuntos, se pueden inicializar listas inmutables mediante List.of():

        List<Integer> numbers = List.of(1, 3, 5, 67);

1.5 Iteración segura con Iterator

Cuando se recorre una colección utilizando un bucle for-each tradicional, no está permitido modificar ni eliminar elementos de la colección. Si se intenta realizar un remove() o un add() directamente sobre la colección durante la iteración, Java lanzará una excepción ConcurrentModificationException.

Para eliminar elementos de forma segura mientras se recorre una colección, se debe emplear un objeto Iterator:

// Obtención del iterador
Iterator it = estudiantes.iterator();

while (it.hasNext()) {
    String nombre = it.next();
    if (nombre.startsWith("J")) {
        it.remove(); // Eliminación segura sin lanzar ConcurrentModificationException
    }
}

2. Maps

Un Map es una estructura que almacena asociaciones de clave-valor (Key $\rightarrow$ Value). Las claves son únicas (no se pueden repetir) y sirven para acceder directamente al valor asociado.

2.1 Conceptos clave e implementaciones

Implementaciones principales:

Métodos clave de Map:

Map<String, Integer> ages = new HashMap<String, Integer>();

ages.put("Ana", 20);
ages.put("Pedro", 25);
ages.put("María", 21); 
ages.put("Ana", 21); // Actualiza la edad de Ana a 21

for (String name : ages.keySet()) {
    IO.println(name + ": " + ages.get(name));
}

También es posible instanciar mapas inmutables con Map.of():

        Map<String, Integer> ages = Map.of("Ana", 20, "Pedro", 25, "María", 21);

2.2 Recorrido eficiente: keySet() vs. entrySet()

Existen dos formas principales de iterar sobre un mapa mediante un bucle for-each:

Recorrido mediante keySet()

El método keySet() devuelve un conjunto con las claves del mapa. Para obtener el valor asociado a cada clave, es necesario llamar a map.get(clave) en cada iteración (lo que realiza una búsqueda adicional):

// Menos eficiente: realiza una búsqueda extra en el mapa con .get() en cada iteración
for (String producto : precios.keySet()) {
    Double precio = precios.get(producto);
    IO.println(producto + " cuesta " + precio + "€");
}

Recorrido eficiente mediante entrySet()

El método entrySet() devuelve un conjunto de parejas clave-valor representadas por la interfaz interna Map.Entry<K, V>. Permite acceder tanto a la clave (getKey()) como al valor (getValue()) de forma directa sin realizar búsquedas adicionales:

// Recomendado y más eficiente para leer clave y valor simultáneamente
for (Map.Entry<String, Double> entrada : precios.entrySet()) {
   IO.println(entrada.getKey() + " cuesta " + entrada.getValue() + "€");
}

3. Requisitos de los Objetos: Hash vs. Árboles

El comportamiento interno de las colecciones impone ciertos requisitos sobre las clases de los objetos que se almacenan en ellas.

3.1 Estructuras basadas en Hash (HashSet, HashMap)

Las colecciones con el prefijo Hash utilizan una tabla de dispersión (Hash Table) para localizar elementos en tiempo constante $O(1)$.

Para que funcionen correctamente, la clase de los objetos almacenados debe sobrescribir obligatoriamente los métodos equals() y hashCode() de Object:

3.2 Estructuras basadas en Árboles (TreeSet, TreeMap)

Las colecciones con el prefijo Tree mantienen sus elementos ordenados automáticamente. No utilizan hashCode(), sino que requieren que los elementos se puedan comparar entre sí:

4. La Clase de Utilidad java.util.Collections

La clase java.util.Collections (en plural) contiene exclusivamente métodos estáticos de utilidad diseñados para operar sobre listas y colecciones.

Principales métodos estáticos:

List<Integer> notas = new ArrayList<>();
notas.add(8);
notas.add(4);
notas.add(10);

Collections.sort(notas); // La lista ahora es [4, 8, 10]
Collections.reverse(notas); // La lista ahora es [10, 8, 4]
int notaMaxima = Collections.max(notas); // Devuelve 10

Ejercicios

Ejercicio 1

Crea un programa que realice las siguientes acciones:

Ejercicio 2

Crea un método estático que reciba una lista de cadenas de texto List y realice las siguientes transformaciones:

Ejercicio 3

Dada una lista inicial de direcciones de correo electrónico:

Ejercicio 4

Crea un programa con una lista con elementos repetidos:

Ejercicio 5

Desarrolla un programa de gestión de inventario para una tienda:

Ejercicio 6

Dado un texto/frase introducido por teclado o almacenado en una cadena (ej. “java es un lenguaje java orientado a objetos y java es popular”):

Ejercicio 7

Dada la clase Estudiante con las propiedades dni (String) y nombre (String):

Ejercicio 8

Crea un sistema para clasificar alumnos por asignatura: