Saltar al contenido principal

Capítulo 7: Colecciones Java — Guía de entrevista

Objetivo

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ónTipo¿Modificable?¿Acepta null?
List.of("a", "b")Lista inmutableNoNo
new ArrayList<>(List.of("a", "b"))Lista modificableSíSí
Collections.unmodifiableList(list)Vista inmutableNo (pero la original sí)Sí
List.copyOf(list)Copia inmutableNoNo

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ónUsarPor qué
Constantes, configuracionesList.of()Inmutable, thread-safe
Construir una lista progresivamentenew ArrayList<>()Necesita agregar elementos
Retorno de método (caso vacío)List.of() o Collections.emptyList()Nunca retornar null
DTO / RecordsList.copyOf()Copia defensiva, garantiza inmutabilidad
Pruebas unitariasList.of()Conciso y claro
Frase de entrevista
"List.of() creates an immutable list — it's compact, null-safe, and thread-safe. new ArrayList<>() creates a mutable list that I can modify. I use List.of() for constants and return values, ArrayList for building collections."

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ónArrayListLinkedList
get(index)O(1)O(n)
add(element) (final)O(1) amortizadoO(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
MemoriaCompactaMás pesada (punteros)
Caché CPUBuena (elementos contiguos)Mala (elementos dispersos)
En la práctica

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.

Frase de entrevista

"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ónCaso promedioPeor 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​

MapOrdenComplejidadCuándo usarlo
HashMapSin orden garantizadoO(1)Por defecto, el más rápido
TreeMapOrdenado por clave (natural o Comparator)O(log n)Necesita claves ordenadas
LinkedHashMapOrden de inserciónO(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​

SetOrdenDuplicadosComplejidad
HashSetNingunoNoO(1)
TreeSetOrdenadoNoO(log n)
LinkedHashSetOrden de inserciónNoO(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]
Recordatorio

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)
No usar Stack

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();
Frase de entrevista

"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
ArrayListNoCopyOnWriteArrayList o Collections.synchronizedList()
HashMapNoConcurrentHashMap
HashSetNoConcurrentHashMap.newKeySet()
LinkedListNoConcurrentLinkedQueue
// ❌ 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
Frase de entrevista

"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​

TipoEjemplosRetorna
Intermedias (lazy)filter, map, sorted, distinct, limit, skipStream
Terminales (ejecutan)toList, collect, forEach, count, sum, reduce, anyMatchResultado
Frase de entrevista

"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?​

NecesidadColecciónPor qué
Lista ordenada, acceso por índiceArrayListO(1) para get/set
Sin duplicadosHashSetO(1) para add/contains
Sin duplicados + ordenadoTreeSetO(log n), ordenado
Clave → ValorHashMapO(1) para put/get
Clave → Valor, ordenado por claveTreeMapO(log n), ordenado
FIFO (cola)ArrayDequeMás rápido que LinkedList
LIFO (pila)ArrayDequeReemplaza Stack (legacy)
Map thread-safeConcurrentHashMapLock striping
Lista inmutableList.of()Thread-safe, compacta

⬅️ Capítulo 6: Trampas Java | ⬅️ Volver al Módulo 18