Capítulo 7: Colecciones Java — Guía de entrevista
Este capítulo cubre todo lo que necesita saber sobre las Colecciones Java para una entrevista: cuándo usar cada una, las diferencias entre List.of() y new ArrayList<>(), trampas comunes y preguntas clásicas.
1. La jerarquía de las Colecciones
Iterable
│
Collection
╱ │ ╲
List Set Queue
╱ │ │ ╲ │
ArrayList │ HashSet │ LinkedList
LinkedList │ TreeSet │ PriorityQueue
│ LinkedHashSet
Vector
(legacy)
Map (¡no es una Collection!)
╱ │ ╲
HashMap TreeMap LinkedHashMap
│
ConcurrentHashMap
2. List.of() vs new ArrayList — Mutable vs Inmutable
La diferencia fundamental
| Creación | Tipo | ¿Modificable? | ¿Acepta null? |
|---|---|---|---|
List.of("a", "b") | Lista inmutable | No | No |
new ArrayList<>(List.of("a", "b")) | Lista modificable | Sí | Sí |
Collections.unmodifiableList(list) | Vista inmutable | No (pero la original sí) | Sí |
List.copyOf(list) | Copia inmutable | No | No |
Ejemplos concretos
// INMUTABLE — NO se puede modificar
List<String> immutable = List.of("a", "b", "c");
immutable.add("d"); // 💥 UnsupportedOperationException
immutable.remove(0); // 💥 UnsupportedOperationException
immutable.set(0, "x"); // 💥 UnsupportedOperationException
// MODIFICABLE — se puede modificar
List<String> mutable = new ArrayList<>();
mutable.add("a"); // ✅ OK
mutable.add("b"); // ✅ OK
mutable.remove(0); // ✅ OK
mutable.set(0, "x"); // ✅ OK
// Crear una lista modificable a partir de una inmutable
List<String> mutable2 = new ArrayList<>(List.of("a", "b", "c"));
mutable2.add("d"); // ✅ OK
Map.of() y Set.of() — mismo principio
// Map inmutable
Map<String, Integer> map = Map.of("a", 1, "b", 2);
map.put("c", 3); // 💥 UnsupportedOperationException
// Map modificable
Map<String, Integer> map2 = new HashMap<>(Map.of("a", 1, "b", 2));
map2.put("c", 3); // ✅ OK
// Set inmutable
Set<String> set = Set.of("a", "b", "c");
set.add("d"); // 💥 UnsupportedOperationException
// Set modificable
Set<String> set2 = new HashSet<>(Set.of("a", "b", "c"));
set2.add("d"); // ✅ OK
Collections.emptyList() vs List.of()
// Ambos devuelven una lista vacía inmutable
List<String> empty1 = Collections.emptyList(); // Java 5+
List<String> empty2 = List.of(); // Java 9+ (preferido)
// Ambos son equivalentes, List.of() es más moderno
¿Cuándo usar cada uno?
| Situación | Usar | Por qué |
|---|---|---|
| Constantes, configuraciones | List.of() | Inmutable, thread-safe |
| Construir una lista progresivamente | new ArrayList<>() | Necesita agregar elementos |
| Retorno de método (caso vacío) | List.of() o Collections.emptyList() | Nunca retornar null |
| DTO / Records | List.copyOf() | Copia defensiva, garantiza inmutabilidad |
| Pruebas unitarias | List.of() | Conciso y claro |
3. ArrayList vs LinkedList
ArrayList (arreglo dinámico) :
┌───┬───┬───┬───┬───┬───┬───┬ ┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │...│ ← acceso directo por índice
└───┴───┴───┴───┴───┴───┴───┴ ┘
LinkedList (lista doblemente enlazada) :
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
| Operación | ArrayList | LinkedList |
|---|---|---|
get(index) | O(1) | O(n) |
add(element) (final) | O(1) amortizado | O(1) |
add(index, element) (medio) | O(n) | O(1) si se tiene el nodo |
remove(index) | O(n) | O(1) si se tiene el nodo |
| Memoria | Compacta | Más pesada (punteros) |
| Caché CPU | Buena (elementos contiguos) | Mala (elementos dispersos) |
Utilice ArrayList en el 99% de los casos. LinkedList rara vez es más rápido en la práctica debido a la localidad de caché de la CPU. Incluso para inserciones frecuentes, ArrayList suele ser más rápido.
"Casi siempre uso ArrayList. LinkedList tiene mejor complejidad teórica para inserciones, pero ArrayList gana en la práctica debido a la localidad de caché de la CPU. LinkedList es útil como una Queue (FIFO) a través de la interfaz Deque."
4. HashMap — ¿Cómo funciona?
Estructura interna
HashMap (Java 8+) :
┌──────────────────────────────┐
│ Buckets (arreglo) │
├──────┬───────────────────────┤
│ [0] │ null │
│ [1] │ → Entry(key1, val1) │
│ [2] │ null │
│ [3] │ → Entry(key2, val2) → Entry(key3, val3) ← ¡colisión!
│ [4] │ null │
│ ... │ │
│ [n] │ → Entry(keyN, valN) │
└──────┴───────────────────────┘
Si una lista enlazada en un bucket supera 8 elementos → se convierte en un árbol rojo-negro (O(log n))
¿Cómo funciona?
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);
// Paso 1: hashCode("Alice") → 63650026
// Paso 2: 63650026 % tamaño_arreglo → índice del bucket
// Paso 3: Almacena Entry("Alice", 25) en ese bucket
Colisión = mismo bucket
// Si dos claves tienen el mismo índice de bucket:
map.put("Alice", 25); // bucket 3
map.put("Bob", 30); // también bucket 3 (colisión)
// Java crea una lista enlazada en el bucket 3:
// bucket[3] → Entry("Alice", 25) → Entry("Bob", 30)
// Para encontrar "Bob", Java recorre la lista y usa equals()
Complejidades
| Operación | Caso promedio | Peor caso (muchas colisiones) |
|---|---|---|
put() | O(1) | O(log n) (árbol desde Java 8) |
get() | O(1) | O(log n) |
remove() | O(1) | O(log n) |
containsKey() | O(1) | O(log n) |
containsValue() | O(n) | O(n) |
5. HashMap vs TreeMap vs LinkedHashMap
| Map | Orden | Complejidad | Cuándo usarlo |
|---|---|---|---|
| HashMap | Sin orden garantizado | O(1) | Por defecto, el más rápido |
| TreeMap | Ordenado por clave (natural o Comparator) | O(log n) | Necesita claves ordenadas |
| LinkedHashMap | Orden de inserción | O(1) | Necesita el orden de inserción |
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("banana", 2);
hashMap.put("apple", 1);
hashMap.put("cherry", 3);
// Iteración: orden arbitrario
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("banana", 2);
treeMap.put("apple", 1);
treeMap.put("cherry", 3);
// Iteración: apple → banana → cherry (alfabético)
Map<String, Integer> linkedMap = new LinkedHashMap<>();
linkedMap.put("banana", 2);
linkedMap.put("apple", 1);
linkedMap.put("cherry", 3);
// Iteración: banana → apple → cherry (orden de inserción)
6. HashSet vs TreeSet vs LinkedHashSet
| Set | Orden | Duplicados | Complejidad |
|---|---|---|---|
| HashSet | Ninguno | No | O(1) |
| TreeSet | Ordenado | No | O(log n) |
| LinkedHashSet | Orden de inserción | No | O(1) |
// HashSet = el más usado
Set<String> unique = new HashSet<>(List.of("b", "a", "c", "a"));
// → [a, b, c] (sin duplicados, orden arbitrario)
// TreeSet = ordenado automáticamente
Set<String> sorted = new TreeSet<>(List.of("banana", "apple", "cherry"));
// → [apple, banana, cherry]
HashSet utiliza hashCode() y equals() para detectar duplicados. Si sus objetos personalizados no implementan estos métodos, el Set no funcionará correctamente.
7. Queue y Deque
// Queue (FIFO: First In, First Out) — cola
Queue<String> queue = new LinkedList<>();
queue.offer("primero"); // agregar al final
queue.offer("segundo");
queue.offer("tercero");
queue.poll(); // quitar del inicio → "primero"
// Deque (Double-Ended Queue) — pila o cola
Deque<String> deque = new ArrayDeque<>();
// Como Stack (LIFO):
deque.push("a");
deque.push("b");
deque.pop(); // → "b" (último en entrar, primero en salir)
// Como Queue (FIFO):
deque.offerLast("a");
deque.offerLast("b");
deque.pollFirst(); // → "a" (primero en entrar, primero en salir)
java.util.Stack es una clase legacy (Java 1.0) innecesariamente sincronizada. Utilice ArrayDeque en su lugar.
// ❌ Legacy
Stack<String> stack = new Stack<>();
// ✅ Moderno
Deque<String> stack = new ArrayDeque<>();
8. Iteración sobre las Colecciones
Las 5 formas de iterar
List<String> names = List.of("Alice", "Bob", "Charlie");
// 1. For-each (la más común)
for (String name : names) {
System.out.println(name);
}
// 2. Stream API (funcional)
names.stream()
.filter(n -> n.startsWith("A"))
.forEach(System.out::println);
// 3. forEach con lambda
names.forEach(name -> System.out.println(name));
// 4. Iterator (cuando necesita eliminar mientras itera)
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.equals("Bob")) it.remove();
}
// 5. For clásico con índice (solo ArrayList)
for (int i = 0; i < names.size(); i++) {
System.out.println(names.get(i));
}
Trampa: ConcurrentModificationException
// ❌ ERROR — modificar una lista durante un for-each
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s); // 💥 ConcurrentModificationException
}
// ✅ Solución 1: Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove(); // OK
}
// ✅ Solución 2: removeIf (Java 8+)
list.removeIf(s -> s.equals("b")); // OK, más conciso
// ✅ Solución 3: Stream + filter (crea una nueva lista)
List<String> filtered = list.stream()
.filter(s -> !s.equals("b"))
.toList();
"No se puede modificar una colección mientras se itera con for-each. Utilizo Iterator.remove(), removeIf() o streams para eliminar elementos de forma segura durante la iteración."
9. Colecciones thread-safe
| Colección | ¿Thread-safe? | Alternativa thread-safe |
|---|---|---|
ArrayList | No | CopyOnWriteArrayList o Collections.synchronizedList() |
HashMap | No | ConcurrentHashMap |
HashSet | No | ConcurrentHashMap.newKeySet() |
LinkedList | No | ConcurrentLinkedQueue |
// ❌ HashMap NO es thread-safe
Map<String, Integer> map = new HashMap<>();
// ✅ ConcurrentHashMap es thread-safe SIN sincronización global
Map<String, Integer> map = new ConcurrentHashMap<>();
// ❌ Hashtable es thread-safe pero LEGACY y lento (sincronizado en todo)
Map<String, Integer> map = new Hashtable<>(); // NO USAR
"Para acceso concurrente, uso ConcurrentHashMap en lugar de HashMap — utiliza lock striping para mejor rendimiento que el Hashtable sincronizado legacy."
10. Stream API — Resumen rápido
List<User> users = getUsers();
// Filtrar
List<User> active = users.stream()
.filter(User::isActive)
.toList();
// Transformar
List<String> emails = users.stream()
.map(User::getEmail)
.toList();
// Ordenar
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getName))
.toList();
// Agrupar
Map<String, List<User>> byRole = users.stream()
.collect(Collectors.groupingBy(User::getRole));
// Reducir (suma, max, min)
double totalSalary = users.stream()
.mapToDouble(User::getSalary)
.sum();
// Verificar
boolean anyAdmin = users.stream().anyMatch(u -> "ADMIN".equals(u.getRole()));
boolean allActive = users.stream().allMatch(User::isActive);
boolean noneBlocked = users.stream().noneMatch(User::isBlocked);
Las operaciones
| Tipo | Ejemplos | Retorna |
|---|---|---|
| Intermedias (lazy) | filter, map, sorted, distinct, limit, skip | Stream |
| Terminales (ejecutan) | toList, collect, forEach, count, sum, reduce, anyMatch | Resultado |
"Los streams son lazy — las operaciones intermedias como filter y map no se ejecutan hasta que se llama a una operación terminal como toList(). Uso streams para procesamiento de datos declarativo y legible."
Tabla resumen — ¿Qué colección usar?
| Necesidad | Colección | Por qué |
|---|---|---|
| Lista ordenada, acceso por índice | ArrayList | O(1) para get/set |
| Sin duplicados | HashSet | O(1) para add/contains |
| Sin duplicados + ordenado | TreeSet | O(log n), ordenado |
| Clave → Valor | HashMap | O(1) para put/get |
| Clave → Valor, ordenado por clave | TreeMap | O(log n), ordenado |
| FIFO (cola) | ArrayDeque | Más rápido que LinkedList |
| LIFO (pila) | ArrayDeque | Reemplaza Stack (legacy) |
| Map thread-safe | ConcurrentHashMap | Lock striping |
| Lista inmutable | List.of() | Thread-safe, compacta |