Estructuras de Datos Dinámicas en Java
¡Hola, futuros arquitectos de software! En la unidad anterior, trabajamos con arrays, que son como trenes de mercancías: increíblemente eficientes para transportar una cantidad fija de vagones en una vía recta. Sabes exactamente cuántos vagones tienes, y llegar al quinto vagón es súper rápido. Pero, ¿qué pasa si a mitad de camino necesitas añadir un vagón extra? O quitar uno del medio... ¡Tienes que parar todo el tren, desmontarlo y volverlo a montar! Es rígido y poco práctico para situaciones cambiantes.
Ahora, vamos a aparcar el tren y a coger las llaves de un coche en una gran ciudad. Con un coche, puedes ir donde quieras, cambiar de ruta, recoger a más gente por el camino y no tienes un número fijo de asientos para todo tu viaje. Esta flexibilidad es la esencia de las estructuras de datos dinámicas.
En esta unidad, nos sumergiremos en el Java Collections Framework (JCF), que es como el concesionario de vehículos de Java. Nos ofrece un catálogo impresionante de "vehículos" para gestionar nuestros datos: desde las versátiles "furgonetas" ArrayList y los "trenes de carretera" LinkedList, hasta los "almacenes con direcciones únicas" HashMap. Aprenderemos a elegir el vehículo correcto para cada tipo de carga (datos) y cada tipo de viaje (operación). ¡Abróchate el cinturón, que arrancamos!
1. Introducción a las estructuras de datos dinámicas
Cuando el volumen de datos que una aplicación debe manejar crece, las variables simples y los arrays estáticos se quedan cortos. Imagina gestionar un único pedido en una tienda online: podrías usar unas cuantas variables. Pero, ¿qué ocurre cuando tienes que gestionar miles de pedidos simultáneamente, cada uno con su cliente, su lista de productos y su estado? Aquí es donde entran en juego las estructuras de datos dinámicas.
Concepto de Colección
Definición: Colección (Collection)
Una colección es un objeto que agrupa múltiples elementos en una sola unidad. Representa un conjunto de objetos que, a priori, tienen alguna relación entre sí. Piénsalo como una "caja" inteligente diseñada para almacenar y manipular otros objetos de forma eficiente.
El Java Collections Framework (JCF) es la librería estándar de Java que nos proporciona un conjunto de interfaces y clases para trabajar con estas colecciones. Es una de las APIs más importantes y utilizadas del lenguaje.
Diferencias Respecto a Arrays
Los arrays, como vimos en la UD4, son la estructura de datos más básica. Las colecciones son una evolución que soluciona muchas de sus limitaciones.
| Característica | Arrays (String[]) |
Colecciones (List<String>) |
|---|---|---|
| Tamaño | Fijo. Se define en la creación y no se puede cambiar. | Dinámico. Crece o decrece automáticamente según se añaden o eliminan elementos. |
| Funcionalidad | Básica. Ofrece acceso por índice, pero pocas operaciones integradas. | Rica. Incluye métodos para añadir, eliminar, buscar, ordenar, etc. |
| Tipo de Datos | Puede almacenar tipos primitivos (int, char) y objetos. |
Solo almacena objetos. Para primitivos, usa las clases Wrapper (Integer, Character). |
| Flexibilidad | Baja. Modificar (insertar/eliminar en medio) es ineficiente. | Alta. Proporciona implementaciones optimizadas para diferentes casos de uso. |
Tipos Fundamentales
El JCF nos ofrece una variedad de estructuras, cada una con sus propias fortalezas. Estas son las más importantes:
- Listas (
List): Colecciones ordenadas de elementos. Permiten duplicados y el acceso a los elementos es por su posición (índice). Son como un estante de libros numerado. - Colas (
Queue): Colecciones diseñadas para mantener elementos antes de su procesamiento. Siguen el principio FIFO (First-In, First-Out), como la cola del supermercado. - Pilas (
Stack): Colecciones que siguen el principio LIFO (Last-In, First-Out). El último elemento en entrar es el primero en salir, como una pila de platos. - Conjuntos (
Set): Colecciones que no permiten elementos duplicados. Son como una bolsa de canicas únicas. - Mapas (
Map): Colecciones que almacenan pares clave-valor. Cada clave es única y se usa para recuperar su valor asociado. Son como un diccionario. - Árboles (
Tree): Estructuras jerárquicas no lineales donde cada elemento (nodo) puede tener hijos. Son excelentes para almacenar datos ordenados y realizar búsquedas eficientes. - Grafos (
Graph): Estructuras no lineales compuestas por nodos y las conexiones (aristas) entre ellos. Perfectas para modelar redes (sociales, de carreteras, etc.). - Tablas Hash (
Hash Table): Estructuras que usan una función "mágica" (hash) para asignar a cada elemento una ubicación única, permitiendo inserciones y búsquedas increíblemente rápidas (promedio O(1)). Son la base deHashSetyHashMap.
Reflexiona
- ¿Por qué un
ArrayListes más flexible que un array tradicional para gestionar la lista de la compra? - Si estás desarrollando el historial de "Deshacer" (Ctrl+Z) de un editor de texto, ¿qué estructura de datos te parece más adecuada y por qué?
- ¿Qué estructura de datos usarías para almacenar los contactos de tu teléfono, donde cada nombre está asociado a un número?
- ¿Por qué las colecciones en Java no pueden almacenar tipos primitivos como
intdirectamente?
2. Interfaces Base de Colecciones en Java
El Java Collections Framework (JCF) está brillantemente diseñado en torno a un conjunto de interfaces. Esto significa que aprendes a programar contra un "contrato" (List, Set, Map), y puedes cambiar la implementación concreta (ArrayList, HashSet) con un impacto mínimo en tu código.
Interfaz Collection<E>: La Raíz de Todo
Casi todas las colecciones en Java (excepto los Mapas) heredan de la interfaz Collection<E>. Esta interfaz define el comportamiento más básico que cualquier colección debe tener.
Definición: Interfaz Collection<E>
Es la interfaz raíz de la jerarquía de colecciones. Define las operaciones fundamentales como añadir, eliminar, y consultar elementos, sin especificar detalles como el orden o si se permiten duplicados. La E es un parámetro de tipo genérico, que se sustituye por la clase concreta de objetos que queremos almacenar (ej: Collection<String>, Collection<Alumno>).
Operaciones Principales
| Método | Descripción |
|---|---|
boolean add(E elemento) |
Añade un elemento a la colección. Devuelve true si la colección cambió. |
boolean remove(Object objeto) |
Elimina una única instancia del objeto especificado. |
int size() |
Devuelve el número de elementos en la colección. |
boolean isEmpty() |
Devuelve true si la colección no tiene elementos. |
boolean contains(Object objeto) |
Devuelve true si la colección contiene el elemento especificado. |
void clear() |
Elimina todos los elementos de la colección. |
Object[] toArray() |
Convierte la colección en un array de objetos. |
Iterator<E> iterator() |
Devuelve un iterador para recorrer los elementos de la colección (lo veremos en detalle más adelante). |
Decidir qué Colección Utilizar
Antes de empezar a programar, hazte estas tres preguntas clave. La respuesta te guiará hacia la colección correcta como si fuera un GPS.
- ¿Necesito mantener el orden?
- Sí: Necesitas una
List(ArrayList,LinkedList). - No: Un
Set(HashSet) puede ser más eficiente.
- Sí: Necesitas una
- ¿Necesito almacenar elementos duplicados?
- Sí: Una
Listes tu opción. - No: Debes usar un
Set.
- Sí: Una
- ¿Necesito acceder a elementos por una clave única en lugar de por su posición?
- Sí: Necesitas un
Map(HashMap,TreeMap).
- Sí: Necesitas un
graph TD
Start{Inicio: ¿Qué necesito?} --> Q1{¿Necesito<br>duplicados?};
Q1 -- Sí --> Q2{¿Necesito<br>orden?};
Q1 -- No --> UseSet[Usa un Set: HashSet, TreeSet];
Q2 -- Sí, por inserción o acceso posicional --> UseList[Usa una List: ArrayList, LinkedList];
Q2 -- Sí, por orden natural o personalizado --> UseTreeSet[Considera un TreeSet o una List ordenada];
Q2 -- No --> UseSet;
Start --> Q3{¿Necesito<br>pares Clave-Valor?};
Q3 -- Sí --> UseMap[Usa un Map: HashMap, TreeMap];
Q3 -- No --> Q1;
Reflexiona
- Estás creando una baraja de cartas para un juego. No puede haber cartas repetidas. ¿Qué interfaz de colección principal usarías?
- Necesitas almacenar el historial de páginas web visitadas por un usuario, en el orden en que las visitó. ¿Qué interfaz es la más adecuada?
- Para una agenda, necesitas asociar el NIF de una persona (que es único) con su objeto
Personacompleto. ¿Qué estructura elegirías? - ¿Por qué crees que el método
add()devuelve unbooleanen lugar devoid? ¿En qué tipo de colección podría devolverfalse?
3. Listas en Java (List<E>)
Las listas son, con diferencia, la colección más común y versátil que usarás. Son la evolución natural de los arrays, pero con superpoderes.
3.1 Concepto y Características
Definición: Interfaz List<E>
Una List es una subinterfaz de Collection que representa una colección ordenada de elementos (también conocida como secuencia). A diferencia de los Set, las listas permiten elementos duplicados.
Sus características clave son:
- Elementos Ordenados: Las listas mantienen el orden de inserción. El primer elemento que añades estará en la posición 0, el segundo en la 1, y así sucesivamente.
- Permiten Duplicados: Puedes añadir el mismo objeto a una lista varias veces.
- Acceso Posicional (por Índice): Puedes acceder, modificar o eliminar elementos especificando su posición numérica (índice), igual que en un array.
La interfaz List hereda todos los métodos de Collection y añade otros nuevos basados en el índice:
| Método | Descripción |
|---|---|
E get(int index) |
Devuelve el elemento en la posición especificada. |
E set(int index, E element) |
Reemplaza el elemento en la posición index con element. |
void add(int index, E element) |
Inserta un elemento en la posición index, desplazando los siguientes. |
E remove(int index) |
Elimina el elemento en la posición index. |
int indexOf(Object o) |
Devuelve el índice de la primera aparición del objeto, o -1 si no está. |
int lastIndexOf(Object o) |
Devuelve el índice de la última aparición del objeto, o -1 si no está. |
3.2 ArrayList
Es la implementación más común de la interfaz List. Internamente, usa un array dinámico.
Bajo el capó de un ArrayList
Un ArrayList empieza con un array de un tamaño inicial (por ejemplo, 10). Cuando intentas añadir el 11º elemento, la clase crea un nuevo array más grande (normalmente un 50% más grande), copia todos los elementos del array viejo al nuevo y luego descarta el viejo. Este proceso de "redimensionamiento" es automático, pero puede tener un coste de rendimiento si añades muchísimos elementos uno a uno.
Uso básico: ArrayList es excelente para acceso rápido a elementos por su índice (get) y para recorrer la lista, pero es menos eficiente para inserciones o eliminaciones en medio de la lista, ya que requiere desplazar todos los elementos posteriores.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class EjemploArrayList {
public static void main(String[] args) {
// Buena práctica: programar contra la interfaz List
List<String> listaCompra = new ArrayList<>();
// 1. Añadir elementos (add)
listaCompra.add("Leche");
listaCompra.add("Pan");
listaCompra.add("Manzanas");
listaCompra.add(1, "Huevos"); // Añade en una posición específica
System.out.println("Lista de la compra: " + listaCompra);
// 2. Acceder a un elemento (get)
String segundoElemento = listaCompra.get(1);
System.out.println("El segundo elemento es: " + segundoElemento);
// 3. Modificar un elemento (set)
listaCompra.set(0, "Leche desnatada");
System.out.println("Lista actualizada: " + listaCompra);
// 4. Eliminar un elemento (remove)
listaCompra.remove("Pan"); // Por objeto
listaCompra.remove(2); // Por índice
// 5. Ordenación con Collections.sort()
Collections.sort(listaCompra);
System.out.println("Lista ordenada: " + listaCompra);
}
}
3.3 LinkedList
Es la otra gran implementación de List. Internamente, usa una lista doblemente enlazada.
Bajo el capó de un LinkedList
No usa un array. Cada elemento es un "nodo" que contiene el dato y dos punteros: uno al nodo anterior y otro al siguiente. Para añadir o quitar un elemento en medio, solo hay que cambiar un par de punteros. Para acceder al elemento en la posición 500, tiene que empezar desde el principio (o el final) y seguir los punteros 500 veces.
Uso básico: LinkedList es extremadamente eficiente para añadir o eliminar elementos al principio, al final o en medio de la lista. Sin embargo, el acceso a un elemento por su índice (get(i)) es mucho más lento que en un ArrayList, ya que tiene que recorrer la lista desde el principio.
Además de List, LinkedList también implementa las interfaces Queue (cola) y Deque (cola de doble extremo), lo que le da métodos adicionales para comportarse como una pila o una cola.
import java.util.LinkedList;
public class EjemploLinkedList {
public static void main(String[] args) {
LinkedList<String> playlist = new LinkedList<>();
// Métodos de Deque: añadir al principio y al final
playlist.addFirst("Bohemian Rhapsody - Queen");
playlist.addLast("Stairway to Heaven - Led Zeppelin");
playlist.addFirst("Hotel California - Eagles");
System.out.println("Playlist: " + playlist);
// Métodos de Deque: eliminar del principio y del final
String primera = playlist.removeFirst();
System.out.println("Reproduciendo ahora: " + primera);
String ultima = playlist.removeLast();
System.out.println("Quitando la última: " + ultima);
System.out.println("Playlist restante: " + playlist);
// Método de Queue: mirar el primer elemento sin quitarlo
System.out.println("Siguiente en la cola: " + playlist.peek());
}
}
Reflexiona
- Estás creando una aplicación para gestionar una lista de tareas. Los usuarios añadirán tareas principalmente al final y las consultarán a menudo por su posición. ¿Usarías
ArrayListoLinkedList? ¿Por qué? - Ahora imagina una aplicación que gestiona el historial de un navegador web. Los usuarios pueden ir "atrás" y "adelante". ¿Qué estructura te parece más adecuada?
- ¿Qué crees que es más rápido:
lista.get(50000)en unArrayListde 100.000 elementos o en unLinkedListdel mismo tamaño? ¿Ylista.add(50000, "nuevoElemento")? - El método
remove(Object o)en unaListelimina la primera ocurrencia del objeto. ¿Cómo harías para eliminar todas las ocurrencias de un objeto en unArrayList?
4. Pilas (Stack) y Colas (Queue)
Las pilas y las colas son colecciones especializadas, diseñadas para procesar elementos en un orden específico. Son fundamentales en muchos algoritmos e incluso en el funcionamiento interno de los sistemas operativos.
4.1 Clase Stack (LIFO)
Una pila funciona bajo el principio LIFO (Last-In, First-Out): el último elemento que entra es el primero que sale.
Definición: Pila (Stack)
Es una estructura de datos que permite dos operaciones principales: push (empujar), para agregar un elemento en la cima, y pop (sacar), para eliminar el elemento de la cima. Es como una pila de platos: siempre coges el de arriba.
En Java, existe una clase Stack heredada, pero se considera obsoleta.
La clase Stack está obsoleta
La clase java.util.Stack es antigua (desde Java 1.0) y extiende Vector, que es una clase sincronizada y lenta. La recomendación moderna es usar una implementación de la interfaz Deque (como ArrayDeque o LinkedList) para modelar una pila.
Las operaciones principales son:
* push(E item): Añade un elemento a la cima de la pila.
* E pop(): Elimina y devuelve el elemento de la cima. Lanza EmptyStackException si la pila está vacía.
* E peek(): Devuelve el elemento de la cima sin eliminarlo.
* boolean empty(): Devuelve true si la pila está vacía.
* int search(Object o): Busca un objeto y devuelve su distancia desde la cima (1-based).
4.2 Colas (Queue y Deque) (FIFO)
Una cola funciona bajo el principio FIFO (First-In, First-Out): el primer elemento que entra es el primero que sale, como la cola para pagar en una tienda.
Definición: Cola (Queue)
Es una estructura de datos para mantener elementos antes de procesarlos. Las operaciones principales son add u offer (encolar) para agregar un elemento al final, y remove o poll (desencolar) para eliminar el elemento del principio.
La interfaz Deque (pronunciado "deck", por "Double Ended Queue") es aún más versátil, ya que permite añadir y quitar elementos de ambos extremos, por lo que puede funcionar perfectamente como una pila (usando push y pop) o como una cola.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
public class EjemploPilasYColas {
public static void main(String[] args) {
System.out.println("--- Ejemplo de Pila (con Deque) ---");
Deque<String> historialNavegador = new ArrayDeque<>();
historialNavegador.push("google.com"); // Equivale a addFirst()
historialNavegador.push("github.com");
historialNavegador.push("wikipedia.org");
System.out.println("Página actual: " + historialNavegador.peek()); // wikipedia.org
String paginaAtras = historialNavegador.pop(); // Equivale a removeFirst()
System.out.println("Volviendo atrás... ahora en: " + paginaAtras);
System.out.println("Página actual: " + historialNavegador.peek()); // github.com
System.out.println("\n--- Ejemplo de Cola (con LinkedList) ---");
Queue<String> colaImpresion = new LinkedList<>();
colaImpresion.offer("Documento1.pdf"); // Equivale a add() o addLast()
colaImpresion.offer("Foto.jpg");
colaImpresion.offer("Contrato.docx");
System.out.println("Cola de impresión: " + colaImpresion);
String imprimiendo = colaImpresion.poll(); // Equivale a remove() o removeFirst()
System.out.println("Imprimiendo ahora: " + imprimiendo);
System.out.println("Cola restante: " + colaImpresion);
}
}
graph TD
subgraph "Pila (LIFO)"
direction TB
A["push(A)"] --> B["push(B)"] --> C["push(C)"]
C -- pop() --> D("Sale C")
B -- pop() --> E("Sale B")
end
subgraph "Cola (FIFO)"
direction LR
X["offer(X)"] --> Y["offer(Y)"] --> Z["offer(Z)"]
X -- poll() --> W("Sale X")
Y -- poll() --> V("Sale Y")
end
Reflexiona
- Además del historial del navegador o la función "deshacer", ¿en qué otra situación del mundo real o de la informática se te ocurre que una pila (LIFO) sería útil?
- Las colas se usan masivamente en sistemas operativos para gestionar tareas. ¿Puedes pensar por qué el principio FIFO es el más justo y adecuado para, por ejemplo, una cola de impresión?
- La interfaz
Dequetiene métodos comoofferFirstypollFirst, y tambiénpushypop. ¿Cuál es la principal diferencia entrepollypop? (Pista: ¿qué pasa si la colección está vacía?). - Si quieres implementar una pila, ¿por qué
ArrayDequees generalmente preferible aLinkedList? (Pista: investiga la eficiencia de añadir/quitar elementos al principio/final en ambas).
5. Conjuntos (Set<E>)
Los conjuntos son colecciones que modelan la abstracción matemática de un conjunto: una agrupación de elementos únicos y sin un orden particular.
Definición: Interfaz Set<E>
Es una subinterfaz de Collection que representa una colección que no permite elementos duplicados. Si intentas añadir un elemento que ya existe (según su método equals()), la operación simplemente no tendrá efecto y el método add() devolverá false.
Hay tres implementaciones principales:
HashSet: Es la más común. No garantiza ningún orden en los elementos. Es extremadamente rápida para añadir, eliminar y comprobar si un elemento existe (rendimiento de tiempo constante, O(1), en promedio).LinkedHashSet: Como unHashSet, pero mantiene el orden de inserción. Es ligeramente más lenta queHashSet.TreeSet: Mantiene los elementos en un orden natural (o un orden personalizado). Es más lenta queHashSet(rendimiento logarítmico, O(log n)), pero te permite recorrer los elementos de forma ordenada.
HashSet y la importancia de equals() y hashCode()
HashSet utiliza una tabla hash para almacenar sus elementos. Para determinar si un elemento es "duplicado", no solo usa equals(), sino que depende críticamente de hashCode().
¡El Contrato es Ley!
Para que un HashSet (y un HashMap) funcione correctamente con tus propios objetos, tu clase DEBE sobrescribir equals() y hashCode() siguiendo estas reglas:
1. Si objeto1.equals(objeto2) es true, entonces objeto1.hashCode() debe ser igual a objeto2.hashCode().
2. El hashCode() de un objeto debe ser consistente (devolver el mismo valor) mientras no se modifiquen los campos usados en equals().
Si no lo haces, te encontrarás con que el HashSet permite "duplicados" o no encuentra objetos que sabes que están ahí.
Ejemplo con HashSet y Alumno
Veamos qué pasa cuando la clase Alumno no tiene equals() y hashCode() correctos, y qué pasa cuando sí los tiene.
// Escenario 1: Clase Alumno SIN equals() y hashCode() correctos
// Usará la implementación de Object, que compara referencias de memoria.
public class AlumnoMal {
private String nia;
private String nombre;
// ... constructor, getters ...
}
Set<AlumnoMal> claseMal = new HashSet<>();
claseMal.add(new AlumnoMal("111A", "Ana"));
claseMal.add(new AlumnoMal("111A", "Ana")); // ¡Debería ser un duplicado!
System.out.println(claseMal.size()); // Imprime 2. ¡ERROR! Permitió un duplicado.
// Escenario 2: Clase Alumno CON equals() y hashCode() correctos (basados en NIA)
public class AlumnoBien {
private String nia;
private String nombre;
// ... constructor, getters ...
@Override
public int hashCode() {
return Objects.hash(nia);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
AlumnoBien other = (AlumnoBien) obj;
return Objects.equals(nia, other.nia);
}
}
Set<AlumnoBien> claseBien = new HashSet<>();
claseBien.add(new AlumnoBien("222B", "Juan"));
claseBien.add(new AlumnoBien("222B", "Juan")); // Duplicado
System.out.println(claseBien.size()); // Imprime 1. ¡CORRECTO! No permitió el duplicado.
TreeSet y la Ordenación
TreeSet mantiene los elementos ordenados. Para ello, los objetos que almacena deben implementar la interfaz Comparable (orden natural) o debes pasarle un Comparator al crearlo.
Consistencia entre compareTo y equals
Para TreeSet, la unicidad se determina usando compareTo() (o compare()), no equals(). Si a.compareTo(b) devuelve 0, TreeSet considera que los objetos son iguales y no añadirá el segundo. Es una buena práctica que el orden definido por compareTo sea "consistente con equals". Esto significa que a.compareTo(b) == 0 debería ser verdad si y solo si a.equals(b) es verdad. Si no es así, TreeSet y HashSet se comportarán de forma diferente para la misma clase.
// Suponiendo que Alumno implementa Comparable por nombre
Set<String> nombresOrdenados = new TreeSet<>();
nombresOrdenados.add("Carlos");
nombresOrdenados.add("Ana");
nombresOrdenados.add("Beatriz");
System.out.println(nombresOrdenados); // Salida: [Ana, Beatriz, Carlos]
Reflexiona
- Si quieres una colección que no permita duplicados y que, al recorrerla, te devuelva los elementos en el mismo orden en que los insertaste, ¿qué implementación de
Setusarías? - En el
Escenario 1delAlumnoMal, ¿por quéHashSetconsidera que los dos objetos son diferentes? - ¿Qué pasaría si metes un objeto en un
HashSet, luego modificas un campo de ese objeto que se usa en el cálculo dehashCode(), y finalmente intentas hacermiSet.contains(miObjeto)? ¿Lo encontraría? - ¿Por qué
TreeSetno permite elementosnull?
6. Mapas (Map<K, V>)
A diferencia de las colecciones anteriores que almacenan elementos individuales, un mapa almacena asociaciones entre pares clave-valor.
Definición: Interfaz Map<K, V>
Un Map es un objeto que mapea claves (K, de Key) a valores (V, de Value). Cada clave debe ser única; no puede haber claves duplicadas. Cada clave puede mapear a, como máximo, un valor. Es como un diccionario: la palabra (clave) es única y tiene una definición (valor) asociada.
La interfaz Map no extiende la interfaz Collection.
Implementaciones Principales
HashMap: La implementación más común. Usa una tabla hash. No garantiza ningún orden. Permite una clavenully múltiples valoresnull. Ofrece rendimiento O(1) paraget()yput().LinkedHashMap: Mantiene el orden de inserción de las claves.TreeMap: Mantiene las claves ordenadas (orden natural o porComparator).
Métodos Principales de Map
| Método | Descripción |
|---|---|
V put(K key, V value) |
Asocia la clave key con el valor value. Si la clave ya existía, reemplaza el valor antiguo y lo devuelve. |
V get(Object key) |
Devuelve el valor asociado a la clave, o null si la clave no existe. |
V remove(Object key) |
Elimina el mapeo para una clave. |
boolean containsKey(Object key) |
Devuelve true si el mapa contiene un mapeo para la clave especificada. |
boolean containsValue(Object value) |
Devuelve true si el mapa mapea una o más claves al valor especificado. |
int size() |
Devuelve el número de pares clave-valor. |
Set<K> keySet() |
Devuelve un Set con todas las claves del mapa. |
Collection<V> values() |
Devuelve una Collection con todos los valores del mapa. |
Set<Map.Entry<K,V>> entrySet() |
Devuelve un Set de objetos Map.Entry que representan cada par clave-valor. |
Ejemplo: Contando la Frecuencia de Palabras
Un uso clásico de HashMap es contar la frecuencia de elementos.
import java.util.HashMap;
import java.util.Map;
public class FrecuenciaPalabras {
public static void main(String[] args) {
String texto = "hola mundo hola a todos y todas en el mundo";
Map<String, Integer> frecuencias = new HashMap<>();
String[] palabras = texto.split(" ");
for (String palabra : palabras) {
// getOrDefault es muy útil: si la clave no existe, devuelve el valor por defecto (0)
int contador = frecuencias.getOrDefault(palabra, 0);
frecuencias.put(palabra, contador + 1);
}
System.out.println("Frecuencia de palabras:");
// Recorrer con entrySet() es la forma más eficiente
for (Map.Entry<String, Integer> entry : frecuencias.entrySet()) {
System.out.println("'" + entry.getKey() + "': " + entry.getValue());
}
}
}
Ejemplo Avanzado: Map<Alumno, ArrayList<Calificacion>>
Los mapas son increíblemente potentes para modelar datos complejos. Imagina que queremos almacenar las calificaciones de varios módulos para cada alumno.
public class Calificacion {
private String modulo;
private List<Double> notas;
// ... constructor, getters, etc.
}
public class Alumno {
// ... con equals y hashCode basados en NIA
}
// En nuestro programa principal
Map<Alumno, List<Calificacion>> expediente = new TreeMap<>(); // TreeMap para tener los alumnos ordenados
// ... código para rellenar el mapa ...
Reflexiona
- ¿Por qué
Mapno hereda deCollection? (Pista:add(E element)no tiene sentido para unMap). - Si usas un objeto de tu propia clase como clave en un
HashMap, ¿qué métodos de esa clase son absolutamente críticos para que el mapa funcione correctamente? - Explica las tres formas de recorrer un
Map(keySet,values,entrySet) y cuál consideras más eficiente si necesitas tanto la clave como el valor en cada iteración. - ¿Qué pasaría si intentas hacer
miMapa.put(null, "valor");en unHashMap? ¿Y en unTreeMap? Investígalo.
7. Iteradores
¿Cómo recorremos una colección de forma segura y estandarizada, especialmente si queremos modificarla durante el recorrido? La respuesta son los iteradores.
7.1 Interfaz Iterator<E>
Definición: Iterator<E>
Es un objeto que permite recorrer una colección elemento a elemento. Proporciona una forma unificada de acceder a los elementos secuencialmente, sin exponer la estructura interna de la colección.
Proporciona tres métodos clave:
* boolean hasNext(): Devuelve true si hay más elementos por recorrer.
* E next(): Devuelve el siguiente elemento y avanza el "cursor" del iterador.
* void remove(): Elimina de la colección el último elemento devuelto por next(). Este es el único modo seguro de modificar una colección mientras se itera sobre ella.
List<String> lista = new ArrayList<>(List.of("A", "B", "C", "D"));
Iterator<String> it = lista.iterator();
while (it.hasNext()) {
String elemento = it.next();
System.out.println(elemento);
if (elemento.equals("B")) {
it.remove(); // ¡Forma correcta de eliminar!
}
}
System.out.println("Lista final: " + lista); // [A, C, D]
ConcurrentModificationException
Si intentas modificar una colección (añadir o quitar elementos) usando los métodos de la propia colección mientras la recorres con un bucle for-each o un iterador, Java lanzará una ConcurrentModificationException. Siempre debes usar iterator.remove().
7.2 Interfaz Iterable<E>
Esta es una interfaz muy simple, pero fundamental. Solo tiene un método: iterator().
Iterable y el bucle for-each
Cualquier clase que implemente la interfaz Iterable<E> puede ser utilizada en un bucle for-each (for (Elemento e : miColeccion)). Todas las colecciones del JCF implementan Iterable.
Podemos hacer que nuestras propias clases sean "iterables":
public class Grupo implements Iterable<Alumno> {
private String nombre;
private List<Alumno> alumnos;
public Grupo(String nombre) {
this.nombre = nombre;
this.alumnos = new ArrayList<>();
}
public void addAlumno(Alumno alumno) {
alumnos.add(alumno);
}
// Implementación del método de Iterable
@Override
public Iterator<Alumno> iterator() {
// Simplemente delegamos al iterador de nuestra lista interna
return alumnos.iterator();
}
}
// Ahora podemos hacer esto:
Grupo daw1 = new Grupo("1º DAW");
daw1.addAlumno(new Alumno("Ana", 10));
daw1.addAlumno(new Alumno("Luis", 8));
for (Alumno alumno : daw1) { // ¡Funciona gracias a Iterable!
System.out.println(alumno.getNombre());
}
7.3 Interfaz ListIterator<E>
Es un subtipo de Iterator específico para Lists, que añade muchísima más potencia.
- Permite el recorrido bidireccional (
hasPrevious(),previous()). - Permite modificar la lista durante la iteración (
add(),set()). - Permite conocer el índice del elemento siguiente y anterior (
nextIndex(),previousIndex()).
| Característica | Iterator |
ListIterator |
|---|---|---|
| Dirección | Hacia adelante | Hacia adelante y hacia atrás |
| Operaciones | hasNext(), next(), remove() |
Todo lo de Iterator más hasPrevious(), previous(), add(), set() |
| Colecciones | Todas las de Collection |
Solo para List |
Reflexiona
- ¿Por qué
iterator.remove()es seguro mientras quecollection.remove()no lo es durante una iteración? - Implementa la interfaz
Iterableen la claseAutordel ejemplo N:M para que se pueda iterar sobre los libros que ha escrito. - Usando un
ListIterator, recorre una lista de números del final al principio e imprime cada número. - ¿Por qué no existe un
SetIteratorcon las mismas funcionalidades queListIterator?
8. Ordenación y Comparación
A menudo, necesitamos que nuestras colecciones estén ordenadas. Java nos ofrece dos interfaces para definir criterios de ordenación: Comparable y Comparator.
8.1 Interface Comparable<T>
Esta interfaz se usa para definir el orden natural de los objetos de una clase.
Definición: Comparable<T>
Una clase que implementa Comparable<T> está diciendo: "Yo sé cómo compararme con otros objetos de mi mismo tipo". Implementa un único método, compareTo(T otro), que devuelve:
* Un int negativo si this objeto es "menor que" otro.
* Cero si this objeto es "igual a" otro en términos de orden.
* Un int positivo si this objeto es "mayor que" otro.
public class Alumno implements Comparable<Alumno> {
private String nombre;
private int edad;
// ... Constructor, getters, etc. ...
// Orden natural: por edad, de menor a mayor. Si la edad es la misma, por nombre.
@Override
public int compareTo(Alumno otro) {
int comparacionEdad = Integer.compare(this.edad, otro.edad);
if (comparacionEdad != 0) {
return comparacionEdad;
} else {
return this.nombre.compareTo(otro.nombre);
}
}
}
8.2 Interfaz Comparator<T>
¿Y si queremos ordenar los alumnos por nota a veces, y por nombre otras? Comparable solo define un orden natural. Para múltiples criterios de ordenación, o para ordenar clases que no puedes modificar, usamos Comparator.
Definición: Comparator<T>
Un Comparator es un objeto cuya única función es comparar otros dos objetos. Implementa un método compare(T o1, T o2). La lógica de devolución es la misma que en compareTo.
import java.util.Comparator;
// Un comparador para ordenar Alumnos por nota, de mayor a menor.
public class AlumnoPorNotaComparator implements Comparator<Alumno> {
@Override
public int compare(Alumno a1, Alumno a2) {
return Integer.compare(a2.getNota(), a1.getNota()); // Orden descendente
}
}
// Uso:
List<Alumno> clase = ...;
Collections.sort(clase); // Usa el orden natural de Alumno (Comparable)
Collections.sort(clase, new AlumnoPorNotaComparator()); // Usa el criterio del Comparator
Clases Anónimas y Lambdas
Desde Java 8, es muy común crear Comparators "al vuelo" usando clases anónimas o, de forma mucho más concisa, expresiones lambda.
// Ordenar por nombre, alfabéticamente
clase.sort((a1, a2) -> a1.getNombre().compareTo(a2.getNombre()));
// O usando métodos de referencia, ¡aún más corto!
clase.sort(Comparator.comparing(Alumno::getNombre));
-> Ampliación Comparator y Comparable
9. Estructuras de Datos Clásicas (Conceptos)
El JCF es una abstracción de alto nivel. Es útil entender brevemente las estructuras de datos "clásicas" que hay debajo, ya que son la base de la informática.
9.1 Listas Enlazadas
Una lista enlazada consiste en nodos. Cada nodo contiene un dato y un "puntero" o referencia al siguiente nodo de la secuencia. En una lista doblemente enlazada (como la LinkedList de Java), cada nodo tiene también un puntero al nodo anterior. Son muy eficientes para inserciones y eliminaciones, pero lentas para el acceso aleatorio.
9.2 Árboles
Son estructuras jerárquicas. Comienzan en un nodo raíz. Cada nodo puede tener cero o más nodos hijos. Los nodos sin hijos se llaman hojas. Un tipo muy común es el Árbol Binario de Búsqueda (BST), donde cada nodo tiene como máximo dos hijos, y todos los valores del subárbol izquierdo son menores que el nodo, y los del subárbol derecho son mayores. Esto permite búsquedas muy rápidas (O(log n)), pero solo si el árbol está balanceado (no degenera en una lista).
9.3 Grafos
Un grafo es un conjunto de vértices (o nodos) y un conjunto de aristas (o conexiones) que los unen. Se usan para modelar todo tipo de redes: sociales (amigos), de transporte (carreteras entre ciudades), de internet (routers conectados), etc. Pueden ser dirigidos (las conexiones tienen un sentido) o no dirigidos, y ponderados (cada conexión tiene un "coste" o "peso") o no ponderados.
9.4 Tablas Hash
Son la magia detrás de HashSet y HashMap. Una tabla hash es básicamente un array. Para almacenar un objeto, se le aplica una función hash que convierte el objeto en un número entero. Este número (o su módulo respecto al tamaño del array) se usa como índice para decidir en qué "cubo" o bucket del array guardar el objeto.
* Colisiones: Ocurre cuando dos objetos diferentes producen el mismo hash. Se gestionan, por ejemplo, almacenando una lista de objetos en ese bucket.
* Una buena función hash distribuye los objetos de manera uniforme, minimizando las colisiones y garantizando un rendimiento cercano a O(1). Una mala función hash puede hacer que el rendimiento degenere a O(n).
Aplicación en el Mundo Real
ArrayList: Es tu caballo de batalla para casi cualquier lista donde el acceso por índice es frecuente (ej: mostrar una lista de productos en una web).LinkedList: Ideal para implementar colas, pilas, o listas donde hay constantes inserciones/eliminaciones en los extremos (ej: una playlist de música donde puedes añadir canciones al principio o al final).HashSet: Perfecto para saber rápidamente si un elemento ya ha sido procesado o si existe en un gran conjunto de datos, sin importar el orden (ej: comprobar si un email ya está registrado).HashMap: La estructura más usada para mapear un identificador único a un objeto complejo (ej: unMap<Integer, Usuario>para cachear usuarios por su ID y acceder a ellos instantáneamente).TreeSet/TreeMap: Se usan cuando necesitas mantener los datos constantemente ordenados (ej: un ranking de puntuaciones de un juego).
Para Saber Más
- Documentación Oficial de Oracle sobre el Collections Framework: The Java™ Tutorials - Collections - La guía de referencia completa y oficial.
- Visualgo - Visualizing data structures and algorithms: https://visualgo.net/en - Una herramienta interactiva increíble para visualizar cómo funcionan internamente las listas enlazadas, árboles, tablas hash y otras estructuras.
- Baeldung: A Guide to the Java Collections Framework: Java Collections Framework Guide - Un tutorial muy completo que cubre todas las colecciones principales con ejemplos prácticos.

