📚 ¿Qué son las colecciones en Java?
Las colecciones en Java son estructuras de datos dinámicas que permiten almacenar, organizar y manipular grupos de objetos de forma eficiente. A diferencia de los arrays tradicionales, cuyo tamaño es fijo una vez declarado, las colecciones pueden crecer y reducirse automáticamente según las necesidades del programa. Constituyen una de las herramientas más utilizadas en el desarrollo profesional con Java y resultan imprescindibles para cualquier programador que aspire a escribir código robusto y mantenible.
El Java Collections Framework (JCF) es la arquitectura unificada que proporciona interfaces, implementaciones y algoritmos para trabajar con colecciones. Fue introducido en Java 2 (JDK 1.2) y ha evolucionado significativamente, especialmente con la llegada de los genéricos en Java 5 y los Streams en Java 8. Este framework se encuentra en el paquete java.util y resuelve problemas fundamentales como almacenar listas de elementos, asociar claves a valores, eliminar duplicados o gestionar colas de procesamiento.
En la práctica, las colecciones son la espina dorsal de casi cualquier aplicación Java: desde gestionar la lista de productos de un carrito de compra hasta indexar millones de registros por clave en un servidor de alto rendimiento. Dominar las colecciones no es opcional para un desarrollador Java profesional: es un requisito fundamental.
🗂️ Jerarquía del Collections Framework
El Collections Framework de Java se organiza en torno a un conjunto de interfaces que definen los contratos de comportamiento, y múltiples clases concretas que los implementan. Comprender esta jerarquía es esencial para elegir la estructura de datos correcta en cada situación.
🔹 Interfaces principales
En la raíz de la jerarquía se encuentra la interfaz Iterable, que permite recorrer cualquier colección con un bucle for-each. De ella desciende Collection, la interfaz madre de la que derivan tres grandes ramas:
| Interfaz | Descripción | Permite duplicados | Ordenada | Implementaciones principales |
|---|---|---|---|---|
List |
Secuencia ordenada de elementos con acceso posicional | ✅ Sí | ✅ Por inserción | ArrayList, LinkedList, Vector |
Set |
Colección sin elementos duplicados | ❌ No | Depende | HashSet, TreeSet, LinkedHashSet |
Queue |
Cola para procesamiento en orden FIFO o por prioridad | ✅ Sí | ✅ FIFO/prioridad | LinkedList, PriorityQueue, ArrayDeque |
Map |
Asociación de claves únicas a valores | Claves: ❌ / Valores: ✅ | Depende | HashMap, TreeMap, LinkedHashMap |
Map no extiende de Collection. Aunque se incluye dentro del Collections Framework, su estructura clave-valor la hace conceptualmente diferente. Sin embargo, se puede obtener una vista de tipo Collection a través de map.values(), map.keySet() o map.entrySet().
🔹 Diagrama conceptual de la jerarquía
Iterable
│
Collection Map
/ | \ │
/ | \ ┌───┴───┐
List Set Queue HashMap SortedMap
/ \ / \ | │
ArrayList │ HashSet Deque TreeMap
LinkedList │ TreeSet / \
│ LinkedHS ArrayDeque
SortedSet PriorityQueue
│
TreeSet
📋 La interfaz List: ArrayList y LinkedList
La interfaz List representa una secuencia ordenada de elementos que permite acceso por índice posicional, duplicados y control preciso sobre dónde se inserta cada elemento. Es la estructura más utilizada en la programación Java del día a día.
▶️ ArrayList: el caballo de batalla
ArrayList es la implementación más popular de List. Internamente utiliza un array dinámico que se redimensiona automáticamente cuando se llena (normalmente al 150% de su capacidad actual). Su principal ventaja es el acceso aleatorio en tiempo constante O(1), lo que la convierte en la opción predeterminada para la mayoría de casos.
import java.util.ArrayList;
import java.util.List;
public class EjemploArrayList {
public static void main(String[] args) {
// Crear un ArrayList con genéricos (type-safe)
List<String> lenguajes = new ArrayList<>();
// Añadir elementos
lenguajes.add("Java");
lenguajes.add("Python");
lenguajes.add("JavaScript");
lenguajes.add("C++");
lenguajes.add("Java"); // Los duplicados están permitidos
System.out.println("Lista completa: " + lenguajes);
System.out.println("Elemento en índice 2: " + lenguajes.get(2));
System.out.println("Tamaño: " + lenguajes.size());
// Modificar un elemento
lenguajes.set(3, "Go");
System.out.println("Tras modificar índice 3: " + lenguajes);
// Eliminar por índice y por objeto
lenguajes.remove(0); // Elimina "Java" (el primero)
lenguajes.remove("Python"); // Elimina la primera ocurrencia
System.out.println("Tras eliminaciones: " + lenguajes);
// Buscar elementos
System.out.println("¿Contiene Go? " + lenguajes.contains("Go"));
System.out.println("Índice de JavaScript: " + lenguajes.indexOf("JavaScript"));
}
}
Lista completa: [Java, Python, JavaScript, C++, Java]
Elemento en índice 2: JavaScript
Tamaño: 5
Tras modificar índice 3: [Java, Python, JavaScript, Go, Java]
Tras eliminaciones: [JavaScript, Go, Java]
¿Contiene Go? true
Índice de JavaScript: 0
▶️ LinkedList: ideal para inserciones frecuentes
LinkedList implementa tanto List como Deque. Utiliza una lista doblemente enlazada donde cada nodo apunta al anterior y al siguiente. Su fortaleza está en las inserciones y eliminaciones en los extremos o en posiciones ya localizadas por un iterador, que se realizan en O(1). Sin embargo, el acceso por índice requiere recorrer la lista, resultando en O(n).
import java.util.LinkedList;
public class EjemploLinkedList {
public static void main(String[] args) {
LinkedList<String> tareas = new LinkedList<>();
// Operaciones de List
tareas.add("Diseñar base de datos");
tareas.add("Implementar API REST");
tareas.add("Escribir tests unitarios");
// Operaciones específicas de Deque
tareas.addFirst("Definir requisitos"); // Insertar al inicio
tareas.addLast("Desplegar en producción"); // Insertar al final
System.out.println("Tareas: " + tareas);
System.out.println("Primera: " + tareas.getFirst());
System.out.println("Última: " + tareas.getLast());
// Usar como cola (FIFO)
String siguiente = tareas.poll(); // Retira y devuelve el primero
System.out.println("Procesando: " + siguiente);
System.out.println("Restantes: " + tareas);
}
}
Tareas: [Definir requisitos, Diseñar base de datos, Implementar API REST, Escribir tests unitarios, Desplegar en producción]
Primera: Definir requisitos
Última: Desplegar en producción
Procesando: Definir requisitos
Restantes: [Diseñar base de datos, Implementar API REST, Escribir tests unitarios, Desplegar en producción]
| Operación | ArrayList | LinkedList |
|---|---|---|
Acceso por índice get(i) |
O(1) ✅ | O(n) ❌ |
Inserción al final add(e) |
O(1) amortizado | O(1) |
Inserción al inicio add(0, e) |
O(n) ❌ | O(1) ✅ |
| Eliminación por índice | O(n) | O(n) localizar + O(1) eliminar |
| Consumo de memoria | Menor (array compacto) | Mayor (nodos con punteros) |
ArrayList como opción predeterminada. Solo elige LinkedList cuando tu caso de uso implique inserciones o eliminaciones frecuentes en los extremos de la lista y rara vez necesites acceso por índice.
🔵 La interfaz Set: HashSet, TreeSet y LinkedHashSet
La interfaz Set modela el concepto matemático de conjunto: una colección que no permite elementos duplicados. Es la estructura ideal cuando necesitamos garantizar la unicidad de los datos, como un conjunto de identificadores, correos electrónicos o códigos de producto.
▶️ HashSet: máximo rendimiento
HashSet almacena los elementos en una tabla hash (internamente usa un HashMap). Ofrece operaciones de inserción, búsqueda y eliminación en O(1) amortizado, pero no garantiza ningún orden de iteración. Es la implementación más rápida y la más utilizada.
import java.util.HashSet;
import java.util.Set;
import java.util.Arrays;
public class EjemploHashSet {
public static void main(String[] args) {
// Crear un HashSet e intentar añadir duplicados
Set<String> tecnologias = new HashSet<>();
tecnologias.add("Java");
tecnologias.add("Spring");
tecnologias.add("Docker");
tecnologias.add("Java"); // Duplicado: será ignorado
tecnologias.add("Docker"); // Duplicado: será ignorado
System.out.println("Tecnologías únicas: " + tecnologias);
System.out.println("Tamaño: " + tecnologias.size()); // 3
// Operaciones de conjuntos
Set<String> backend = new HashSet<>(Arrays.asList("Java", "Spring", "PostgreSQL"));
Set<String> devops = new HashSet<>(Arrays.asList("Docker", "Kubernetes", "Java"));
// Unión
Set<String> union = new HashSet<>(backend);
union.addAll(devops);
System.out.println("Unión: " + union);
// Intersección
Set<String> interseccion = new HashSet<>(backend);
interseccion.retainAll(devops);
System.out.println("Intersección: " + interseccion);
// Diferencia
Set<String> soloBackend = new HashSet<>(backend);
soloBackend.removeAll(devops);
System.out.println("Solo en backend: " + soloBackend);
}
}
Tecnologías únicas: [Java, Docker, Spring]
Tamaño: 3
Unión: [Java, Docker, Spring, PostgreSQL, Kubernetes]
Intersección: [Java]
Solo en backend: [Spring, PostgreSQL]
▶️ TreeSet: elementos ordenados automáticamente
TreeSet mantiene los elementos ordenados de forma natural (orden alfabético para String, numérico para Integer) o según un Comparator proporcionado en el constructor. Internamente usa un árbol rojo-negro, con operaciones en O(log n). No permite null.
import java.util.TreeSet;
public class EjemploTreeSet {
public static void main(String[] args) {
TreeSet<Integer> calificaciones = new TreeSet<>();
calificaciones.add(85);
calificaciones.add(92);
calificaciones.add(78);
calificaciones.add(95);
calificaciones.add(88);
System.out.println("Ordenadas: " + calificaciones);
System.out.println("Menor: " + calificaciones.first());
System.out.println("Mayor: " + calificaciones.last());
System.out.println("Menores que 90: " + calificaciones.headSet(90));
System.out.println("Desde 85 en adelante: " + calificaciones.tailSet(85));
System.out.println("Entre 80 y 92: " + calificaciones.subSet(80, true, 92, true));
}
}
Ordenadas: [78, 85, 88, 92, 95]
Menor: 78
Mayor: 95
Menores que 90: [78, 85, 88]
Desde 85 en adelante: [85, 88, 92, 95]
Entre 80 y 92: [85, 88, 92]
▶️ LinkedHashSet: orden de inserción preservado
LinkedHashSet combina la rapidez de HashSet con una lista enlazada interna que preserva el orden de inserción. Es útil cuando necesitas unicidad pero también quieres iterar en el mismo orden en que añadiste los elementos. Su rendimiento es ligeramente inferior al de HashSet debido a la lista enlazada adicional.
| Característica | HashSet | TreeSet | LinkedHashSet |
|---|---|---|---|
| Rendimiento | O(1) ✅ | O(log n) | O(1) |
| Orden de iteración | No garantizado | Orden natural/Comparator | Orden de inserción |
| Permite null | ✅ Sí (uno) | ❌ No | ✅ Sí (uno) |
| Caso de uso | Unicidad sin orden | Unicidad + orden natural | Unicidad + orden inserción |
🗺️ La interfaz Map: HashMap, TreeMap y LinkedHashMap
La interfaz Map almacena pares clave-valor donde cada clave es única y apunta a exactamente un valor. Es el equivalente Java de los diccionarios en Python o los objetos en JavaScript. Los mapas son fundamentales para indexar datos, crear cachés, contadores de frecuencia y cualquier escenario donde necesitemos buscar valores por una clave identificadora.
▶️ HashMap: la implementación estándar
HashMap es la implementación más utilizada de Map. Almacena las entradas en una tabla hash con operaciones en O(1) amortizado. No garantiza orden de iteración. Permite una clave null y múltiples valores null.
import java.util.HashMap;
import java.util.Map;
public class Inventario {
public static void main(String[] args) {
// Clave: código producto, Valor: cantidad en stock
Map<String, Integer> stock = new HashMap<>();
// Añadir productos
stock.put("LAPTOP-001", 45);
stock.put("MOUSE-002", 200);
stock.put("TECLADO-003", 150);
stock.put("MONITOR-004", 30);
// Consultar stock
System.out.println("Stock del monitor: " + stock.get("MONITOR-004"));
// getOrDefault: evitar NullPointerException
int stockCable = stock.getOrDefault("CABLE-005", 0);
System.out.println("Stock del cable: " + stockCable);
// Actualizar stock (venta de 5 laptops)
stock.put("LAPTOP-001", stock.get("LAPTOP-001") - 5);
// Recorrer el mapa completo
System.out.println("\n--- Inventario completo ---");
for (Map.Entry<String, Integer> entrada : stock.entrySet()) {
System.out.printf("%-15s → %d unidades%n",
entrada.getKey(), entrada.getValue());
}
// Verificar existencia de clave
System.out.println("\n¿Existe LAPTOP-001? " + stock.containsKey("LAPTOP-001"));
System.out.println("¿Algún producto con 200 unidades? " + stock.containsValue(200));
// Métodos Java 8+
stock.putIfAbsent("CABLE-005", 500); // Solo inserta si no existe
stock.merge("MOUSE-002", 50, Integer::sum); // Sumar 50 al stock actual
System.out.println("\nStock del ratón actualizado: " + stock.get("MOUSE-002"));
}
}
Stock del monitor: 30
Stock del cable: 0
--- Inventario completo ---
LAPTOP-001 → 40 unidades
MOUSE-002 → 200 unidades
MONITOR-004 → 30 unidades
TECLADO-003 → 150 unidades
¿Existe LAPTOP-001? true
¿Algún producto con 200 unidades? true
Stock del ratón actualizado: 250
▶️ TreeMap y LinkedHashMap
TreeMap ordena las entradas por la clave de forma natural o mediante un Comparator, con operaciones en O(log n). LinkedHashMap mantiene el orden de inserción (o de acceso, si se configura) y resulta ideal para implementar cachés LRU (Least Recently Used). Ambas alternativas sacrifican algo de rendimiento frente a HashMap a cambio de funcionalidades adicionales.
import java.util.TreeMap;
import java.util.Map;
public class RankingPuntuaciones {
public static void main(String[] args) {
// Orden natural de las claves (alfabético)
TreeMap<String, Integer> ranking = new TreeMap<>();
ranking.put("Ana", 950);
ranking.put("Carlos", 870);
ranking.put("Elena", 920);
ranking.put("Bruno", 890);
System.out.println("Ranking alfabético: " + ranking);
System.out.println("Primer jugador: " + ranking.firstKey());
System.out.println("Último jugador: " + ranking.lastKey());
// Jugadores del rango B-D (inclusive)
Map<String, Integer> rangoBC = ranking.subMap("B", true, "D", true);
System.out.println("Jugadores B-D: " + rangoBC);
}
}
Ranking alfabético: {Ana=950, Bruno=890, Carlos=870, Elena=920}
Primer jugador: Ana
Último jugador: Elena
Jugadores B-D: {Bruno=890, Carlos=870}
📬 La interfaz Queue y Deque
Las colas modelan el principio FIFO (First In, First Out): el primer elemento que entra es el primero en salir, como una fila en una taquilla. La interfaz Queue proporciona métodos para añadir, consultar y retirar elementos del frente de la cola.
La interfaz Deque (double-ended queue) extiende Queue permitiendo operaciones en ambos extremos: se puede insertar y retirar tanto por el inicio como por el final. Esto permite usarla también como una pila (LIFO).
import java.util.PriorityQueue;
import java.util.ArrayDeque;
import java.util.Deque;
public class EjemploColas {
public static void main(String[] args) {
// PriorityQueue: ordena automáticamente por valor natural
PriorityQueue<Integer> urgencias = new PriorityQueue<>();
urgencias.add(5); // Prioridad baja
urgencias.add(1); // Prioridad alta
urgencias.add(3); // Prioridad media
System.out.println("--- Cola de prioridad ---");
while (!urgencias.isEmpty()) {
System.out.println("Procesando prioridad: " + urgencias.poll());
}
// ArrayDeque: usarla como pila (LIFO)
System.out.println("\n--- Pila con ArrayDeque ---");
Deque<String> pila = new ArrayDeque<>();
pila.push("Página 1");
pila.push("Página 2");
pila.push("Página 3");
while (!pila.isEmpty()) {
System.out.println("Volviendo a: " + pila.pop());
}
}
}
--- Cola de prioridad ---
Procesando prioridad: 1
Procesando prioridad: 3
Procesando prioridad: 5
--- Pila con ArrayDeque ---
Volviendo a: Página 3
Volviendo a: Página 2
Volviendo a: Página 1
Stack existe en Java, está considerada una clase legacy. La documentación oficial recomienda usar ArrayDeque como pila (LIFO) y como cola (FIFO), ya que es más eficiente y no tiene la sobrecarga de sincronización de Stack.
| Método | Lanza excepción | Devuelve valor especial |
|---|---|---|
| Insertar | add(e) |
offer(e) → false si no puede |
| Consultar cabeza | element() |
peek() → null si está vacía |
| Retirar cabeza | remove() |
poll() → null si está vacía |
🔄 Recorrer colecciones: Iterator, for-each y Streams
Java ofrece múltiples mecanismos para iterar sobre los elementos de una colección. Cada uno tiene su contexto óptimo de uso y conocerlos todos permite escribir código más expresivo y eficiente.
▶️ Cuatro formas de recorrer una colección
import java.util.*;
import java.util.stream.Collectors;
public class FormasDeIteracion {
public static void main(String[] args) {
List<String> ciudades = List.of("Madrid", "Barcelona", "Sevilla", "Valencia");
// 1. Bucle for clásico (solo para List, acceso por índice)
System.out.println("--- for clásico ---");
for (int i = 0; i < ciudades.size(); i++) {
System.out.println(i + ": " + ciudades.get(i));
}
// 2. For-each (para cualquier Iterable)
System.out.println("\n--- for-each ---");
for (String ciudad : ciudades) {
System.out.println(ciudad);
}
// 3. Iterator explícito (permite eliminar durante iteración)
System.out.println("\n--- Iterator ---");
List<String> copia = new ArrayList<>(ciudades);
Iterator<String> it = copia.iterator();
while (it.hasNext()) {
String ciudad = it.next();
if (ciudad.startsWith("S")) {
it.remove(); // Eliminación segura durante iteración
}
}
System.out.println("Sin ciudades con S: " + copia);
// 4. Streams (Java 8+): estilo funcional declarativo
System.out.println("\n--- Streams ---");
List<String> conM = ciudades.stream()
.filter(c -> c.startsWith("M") || c.startsWith("V"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
System.out.println("Filtradas y en mayúsculas: " + conM);
}
}
--- for clásico ---
0: Madrid
1: Barcelona
2: Sevilla
3: Valencia
--- for-each ---
Madrid
Barcelona
Sevilla
Valencia
--- Iterator ---
Sin ciudades con S: [Madrid, Barcelona, Valencia]
--- Streams ---
Filtradas y en mayúsculas: [MADRID, VALENCIA]
for-each. Si necesitas eliminar elementos durante la iteración, usa Iterator.remove() o collection.removeIf(predicado) introducido en Java 8.
⚖️ Comparativa: ¿qué colección elegir?
Elegir la colección adecuada es una de las decisiones más importantes que toma un desarrollador Java. La siguiente guía de decisión te ayudará a seleccionar la estructura correcta según tu caso de uso:
| Necesidad | Colección recomendada | Justificación |
|---|---|---|
| Lista de elementos con acceso por índice | ArrayList |
O(1) en acceso, mejor localidad de caché |
| Inserciones/eliminaciones frecuentes en extremos | LinkedList / ArrayDeque |
O(1) en extremos sin desplazamientos |
| Elementos únicos sin orden | HashSet |
O(1) y máximo rendimiento |
| Elementos únicos ordenados | TreeSet |
Mantiene orden natural o por Comparator |
| Diccionario clave → valor rápido | HashMap |
O(1) para get/put, la más usada |
| Diccionario con claves ordenadas | TreeMap |
Iteración en orden de claves |
| Cola FIFO | ArrayDeque |
Más eficiente que LinkedList para colas |
| Cola con prioridad | PriorityQueue |
Extrae siempre el menor elemento |
| Entorno concurrente (multihilo) | ConcurrentHashMap |
Thread-safe sin bloqueo global |
🛠️ La clase Collections: métodos de utilidad
La clase java.util.Collections (nótese la «s» final que la distingue de la interfaz Collection) proporciona métodos estáticos de utilidad para manipular colecciones: ordenar, mezclar, buscar, invertir, crear vistas inmutables y más.
import java.util.*;
public class EjemploCollections {
public static void main(String[] args) {
List<Integer> numeros = new ArrayList<>(Arrays.asList(34, 12, 56, 7, 89, 23));
// Ordenar
Collections.sort(numeros);
System.out.println("Ordenada: " + numeros);
// Orden inverso
Collections.sort(numeros, Collections.reverseOrder());
System.out.println("Invertida: " + numeros);
// Mezclar aleatoriamente
Collections.shuffle(numeros);
System.out.println("Mezclada: " + numeros);
// Valor máximo y mínimo
System.out.println("Máximo: " + Collections.max(numeros));
System.out.println("Mínimo: " + Collections.min(numeros));
// Frecuencia de un elemento
List<String> colores = Arrays.asList("rojo", "azul", "rojo", "verde", "rojo");
System.out.println("Frecuencia de 'rojo': " + Collections.frequency(colores, "rojo"));
// Crear lista inmutable (Java 9+)
List<String> inmutable = List.of("uno", "dos", "tres");
// inmutable.add("cuatro"); // Lanzaría UnsupportedOperationException
// Vista no modificable de una lista existente
List<Integer> soloLectura = Collections.unmodifiableList(numeros);
System.out.println("Solo lectura: " + soloLectura);
}
}
🔒 Genéricos y type safety en colecciones
Los genéricos, introducidos en Java 5, permiten parametrizar las colecciones con un tipo concreto, proporcionando seguridad de tipos en tiempo de compilación. Antes de los genéricos, todas las colecciones almacenaban Object y era necesario hacer casting manual al recuperar elementos, con el riesgo de ClassCastException en tiempo de ejecución.
// ❌ SIN genéricos (Java 1.4 y anteriores) — código inseguro
List listaRaw = new ArrayList();
listaRaw.add("texto");
listaRaw.add(42); // Compila, pero mezcla tipos
String s = (String) listaRaw.get(1); // ClassCastException en tiempo de ejecución
// ✅ CON genéricos (Java 5+) — el compilador previene errores
List<String> listaSegura = new ArrayList<>();
listaSegura.add("texto");
// listaSegura.add(42); // ¡ERROR DE COMPILACIÓN! No compila.
String s2 = listaSegura.get(0); // No necesita casting
<>): Desde Java 7, no es necesario repetir el tipo en el constructor. Basta con escribir new ArrayList<>() en lugar de new ArrayList<String>(). El compilador infiere el tipo automáticamente del lado izquierdo de la asignación.
Una práctica recomendada es programar contra la interfaz, no contra la implementación. Esto significa declarar variables con el tipo de la interfaz y asignar una implementación concreta:
// ✅ Buena práctica: tipo interfaz a la izquierda
List<String> nombres = new ArrayList<>();
Set<Integer> ids = new HashSet<>();
Map<String, Double> precios = new HashMap<>();
// ❌ Mala práctica: tipo concreto a la izquierda (acopla el código)
ArrayList<String> nombres2 = new ArrayList<>();
📖 Ejemplo integrador: sistema de gestión de biblioteca
Este ejemplo combina múltiples colecciones en un escenario realista: un sistema de gestión de biblioteca que usa ArrayList para almacenar libros, HashMap para indexar por ISBN, TreeSet para mantener géneros ordenados y LinkedList como cola de reservas.
import java.util.*;
import java.util.stream.Collectors;
class Libro {
private String isbn;
private String titulo;
private String autor;
private String genero;
private int anio;
private boolean disponible;
public Libro(String isbn, String titulo, String autor, String genero, int anio) {
this.isbn = isbn;
this.titulo = titulo;
this.autor = autor;
this.genero = genero;
this.anio = anio;
this.disponible = true;
}
// Getters
public String getIsbn() { return isbn; }
public String getTitulo() { return titulo; }
public String getAutor() { return autor; }
public String getGenero() { return genero; }
public int getAnio() { return anio; }
public boolean isDisponible() { return disponible; }
public void setDisponible(boolean disponible) { this.disponible = disponible; }
@Override
public String toString() {
return String.format("«%s» de %s (%d) [%s]",
titulo, autor, anio, disponible ? "Disponible" : "Prestado");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Libro)) return false;
return isbn.equals(((Libro) o).isbn);
}
@Override
public int hashCode() { return isbn.hashCode(); }
}
public class SistemaBiblioteca {
// Catálogo completo (List para mantener orden de registro)
private List<Libro> catalogo = new ArrayList<>();
// Índice rápido por ISBN (Map para búsqueda O(1))
private Map<String, Libro> indicePorIsbn = new HashMap<>();
// Géneros disponibles (TreeSet para orden alfabético sin duplicados)
private Set<String> generos = new TreeSet<>();
// Cola de reservas (LinkedList como Queue FIFO)
private Queue<String> colaReservas = new LinkedList<>();
public void agregarLibro(Libro libro) {
catalogo.add(libro);
indicePorIsbn.put(libro.getIsbn(), libro);
generos.add(libro.getGenero());
System.out.println("✅ Añadido: " + libro);
}
public Libro buscarPorIsbn(String isbn) {
return indicePorIsbn.get(isbn);
}
public List<Libro> buscarPorGenero(String genero) {
return catalogo.stream()
.filter(l -> l.getGenero().equalsIgnoreCase(genero))
.collect(Collectors.toList());
}
public void prestarLibro(String isbn) {
Libro libro = indicePorIsbn.get(isbn);
if (libro != null && libro.isDisponible()) {
libro.setDisponible(false);
System.out.println("📕 Prestado: " + libro.getTitulo());
} else if (libro != null) {
colaReservas.add(isbn);
System.out.println("⏳ En cola de reservas: " + libro.getTitulo());
} else {
System.out.println("❌ ISBN no encontrado: " + isbn);
}
}
public void mostrarEstadisticas() {
long disponibles = catalogo.stream().filter(Libro::isDisponible).count();
System.out.println("\n📊 --- Estadísticas de la biblioteca ---");
System.out.println("Total de libros: " + catalogo.size());
System.out.println("Disponibles: " + disponibles);
System.out.println("Prestados: " + (catalogo.size() - disponibles));
System.out.println("Géneros: " + generos);
System.out.println("Reservas pendientes: " + colaReservas.size());
}
public static void main(String[] args) {
SistemaBiblioteca biblioteca = new SistemaBiblioteca();
// Registrar libros
biblioteca.agregarLibro(new Libro("978-0-13-468599-1",
"Effective Java", "Joshua Bloch", "Programación", 2018));
biblioteca.agregarLibro(new Libro("978-0-596-00712-6",
"Head First Design Patterns", "Eric Freeman", "Programación", 2020));
biblioteca.agregarLibro(new Libro("978-84-376-0494-7",
"Don Quijote de la Mancha", "Miguel de Cervantes", "Literatura", 1605));
biblioteca.agregarLibro(new Libro("978-0-06-112008-4",
"To Kill a Mockingbird", "Harper Lee", "Literatura", 1960));
// Buscar y prestar
Libro encontrado = biblioteca.buscarPorIsbn("978-0-13-468599-1");
System.out.println("\nBúsqueda por ISBN: " + encontrado);
biblioteca.prestarLibro("978-0-13-468599-1");
biblioteca.prestarLibro("978-0-13-468599-1"); // Ya prestado → cola
// Buscar por género
System.out.println("\nLibros de Programación:");
biblioteca.buscarPorGenero("Programación").forEach(l ->
System.out.println(" · " + l));
// Estadísticas
biblioteca.mostrarEstadisticas();
}
}
✅ Añadido: «Effective Java» de Joshua Bloch (2018) [Disponible]
✅ Añadido: «Head First Design Patterns» de Eric Freeman (2020) [Disponible]
✅ Añadido: «Don Quijote de la Mancha» de Miguel de Cervantes (1605) [Disponible]
✅ Añadido: «To Kill a Mockingbird» de Harper Lee (1960) [Disponible]
Búsqueda por ISBN: «Effective Java» de Joshua Bloch (2018) [Disponible]
📕 Prestado: Effective Java
⏳ En cola de reservas: Effective Java
Libros de Programación:
· «Effective Java» de Joshua Bloch (2018) [Prestado]
· «Head First Design Patterns» de Eric Freeman (2020) [Disponible]
📊 --- Estadísticas de la biblioteca ---
Total de libros: 4
Disponibles: 3
Prestados: 1
Géneros: [Literatura, Programación]
Reservas pendientes: 1
✏️ Ejercicios prácticos resueltos
🏋️ Ejercicio 1: Contador de frecuencia de palabras
Escribe un programa que reciba una frase y cuente cuántas veces aparece cada palabra, usando un HashMap. Las palabras deben convertirse a minúsculas para que la comparación sea insensible a mayúsculas.
Entrada de ejemplo: "Java es genial y Java es potente y Java es libre"
Salida esperada: {java=3, es=3, genial=1, y=2, potente=1, libre=1}
Ver solución
import java.util.HashMap;
import java.util.Map;
public class ContadorPalabras {
public static void main(String[] args) {
String frase = "Java es genial y Java es potente y Java es libre";
String[] palabras = frase.toLowerCase().split("\\s+");
Map<String, Integer> frecuencia = new HashMap<>();
for (String palabra : palabras) {
frecuencia.merge(palabra, 1, Integer::sum);
}
System.out.println(frecuencia);
// Alternativa con Streams
// Map<String, Long> freq = Arrays.stream(palabras)
// .collect(Collectors.groupingBy(p -> p, Collectors.counting()));
}
}
🏋️ Ejercicio 2: Eliminar duplicados preservando orden
Dada una lista de números enteros con duplicados, escribe un programa que devuelva una nueva lista sin duplicados pero conservando el orden original de aparición. Usa la colección más apropiada.
Entrada: [4, 2, 7, 2, 4, 9, 7, 1, 4]
Salida esperada: [4, 2, 7, 9, 1]
Ver solución
import java.util.*;
public class EliminarDuplicados {
public static void main(String[] args) {
List<Integer> original = Arrays.asList(4, 2, 7, 2, 4, 9, 7, 1, 4);
// LinkedHashSet: elimina duplicados y preserva orden de inserción
Set<Integer> sinDuplicados = new LinkedHashSet<>(original);
List<Integer> resultado = new ArrayList<>(sinDuplicados);
System.out.println("Original: " + original);
System.out.println("Sin duplicados: " + resultado);
}
}
🏋️ Ejercicio 3: Agenda de contactos con TreeMap
Implementa una agenda de contactos que almacene nombres (clave) y números de teléfono (valor), mantenga los contactos ordenados alfabéticamente y permita buscar por prefijo (por ejemplo, todos los contactos cuyo nombre empiece por «M»).
Ver solución
import java.util.TreeMap;
import java.util.Map;
public class AgendaContactos {
public static void main(String[] args) {
TreeMap<String, String> agenda = new TreeMap<>();
agenda.put("Ana García", "+34 612 345 678");
agenda.put("Miguel López", "+34 698 765 432");
agenda.put("María Fernández", "+34 655 111 222");
agenda.put("Carlos Ruiz", "+34 677 333 444");
agenda.put("Marta Sánchez", "+34 633 555 666");
// Mostrar agenda ordenada
System.out.println("--- Agenda completa ---");
agenda.forEach((nombre, tel) ->
System.out.printf("%-20s %s%n", nombre, tel));
// Buscar por prefijo "M" (desde "M" hasta "N" exclusivo)
System.out.println("\n--- Contactos con M ---");
Map<String, String> conM = agenda.subMap("M", "N");
conM.forEach((nombre, tel) ->
System.out.printf("%-20s %s%n", nombre, tel));
}
}
🏋️ Ejercicio 4: Sistema de votación con colecciones
Simula un sistema de votación donde se registran votos para diferentes candidatos. Usa un HashMap para contar los votos y un TreeMap para mostrar los resultados ordenados por nombre. Finalmente, determina el ganador usando Streams.
Ver solución
import java.util.*;
public class SistemaVotacion {
public static void main(String[] args) {
String[] votos = {"Ana", "Carlos", "Ana", "Elena",
"Carlos", "Ana", "Elena", "Carlos",
"Ana", "Elena", "Elena", "Elena"};
// Contar votos con HashMap
Map<String, Integer> conteo = new HashMap<>();
for (String voto : votos) {
conteo.merge(voto, 1, Integer::sum);
}
// Mostrar resultados ordenados por nombre (TreeMap)
TreeMap<String, Integer> resultados = new TreeMap<>(conteo);
System.out.println("--- Resultados ---");
resultados.forEach((candidato, numVotos) ->
System.out.printf("%-10s → %d votos%n", candidato, numVotos));
// Determinar ganador con Streams
Map.Entry<String, Integer> ganador = conteo.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElseThrow();
System.out.printf("%n🏆 Ganador: %s con %d votos%n",
ganador.getKey(), ganador.getValue());
}
}
⚠️ Errores frecuentes con colecciones en Java
Incluso los programadores experimentados cometen errores al trabajar con colecciones. Conocerlos te ahorrará horas de depuración y bugs difíciles de rastrear.
❌ Error 1: Modificar una colección durante un for-each
// ❌ INCORRECTO: modificar la lista dentro de un for-each
List<String> nombres = new ArrayList<>(Arrays.asList("Ana", "Bob", "Carlos"));
for (String nombre : nombres) {
if (nombre.startsWith("B")) {
nombres.remove(nombre); // ¡ConcurrentModificationException!
}
}
// ✅ CORRECTO: usar removeIf (Java 8+)
nombres.removeIf(nombre -> nombre.startsWith("B"));
// ✅ CORRECTO: usar Iterator.remove()
Iterator<String> it = nombres.iterator();
while (it.hasNext()) {
if (it.next().startsWith("B")) {
it.remove();
}
}
❌ Error 2: No implementar hashCode() y equals() para objetos en HashSet/HashMap
// ❌ Clase sin hashCode/equals: HashSet no detecta duplicados
class Producto {
String codigo;
Producto(String codigo) { this.codigo = codigo; }
}
Set<Producto> set = new HashSet<>();
set.add(new Producto("A001"));
set.add(new Producto("A001"));
System.out.println(set.size()); // Imprime 2, ¡debería ser 1!
// ✅ Solución: implementar equals() y hashCode()
class ProductoCorregido {
String codigo;
ProductoCorregido(String codigo) { this.codigo = codigo; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ProductoCorregido)) return false;
return codigo.equals(((ProductoCorregido) o).codigo);
}
@Override
public int hashCode() { return codigo.hashCode(); }
}
❌ Error 3: Usar el tipo concreto en lugar de la interfaz
// ❌ INCORRECTO: declarar con el tipo concreto acopla el código
ArrayList<String> lista = new ArrayList<>();
HashMap<String, Integer> mapa = new HashMap<>();
// ✅ CORRECTO: programar contra la interfaz permite cambiar la implementación
List<String> lista = new ArrayList<>(); // Fácil cambiar a LinkedList
Map<String, Integer> mapa = new HashMap<>(); // Fácil cambiar a TreeMap
❌ Error 4: Confundir Collections.sort() con elementos no comparables
// ❌ INCORRECTO: intentar ordenar objetos que no implementan Comparable
class Empleado {
String nombre;
int edad;
Empleado(String nombre, int edad) { this.nombre = nombre; this.edad = edad; }
}
List<Empleado> empleados = new ArrayList<>();
empleados.add(new Empleado("Ana", 30));
empleados.add(new Empleado("Carlos", 25));
Collections.sort(empleados); // ¡ClassCastException! Empleado no es Comparable
// ✅ CORRECTO: proporcionar un Comparator
Collections.sort(empleados, (e1, e2) -> e1.nombre.compareTo(e2.nombre));
// ✅ O bien: implementar Comparable en la clase
class EmpleadoComparable implements Comparable<EmpleadoComparable> {
String nombre;
int edad;
EmpleadoComparable(String nombre, int edad) { this.nombre = nombre; this.edad = edad; }
@Override
public int compareTo(EmpleadoComparable otro) {
return this.nombre.compareTo(otro.nombre);
}
}
HashMap/HashSet, implementa siempre hashCode() y equals(). Y antes de ordenarla, asegúrate de que implementa Comparable o proporciona un Comparator.
📌 Artículos relacionados
❓ Preguntas frecuentes sobre Colecciones en Java: ArrayList, HashMap, Set y más
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Colecciones en Java: ArrayList, HashMap, Set y más? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!