Skip to content

Top 50 Preguntas Frecuentes de Java

Guía completa con las preguntas que aparecen en la mayoría de entrevistas técnicas de nivel intermedio-avanzado.


Definición profesional: Tres componentes del ecosistema Java con responsabilidades distintas: JVM ejecuta bytecode, JRE añade las bibliotecas estándar, JDK añade las herramientas de desarrollo.

ComponenteDescripción¿Para quién?
JVMJava Virtual Machine - Ejecuta bytecodeInterno
JREJVM + bibliotecas estándarUsuarios finales
JDKJRE + compilador javac + herramientasDesarrolladores

Relación: JDK ⊇ JRE ⊇ JVM.

Cómo responder en entrevista:

“JVM ejecuta el bytecode en cualquier plataforma. JRE incluye la JVM más las librerías estándar para ejecutar apps. JDK incluye el JRE más el compilador javac y herramientas de desarrollo. La relación es JDK ⊇ JRE ⊇ JVM.”


Definición profesional: Código intermedio (archivos .class) generado por javac. La JVM lo interpreta y el JIT lo compila a código nativo en tiempo de ejecución para optimizar partes frecuentes.

Flujo de bytecode
// Código.java → javac → Código.class (bytecode) → JVM → JIT → Código nativo
// Permite Write Once, Run Anywhere (WORA)
// javap -c MiClase.class ← ver el bytecode

Cómo responder en entrevista:

“El bytecode es independiente de la plataforma. La JVM lo interpreta y el JIT compiler optimiza las partes más ejecutadas compilándolas a código nativo del CPU. Esto logra el balance entre portabilidad y rendimiento.”


3. ¿Cuáles son los tipos de datos primitivos?

Section titled “3. ¿Cuáles son los tipos de datos primitivos?”

¿Qué es? Java tiene 8 tipos de datos primitivos (no son objetos).

Tipos enteros:

  • byte: 8 bits (-128 a 127)
  • short: 16 bits
  • int: 32 bits (más usado)
  • long: 64 bits (sufijo L)

Tipos decimales:

  • float: 32 bits (sufijo f)
  • double: 64 bits (más usado)

Otros:

  • char: 16 bits (Unicode)
  • boolean: true/false

Ejemplo práctico:

8 tipos primitivos
// 8 tipos primitivos
byte b = 127; // 8 bits, -128 a 127
short s = 32767; // 16 bits
int i = 2147483647; // 32 bits
long l = 9223372036854775807L; // 64 bits
float f = 3.14f; // 32 bits
double d = 3.14159265; // 64 bits
char c = 'A'; // 16 bits (Unicode)
boolean bool = true; // true/false

Cómo responder en entrevista:

“Java tiene 8 primitivos: byte, short, int y long para enteros; float y double para decimales; char para Unicode y boolean. Se almacenan en el stack directamente, no son objetos. Para usarlos en colecciones existen sus Wrappers: Integer, Double, etc., que se convierten automáticamente con autoboxing.”


¿Qué es? Conversión automática entre tipos primitivos y sus clases Wrapper.

Definiciones:

  • Autoboxing: primitivo → Wrapper (intInteger)
  • Unboxing: Wrapper → primitivo (Integerint)

¿Para qué sirve?

  • Usar primitivos en colecciones (List<Integer>)
  • Simplificar código (conversión automática)

Cuidado: Unboxing de null lanza NullPointerException.

Ejemplo práctico:

Autoboxing y Unboxing
// Autoboxing: primitivo → Wrapper
int primitivo = 10;
Integer wrapper = primitivo; // Autoboxing
// Unboxing: Wrapper → primitivo
Integer wrapper2 = Integer.valueOf(20);
int primitivo2 = wrapper2; // Unboxing
// Cuidado con null
Integer nulo = null;
int valor = nulo; // NullPointerException!

Cómo responder en entrevista:

“Autoboxing convierte int a Integer automáticamente; unboxing hace la inversa. El compilador lo inserta. El peligro es hacer unboxing de null, que lanza NullPointerException en runtime. También hay un impacto de rendimiento al usar Wrappers en bucles intensivos.”


Definición profesional: Operador condicional condicion ? valorSiTrue : valorSiFalse que reemplaza if-else de asignación en una sola expresión.

Operador ternario
int edad = 20;
String mensaje = (edad >= 18) ? "Mayor de edad" : "Menor de edad";
// Equivalente a:
String mensaje2;
if (edad >= 18) {
mensaje2 = "Mayor de edad";
} else {
mensaje2 = "Menor de edad";
}

Cómo responder en entrevista:

“El operador ternario es azúcar sintáctica para un if-else de asignación. Lo uso cuando el código queda más legible en una línea. Para lógica compleja prefiero if-else tradicional para mantener la legibilidad y facilitar el debugging.”


¿Qué es? Método especial que se ejecuta al crear un objeto con new.

Características:

  • Mismo nombre que la clase
  • Sin tipo de retorno (ni siquiera void)
  • Puede sobrecargarse (múltiples constructores)
  • Si no defines ninguno, Java crea uno por defecto sin parámetros

Tipos comunes:

  • Por defecto: Sin parámetros
  • Parametrizado: Con parámetros para inicializar
  • Copia: Recibe otro objeto del mismo tipo

Ejemplo práctico:

Constructores en Java
public class Persona {
private String nombre;
private int edad;
// Constructor por defecto
public Persona() {
this.nombre = "Sin nombre";
this.edad = 0;
}
// Constructor con parámetros
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
// Constructor copia
public Persona(Persona otra) {
this.nombre = otra.nombre;
this.edad = otra.edad;
}
}
// Uso
Persona p1 = new Persona();
Persona p2 = new Persona("Juan", 25);
Persona p3 = new Persona(p2);

Cómo responder en entrevista:

“Un constructor inicializa el objeto al crearlo. Tiene el mismo nombre de la clase y sin tipo de retorno. Se pueden sobrecargar para distintas formas de creación. Si no defines ninguno, Java provee uno por defecto sin parámetros. Con this() se puede delegar a otro constructor de la misma clase.”


¿Qué es? Palabras clave para referenciar al objeto actual y a la clase padre.

this:

  • Referencia al objeto actual
  • Distinguir entre parámetros y atributos con mismo nombre
  • Llamar a otro constructor de la misma clase: this()

super:

  • Referencia a la clase padre
  • Acceder a miembros del padre ocultos por el hijo
  • Llamar al constructor del padre: super()

Ejemplo práctico:

this y super
public class Animal {
protected String nombre;
public Animal(String nombre) {
this.nombre = nombre; // this = referencia al objeto actual
}
}
public class Perro extends Animal {
private String raza;
public Perro(String nombre, String raza) {
super(nombre); // super = referencia a la clase padre
this.raza = raza;
}
public void mostrar() {
System.out.println(super.nombre); // Acceder a miembro del padre
}
}

Cómo responder en entrevista:

this referencia al objeto actual y se usa para distinguir campos de parámetros o llamar a otro constructor con this(). super referencia a la clase padre y se usa para llamar a su constructor con super() o acceder a sus métodos sobrescritos. super() debe ser siempre la primera línea del constructor hijo.”


8. ¿Qué es sobrecarga (overloading) vs sobrescritura (overriding)?

Section titled “8. ¿Qué es sobrecarga (overloading) vs sobrescritura (overriding)?”

¿Qué es? Dos formas de polimorfismo en Java.

Diferencias:

Sobrecarga (Overloading)Sobrescritura (Overriding)
ClaseMisma claseClase hija
MétodoMismo nombre, diferentes parámetrosMisma firma exacta
ResoluciónCompile-timeRuntime
AnotaciónNo necesaria@Override recomendada

Ejemplo práctico:

Overloading vs Overriding
// SOBRECARGA - mismo nombre, diferentes parámetros (compile-time)
public class Calculadora {
public int sumar(int a, int b) {
return a + b;
}
public double sumar(double a, double b) {
return a + b;
}
public int sumar(int a, int b, int c) {
return a + b + c;
}
}
// SOBRESCRITURA - redefinir método del padre (runtime)
public class Animal {
public void hacerSonido() {
System.out.println("Sonido genérico");
}
}
public class Perro extends Animal {
@Override
public void hacerSonido() {
System.out.println("Guau!");
}
}

Cómo responder en entrevista:

“La sobrecarga ocurre en la misma clase con el mismo nombre de método pero distintos parámetros, se resuelve en compilación. La sobrescritura redefine un método heredado con exactamente la misma firma, se resuelve en runtime mediante polimorfismo. @Override es opcional pero recomendada para detectar errores de compilación.”


¿Qué es? Modificador que indica que un miembro pertenece a la CLASE, no a instancias individuales.

Usos:

  • Variables estáticas: Compartidas entre todas las instancias
  • Métodos estáticos: Se llaman sin crear objeto (Math.sqrt())
  • Bloques estáticos: Se ejecutan una vez al cargar la clase
  • Clases internas estáticas: No necesitan instancia de la clase externa

Ejemplo práctico:

Modificador static
public class Contador {
// Variable estática - compartida entre todas las instancias
private static int totalInstancias = 0;
// Variable de instancia - única por objeto
private int id;
public Contador() {
totalInstancias++;
this.id = totalInstancias;
}
// Método estático - se llama sin crear instancia
public static int getTotalInstancias() {
return totalInstancias;
}
// Bloque estático - se ejecuta al cargar la clase
static {
System.out.println("Clase cargada");
}
}
// Uso
Contador c1 = new Contador();
Contador c2 = new Contador();
System.out.println(Contador.getTotalInstancias()); // 2

Cómo responder en entrevista:

static indica que el miembro pertenece a la clase, no a una instancia. Las variables static son compartidas por todos los objetos. Los métodos static se invocan directamente en la clase sin crear objeto, como Math.sqrt(). Los bloques static se ejecutan una sola vez al cargar la clase.”


¿Qué es? Modificador que garantiza visibilidad de cambios entre threads.

¿Para qué sirve?

  • Los cambios son visibles INMEDIATAMENTE para todos los threads
  • Evita que los threads cacheen el valor localmente
  • Útil para flags de control (ej: boolean running)

Limitación: NO garantiza atomicidad. Para operaciones atómicas, usar AtomicInteger, etc.

Ejemplo práctico:

Modificador volatile
public class Ejemplo {
// volatile garantiza visibilidad entre threads
private volatile boolean activo = true;
public void detener() {
activo = false; // Visible inmediatamente para otros threads
}
public void ejecutar() {
while (activo) {
// Hacer algo
}
}
}

Cómo responder en entrevista:

volatile garantiza que un campo se lea/escriba en memoria principal, no en caché del CPU, así todos los threads ven el valor más reciente. No garantiza atomicidad: un contador++ sigue siendo una operación compuesta no atómica. Para incrementos atómicos uso AtomicInteger; para bloques de código uso synchronized.”


Definición profesional: Modificador que excluye un campo del proceso de serialización. Al deserializar, el campo toma su valor por defecto.

Ejemplo práctico:

Modificador transient
public class Usuario implements Serializable {
private String nombre;
private transient String password; // No se serializa
// Al deserializar, password será null
}

Cómo responder en entrevista:

transient excluye un campo de la serialización. Lo uso para datos sensibles como contraseñas o tokens, y para referencias a objetos no serializables como conexiones a BD. Al deserializar el campo vale null para objetos o 0/false para primitivos.”


¿Qué es? Mecanismo para garantizar que solo UN thread ejecute un bloque de código a la vez.

¿Para qué sirve?

  • Prevenir condiciones de carrera (race conditions)
  • Proteger recursos compartidos
  • Garantizar consistencia de datos

Formas de uso:

  • Método sincronizado: Todo el método es exclusivo
  • Bloque sincronizado: Solo una parte del código (más granular)

Ejemplo práctico:

synchronized
public class Contador {
private int valor = 0;
// Método sincronizado
public synchronized void incrementar() {
valor++;
}
// Bloque sincronizado
public void decrementar() {
synchronized(this) {
valor--;
}
}
}

Cómo responder en entrevista:

synchronized garantiza que solo un thread ejecute el bloque a la vez usando el monitor del objeto. En métodos de instancia el monitor es this; en métodos static es la clase. El bloque sincronizado es más granular y eficiente. Para alta concurrencia prefiero ReentrantLock o las estructuras concurrent del paquete java.util.concurrent.”


¿Qué es? Patrón de diseño que garantiza UNA SOLA instancia de una clase en toda la aplicación.

¿Para qué sirve?

  • Conexiones a base de datos
  • Configuración de la aplicación
  • Loggers
  • Cachés

Implementación:

  1. Constructor privado
  2. Instancia estática privada
  3. Método público estático para obtenerla

Ejemplo práctico:

Patrón Singleton
// Singleton - una sola instancia
public class Singleton {
private static volatile Singleton instancia;
private Singleton() {} // Constructor privado
public static Singleton getInstancia() {
if (instancia == null) {
synchronized (Singleton.class) {
if (instancia == null) {
instancia = new Singleton();
}
}
}
return instancia;
}
}
// Singleton con enum (mejor práctica)
public enum SingletonEnum {
INSTANCIA;
public void metodo() {
// ...
}
}

Cómo responder en entrevista:

“Singleton garantiza una sola instancia en toda la aplicación. La implementación con enum es la mejor práctica: es thread-safe por naturaleza, resiste serialización y ataques por reflexión, y es la más simple. El double-checked locking con volatile es correcto pero más complejo. Casos de uso: connection pools, loggers, configuración.”


¿Qué es? Objeto cuyo estado NO puede modificarse después de su creación.

¿Para qué sirve?

  • Thread-safe por naturaleza (no hay estado mutable)
  • Fácil de razonar y debuggear
  • Puede usarse como clave en HashMap
  • Cacheable de forma segura

Cómo crear una clase inmutable:

  1. Clase final (no se puede extender)
  2. Campos private final
  3. Sin setters
  4. Copias defensivas en constructor y getters

Ejemplo práctico:

Clases inmutables
// Clase inmutable
public final class Inmutable {
private final String valor;
private final List<String> lista;
public Inmutable(String valor, List<String> lista) {
this.valor = valor;
this.lista = new ArrayList<>(lista); // Copia defensiva
}
public String getValor() {
return valor;
}
public List<String> getLista() {
return Collections.unmodifiableList(lista); // Vista inmutable
}
}
// String es inmutable
String s = "Hola";
s.concat(" Mundo"); // Crea nuevo String, no modifica s
System.out.println(s); // "Hola"

Cómo responder en entrevista:

“Una clase inmutable es thread-safe por naturaleza, no necesita sincronización. Se crea con clase final, campos private final, sin setters y copias defensivas para colecciones mutables. Los ejemplos clásicos son String, BigDecimal y las clases wrapper. Java 14+ tiene Records que generan clases inmutables automáticamente.”


15. ¿Cuál es la diferencia entre String, StringBuilder y StringBuffer?

Section titled “15. ¿Cuál es la diferencia entre String, StringBuilder y StringBuffer?”

¿Qué es? Tres clases para manejar texto con diferentes características.

Diferencias:

CaracterísticaStringStringBuilderStringBuffer
MutabilidadInmutableMutableMutable
Thread-safeSí (inmutable)No
PerformanceLento para concatenaciónRápidoMás lento

¿Cuál usar?

  • String: Texto que no cambia
  • StringBuilder: Concatenación en un solo thread (más común)
  • StringBuffer: Concatenación multi-thread (raro)

Ejemplo práctico:

String vs StringBuilder vs StringBuffer
// String - crea nuevos objetos
String s = "Hola";
s = s + " Mundo"; // Nuevo objeto
// StringBuilder - modifica el mismo objeto
StringBuilder sb = new StringBuilder("Hola");
sb.append(" Mundo"); // Mismo objeto
// StringBuffer - thread-safe
StringBuffer sbf = new StringBuffer("Hola");
sbf.append(" Mundo");

Cómo responder en entrevista:

String es inmutable: cada concatenación con + crea objetos en el heap, ineficiente en bucles. StringBuilder es mutable y rápido para concatenación en un solo thread. StringBuffer es thread-safe pero más lento. En bucles siempre uso StringBuilder. StringBuffer solo en contextos realmente multi-thread.”


16. ¿Cuál es la jerarquía de Collections?

Section titled “16. ¿Cuál es la jerarquía de Collections?”

Definición profesional: El Collections Framework organiza las estructuras de datos en tres familias: List, Set y Queue, además de Map que no extiende Collection.

Ejemplo práctico:

Jerarquía de Collections
// Collection (interface)
// ├── List (interface) - ordenada, permite duplicados
// │ ├── ArrayList
// │ ├── LinkedList
// │ └── Vector (legacy)
// ├── Set (interface) - sin duplicados
// │ ├── HashSet
// │ ├── LinkedHashSet
// │ └── TreeSet (ordenado)
// └── Queue (interface) - FIFO
// ├── LinkedList
// ├── PriorityQueue
// └── ArrayDeque
// Map (interface) - pares clave-valor
// ├── HashMap
// ├── LinkedHashMap
// ├── TreeMap (ordenado)
// └── Hashtable (legacy)

Cómo responder en entrevista:

“El framework tiene tres ramas: List para listas ordenadas con duplicados (ArrayList por defecto), Set para conjuntos sin duplicados (HashSet por defecto), Queue para colas (ArrayDeque). Map no extiende Collection pero es parte del framework. Cada rama tiene implementaciones para distintas necesidades de rendimiento y orden.”


17. ¿Cuál es la diferencia entre HashMap y Hashtable?

Section titled “17. ¿Cuál es la diferencia entre HashMap y Hashtable?”

¿Qué es? Dos implementaciones de Map con diferencias importantes.

Diferencias:

HashMapHashtable
No sincronizadoSincronizado
Permite null keys/valuesNo permite null
Más rápidoMás lento
ModernoLegacy

Recomendación: Usar HashMap para single-thread, ConcurrentHashMap para multi-thread.

18. ¿Cómo funciona HashMap internamente?

Section titled “18. ¿Cómo funciona HashMap internamente?”

Definición profesional: Usa un array de buckets. El hashCode() de la clave determina el bucket. Las colisiones se resuelven con lista enlazada, o árbol rojo-negro desde Java 8 si el bucket supera 8 elementos.

HashMap internamente
// HashMap: array de buckets + linked list (o árbol rojo-negro desde Java 8)
// 1. hash = key.hashCode()
// 2. índice = hash & (capacidad - 1)
// 3. Colisión → lista enlazada; si >8 elementos → árbol rojo-negro O(log n)
// Factor de carga por defecto: 0.75 (rehash cuando capacidad * 0.75 llena)

Cómo responder en entrevista:

“HashMap usa un array de buckets. Calcula el bucket con hash & (n-1). Las colisiones van a la lista enlazada del bucket. Desde Java 8, si un bucket supera 8 entradas se convierte en árbol rojo-negro: peor caso de O(n) pasa a O(log n). Es crucial implementar hashCode() y equals() correctamente en las claves.”


¿Qué es? Implementación de Map que mantiene las claves ORDENADAS usando un árbol rojo-negro.

Características:

  • Claves ordenadas (natural o con Comparator)
  • O(log n) para todas las operaciones
  • No permite claves null
  • Las claves deben ser Comparable o proveer Comparator

¿Cuándo usarlo? Cuando necesitas iterar en orden o usar métodos como firstKey(), lastKey(), headMap(), tailMap().

Ejemplo práctico:

TreeMap
// TreeMap - Map ordenado por claves (Red-Black Tree)
TreeMap<String, Integer> mapa = new TreeMap<>();
mapa.put("Banana", 2);
mapa.put("Apple", 1);
mapa.put("Cherry", 3);
// Iteración en orden alfabético
for (String key : mapa.keySet()) {
System.out.println(key); // Apple, Banana, Cherry
}
// Métodos útiles
mapa.firstKey(); // "Apple"
mapa.lastKey(); // "Cherry"
mapa.headMap("C"); // Claves < "C"
mapa.tailMap("B"); // Claves >= "B"

Cómo responder en entrevista:

TreeMap mantiene las claves ordenadas con árbol rojo-negro, todas las operaciones son O(log n). Lo uso cuando necesito iterar en orden o aprovechar NavigableMap con headMap, tailMap, floorKey, ceilingKey. No permite null como clave. Si no necesito orden, HashMap es más eficiente con O(1).“


20. ¿Cuál es la diferencia entre HashSet y TreeSet?

Section titled “20. ¿Cuál es la diferencia entre HashSet y TreeSet?”

¿Qué es? Dos implementaciones de Set con diferentes características.

Diferencias:

HashSetTreeSet
No ordenadoOrdenado
O(1) para operacionesO(log n)
Permite un nullNo permite null
Usa HashMap internamenteUsa TreeMap internamente

¿Cuál usar?

  • HashSet: Cuando no necesitas orden (más rápido)
  • TreeSet: Cuando necesitas elementos ordenados

Definición profesional: Patrón de diseño que proporciona un mecanismo uniforme para recorrer colecciones. Es el único modo seguro de eliminar elementos durante la iteración.

Iterator y ListIterator
List<String> lista = new ArrayList<>(Arrays.asList("A", "B", "C"));
// Iterator - único modo seguro de eliminar durante iteración
Iterator<String> it = lista.iterator();
while (it.hasNext()) {
String elem = it.next();
if (elem.equals("B")) it.remove(); // ✅ Seguro
// lista.remove(elem); // ❌ ConcurrentModificationException
}
// ListIterator - bidireccional, permite modificar
ListIterator<String> lit = lista.listIterator();
while (lit.hasNext()) {
lit.set(lit.next().toLowerCase()); // Modificar elemento actual
}

Cómo responder en entrevista:

Iterator proporciona hasNext(), next() y remove(). Es el único modo seguro de eliminar durante iteración: hacerlo directamente en la lista lanza ConcurrentModificationException. ListIterator extiende Iterator con soporte bidireccional y métodos set() y add(). El for-each usa Iterator internamente.”


¿Qué es? Dos comportamientos de iteradores cuando la colección se modifica durante iteración.

Diferencias:

Fail-fastFail-safe
Lanza ConcurrentModificationExceptionNo lanza excepción
ArrayList, HashMap, etc.CopyOnWriteArrayList, ConcurrentHashMap
Detecta modificación inmediatamenteTrabaja con copia

Ejemplo práctico:

Fail-fast vs Fail-safe
// Fail-fast: lanza ConcurrentModificationException
List<String> lista = new ArrayList<>();
lista.add("A");
for (String s : lista) {
lista.add("B"); // ConcurrentModificationException
}
// Fail-safe: trabaja con copia
List<String> listaConcurrente = new CopyOnWriteArrayList<>();
listaConcurrente.add("A");
for (String s : listaConcurrente) {
listaConcurrente.add("B"); // OK, pero no ve los nuevos
}

Cómo responder en entrevista:

“Fail-fast detecta modificaciones concurrentes mediante un contador modCount y lanza ConcurrentModificationException. Es para single-thread: ayuda a detectar bugs. Fail-safe (CopyOnWriteArrayList, ConcurrentHashMap) es para multi-thread: opera sobre copia o con locks finos, sin excepciones pero con mayor coste de memoria.”


¿Qué es? Dos interfaces para definir cómo ordenar objetos.

Diferencias:

ComparableComparator
Implementado EN la claseExterno a la clase
Método compareTo()Método compare()
Un solo orden (natural)Múltiples órdenes posibles
java.langjava.util

¿Cuál usar?

  • Comparable: Para el orden “por defecto” de la clase
  • Comparator: Para órdenes alternativos o clases que no puedes modificar

Ejemplo práctico:

Comparable vs Comparator
// Comparable - orden natural (en la clase)
public class Persona implements Comparable<Persona> {
private String nombre;
private int edad;
@Override
public int compareTo(Persona otra) {
return this.nombre.compareTo(otra.nombre);
}
}
// Comparator - orden personalizado (externo)
Comparator<Persona> porEdad = (p1, p2) ->
Integer.compare(p1.getEdad(), p2.getEdad());
Comparator<Persona> porNombreDesc =
Comparator.comparing(Persona::getNombre).reversed();
// Uso
List<Persona> personas = new ArrayList<>();
Collections.sort(personas); // Usa Comparable
Collections.sort(personas, porEdad); // Usa Comparator
personas.sort(porNombreDesc);

Cómo responder en entrevista:

Comparable se implementa dentro de la propia clase con compareTo() y define el orden natural. Comparator es externo con compare() y permite múltiples criterios. Uso Comparable para el orden por defecto de la clase y Comparator para órdenes alternativos o cuando no puedo modificar la clase.”


¿Qué es? Cola que ordena elementos por prioridad usando un heap.

Características:

  • Por defecto: min-heap (menor primero)
  • O(log n) para inserción y extracción
  • No permite null
  • No es thread-safe

¿Para qué sirve?

  • Algoritmos de Dijkstra
  • Scheduling de tareas
  • Encontrar k elementos más grandes/pequeños

Ejemplo práctico:

PriorityQueue
// PriorityQueue - cola con prioridad (heap)
PriorityQueue<Integer> pq = new PriorityQueue<>(); // Min-heap
pq.add(5);
pq.add(1);
pq.add(3);
System.out.println(pq.poll()); // 1 (menor primero)
System.out.println(pq.poll()); // 3
System.out.println(pq.poll()); // 5
// Max-heap
PriorityQueue<Integer> maxPq = new PriorityQueue<>(
Collections.reverseOrder()
);

Cómo responder en entrevista:

PriorityQueue implementa un heap binario donde poll() siempre extrae el elemento de mayor prioridad en O(log n). Por defecto es min-heap. Con Collections.reverseOrder() se convierte en max-heap. Lo uso en algoritmos de grafos (Dijkstra), scheduling de tareas y problemas de k-th element.”


¿Qué es? Double-Ended Queue - cola que permite insertar y eliminar en AMBOS extremos.

¿Para qué sirve?

  • Como Stack (LIFO): push(), pop()
  • Como Queue (FIFO): addLast(), removeFirst()
  • Implementaciones: ArrayDeque (más rápido), LinkedList

Nota: ArrayDeque es preferido sobre Stack (legacy) y LinkedList para stacks/queues.

Ejemplo práctico:

Deque con ArrayDeque
// Deque - Double Ended Queue
Deque<String> deque = new ArrayDeque<>();
// Como Stack (LIFO)
deque.push("A");
deque.push("B");
deque.pop(); // "B"
// Como Queue (FIFO)
deque.addLast("C");
deque.addLast("D");
deque.removeFirst(); // "A"
// Métodos
deque.addFirst("X");
deque.addLast("Y");
deque.peekFirst();
deque.peekLast();

Cómo responder en entrevista:

Deque es una cola de doble extremo que reemplaza tanto a Stack como a LinkedList para uso como cola. ArrayDeque es la implementación preferida: más eficiente en tiempo y memoria que LinkedList. Lo uso como Stack con push/pop y como Queue con offerLast/pollFirst. No permite null.”


Definición profesional: Java ofrece tres mecanismos para crear threads: extender Thread, implementar Runnable, o usar lambda. En producción se prefiere ExecutorService.

Ejemplo práctico:

Crear Threads
// Opción 1: Extender Thread
class MiThread extends Thread {
@Override
public void run() {
System.out.println("Ejecutando en: " + getName());
}
}
// Opción 2: Implementar Runnable (preferido)
class MiRunnable implements Runnable {
@Override
public void run() {
System.out.println("Ejecutando runnable");
}
}
// Opción 3: Lambda
Runnable lambda = () -> System.out.println("Lambda");
// Uso
new MiThread().start();
new Thread(new MiRunnable()).start();
new Thread(lambda).start();

Cómo responder en entrevista:

“Hay tres formas: extender Thread (limita herencia), implementar Runnable (preferido, más flexible) o usar lambda. En producción evito crear threads directamente; uso ExecutorService para gestionar un pool. Siempre llamo start(), nunca run() directamente.”


27. ¿Cuál es la diferencia entre start() y run()?

Section titled “27. ¿Cuál es la diferencia entre start() y run()?”

¿Qué es? Dos métodos de Thread con comportamientos muy diferentes.

Diferencias:

  • start(): Crea un NUEVO thread y ejecuta run() en él
  • run(): Ejecuta en el thread ACTUAL (no crea nuevo thread)

Error común: Llamar run() directamente no crea concurrencia.

Ejemplo práctico:

start() vs run()
Thread t = new Thread(() -> {
System.out.println("Thread: " + Thread.currentThread().getName());
});
// start() - crea nuevo thread y ejecuta run()
t.start(); // Imprime: Thread: Thread-0
// run() - ejecuta en el thread actual (NO crea nuevo thread)
t.run(); // Imprime: Thread: main

Cómo responder en entrevista:

start() crea un nuevo thread en la JVM y luego llama a run() en él: hay concurrencia real. run() simplemente ejecuta el método en el thread actual como una llamada normal. Llamar run() directamente es el error más común con threads: compila y ejecuta sin error pero no hay paralelismo.”


¿Qué es? Un Thread pasa por 6 estados durante su ciclo de vida.

Estados:

  • NEW: Creado pero no iniciado
  • RUNNABLE: Ejecutándose o listo para ejecutar
  • BLOCKED: Esperando un lock (synchronized)
  • WAITING: Esperando indefinidamente (wait, join)
  • TIMED_WAITING: Esperando con timeout (sleep, wait con tiempo)
  • TERMINATED: Finalizado

Ejemplo práctico:

Estados de Thread
// Estados de Thread
// NEW - creado pero no iniciado
Thread t = new Thread(() -> {});
// RUNNABLE - ejecutándose o listo para ejecutar
t.start();
// BLOCKED - esperando un lock
synchronized(objeto) { }
// WAITING - esperando indefinidamente
object.wait();
thread.join();
// TIMED_WAITING - esperando con timeout
Thread.sleep(1000);
object.wait(1000);
// TERMINATED - finalizado
// Después de que run() termina

Cómo responder en entrevista:

“Un Thread tiene 6 estados: NEW al crear, RUNNABLE al iniciar, BLOCKED esperando un lock de synchronized, WAITING esperando indefinidamente con wait/join, TIMED_WAITING con timeout, y TERMINATED al finalizar. Con jstack puedo ver el estado de todos los threads en producción para detectar deadlocks.”


29. ¿Qué es wait(), notify() y notifyAll()?

Section titled “29. ¿Qué es wait(), notify() y notifyAll()?”

¿Qué es? Métodos de Object para comunicación entre threads.

Métodos:

  • wait(): Libera el lock y espera hasta ser notificado
  • notify(): Despierta UN thread que está esperando
  • notifyAll(): Despierta TODOS los threads esperando

Requisito: Deben usarse DENTRO de un bloque synchronized.

Ejemplo práctico - Productor/Consumidor:

wait(), notify(), notifyAll()
public class ProductorConsumidor {
private List<Integer> buffer = new ArrayList<>();
private int capacidad = 10;
public synchronized void producir(int valor) throws InterruptedException {
while (buffer.size() == capacidad) {
wait(); // Espera hasta que haya espacio
}
buffer.add(valor);
notifyAll(); // Notifica a consumidores
}
public synchronized int consumir() throws InterruptedException {
while (buffer.isEmpty()) {
wait(); // Espera hasta que haya elementos
}
int valor = buffer.remove(0);
notifyAll(); // Notifica a productores
return valor;
}
}

Cómo responder en entrevista:

wait() libera el lock y pone el thread en WAITING hasta que otro llame notify()/notifyAll(). notify() despierta un thread arbitrario; notifyAll() despierta todos. Siempre uso while en lugar de if para re-verificar la condición al despertar (spurious wakeups). Deben estar dentro de synchronized.”


¿Qué es? Situación donde dos o más threads están bloqueados esperando recursos que el otro tiene.

Condiciones para deadlock:

  1. Exclusión mutua
  2. Hold and wait
  3. No preemption
  4. Espera circular

Prevención:

  • Ordenar la adquisición de locks
  • Usar timeout con tryLock()
  • Evitar locks anidados

Ejemplo práctico:

Deadlock
// Deadlock - dos threads esperando recursos del otro
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized(lock1) {
Thread.sleep(100);
synchronized(lock2) { } // Espera lock2
}
});
Thread t2 = new Thread(() -> {
synchronized(lock2) {
Thread.sleep(100);
synchronized(lock1) { } // Espera lock1
}
});
// Prevención: ordenar locks, usar timeout, usar tryLock()

Cómo responder en entrevista:

“Un deadlock ocurre cuando dos threads se bloquean mutuamente esperando locks que el otro tiene. Se previene adquiriendo locks siempre en el mismo orden, usando tryLock() con timeout de ReentrantLock, o evitando locks anidados. Para detectarlos en producción uso jstack para obtener thread dumps.”


¿Qué es? Framework para manejar un pool de threads reutilizables.

¿Para qué sirve?

  • Evitar crear/destruir threads constantemente (costoso)
  • Limitar el número de threads concurrentes
  • Manejar tareas asíncronas de forma ordenada

Métodos principales:

  • execute(Runnable): Ejecutar sin resultado
  • submit(Callable): Ejecutar con resultado (Future)
  • shutdown(): Cerrar el pool

Ejemplo práctico:

ExecutorService
// ExecutorService - pool de threads
ExecutorService executor = Executors.newFixedThreadPool(4);
// Ejecutar Runnable
executor.execute(() -> System.out.println("Tarea"));
// Ejecutar Callable (retorna valor)
Future<Integer> future = executor.submit(() -> {
Thread.sleep(1000);
return 42;
});
int resultado = future.get(); // Bloquea hasta obtener resultado
// Cerrar executor
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);

Cómo responder en entrevista:

ExecutorService gestiona un pool de threads reutilizables. execute() para tareas sin resultado, submit() para Callable con Future como resultado. Siempre cierro con shutdown() y awaitTermination(). En Spring Boot configuro ThreadPoolTaskExecutor con corePoolSize, maxPoolSize y queueCapacity según la carga esperada.”


32. ¿Cuáles son los tipos de ExecutorService?

Section titled “32. ¿Cuáles son los tipos de ExecutorService?”

¿Qué es? Diferentes implementaciones de ExecutorService para distintos casos de uso.

Tipos:

  • FixedThreadPool: Número fijo de threads (para carga predecible)
  • CachedThreadPool: Crea threads según demanda (para muchas tareas cortas)
  • SingleThreadExecutor: Un solo thread (para tareas secuenciales)
  • ScheduledThreadPool: Para tareas programadas/periódicas

Ejemplo práctico:

Tipos de ExecutorService
// Fixed Thread Pool - número fijo de threads
ExecutorService fixed = Executors.newFixedThreadPool(4);
// Cached Thread Pool - crea threads según necesidad
ExecutorService cached = Executors.newCachedThreadPool();
// Single Thread - un solo thread
ExecutorService single = Executors.newSingleThreadExecutor();
// Scheduled - para tareas programadas
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
scheduled.scheduleAtFixedRate(
() -> System.out.println("Cada 5 segundos"),
0, 5, TimeUnit.SECONDS
);

Cómo responder en entrevista:

FixedThreadPool para carga predecible con límite de threads. CachedThreadPool para muchas tareas cortas (cuidado: puede crear miles de threads). SingleThreadExecutor para serializar tareas. ScheduledThreadPool para cron jobs. En Java 21, los Virtual Threads permiten millones de threads ligeros para workloads I/O.”


¿Qué es? Future mejorado (Java 8+) para programación asíncrona no bloqueante.

¿Para qué sirve?

  • Encadenar operaciones asíncronas (thenApply, thenCompose)
  • Combinar múltiples futures (thenCombine, allOf)
  • Manejar errores (exceptionally, handle)

Ventajas sobre Future:

  • No bloqueante
  • Composable
  • Manejo de errores integrado

Ejemplo práctico:

CompletableFuture
// CompletableFuture - programación asíncrona
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
// Tarea asíncrona
return "Resultado";
})
.thenApply(s -> s.toUpperCase())
.thenApply(s -> "Procesado: " + s);
// Combinar futures
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hola");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Mundo");
CompletableFuture<String> combinado = f1.thenCombine(f2,
(s1, s2) -> s1 + " " + s2);
// Esperar múltiples
CompletableFuture.allOf(f1, f2).join();

Cómo responder en entrevista:

CompletableFuture mejora Future añadiendo composición no bloqueante. thenApply transforma, thenCompose encadena futures async, thenCombine combina dos y allOf espera varios en paralelo. exceptionally/handle gestionan errores. Es la base para aplicaciones reactivas antes de Spring WebFlux.”


¿Qué es? Dos mecanismos para sincronización de threads.

Diferencias:

synchronizedLock
Simple, automáticoMás flexible
No timeouttryLock() con timeout
Solo exclusivoReadWriteLock disponible
Libera automáticamenteRequiere unlock() manual

Recomendación: Usar synchronized por defecto, Lock cuando necesites más control.

Ejemplo práctico:

Lock vs synchronized
// synchronized - simple pero limitado
synchronized(objeto) {
// código crítico
}
// Lock - más flexible
Lock lock = new ReentrantLock();
lock.lock();
try {
// código crítico
} finally {
lock.unlock();
}
// tryLock - intenta sin bloquear
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// código
} finally {
lock.unlock();
}
}
// ReadWriteLock - múltiples lectores, un escritor
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
rwLock.writeLock().lock();

Cómo responder en entrevista:

synchronized es suficiente para la mayoría de casos. ReentrantLock añade tryLock con timeout para evitar deadlocks, lockInterruptibly() para threads cancelables, y ReadWriteLock para mayor concurrencia en lecturas. Siempre en try/finally para garantizar el unlock incluso con excepciones.”


¿Qué es? Clases que proporcionan operaciones atómicas sin necesidad de synchronized.

Clases principales:

  • AtomicInteger, AtomicLong: Contadores atómicos
  • AtomicBoolean: Flag atómico
  • AtomicReference<T>: Referencia atómica a objeto

¿Cómo funcionan? Usan CAS (Compare-And-Swap) internamente, más eficiente que locks.

Ejemplo práctico:

Clases Atomic
// Operaciones atómicas sin synchronized
AtomicInteger contador = new AtomicInteger(0);
contador.incrementAndGet(); // ++contador
contador.getAndIncrement(); // contador++
contador.addAndGet(5); // contador += 5
contador.compareAndSet(5, 10); // Si es 5, cambiar a 10
// AtomicReference para objetos
AtomicReference<String> ref = new AtomicReference<>("inicial");
ref.compareAndSet("inicial", "nuevo");

Cómo responder en entrevista:

“Las clases Atomic usan instrucciones CAS del CPU: más eficientes que synchronized para operaciones simples porque no hay bloqueo de threads. AtomicInteger para contadores, AtomicBoolean para flags, AtomicReference para objetos. LongAdder es mejor que AtomicLong en alta contención porque distribuye el trabajo.”


Definición profesional: Función anónima que implementa una interfaz funcional. Sintaxis: (parámetros) -> expresión o (parámetros) -> { bloque }.

Ejemplo práctico:

Expresiones Lambda
// Lambda - función anónima
// (parámetros) -> expresión
// (parámetros) -> { statements }
// Sin parámetros
Runnable r = () -> System.out.println("Hola");
// Un parámetro
Consumer<String> c = s -> System.out.println(s);
// Múltiples parámetros
BiFunction<Integer, Integer, Integer> suma = (a, b) -> a + b;
// Con bloque
Comparator<String> comp = (s1, s2) -> {
int len1 = s1.length();
int len2 = s2.length();
return Integer.compare(len1, len2);
};

Cómo responder en entrevista:

“Una lambda es una función anónima que implementa una interfaz funcional. La sintaxis es (params) -> expresión. Simplifican el código al reemplazar clases anónimas verbosas. Las variables capturadas del scope externo deben ser effectively final. Son el fundamento de Streams y la programación funcional en Java 8+.“


¿Qué es? Interfaz con UN SOLO método abstracto (SAM - Single Abstract Method).

Características:

  • Puede tener métodos default y static
  • Anotación @FunctionalInterface opcional pero recomendada
  • Puede ser implementada con lambda

Interfaces funcionales comunes:

  • Function<T, R>: T → R
  • Consumer<T>: T → void
  • Supplier<T>: () → T
  • Predicate<T>: T → boolean

Ejemplo práctico:

Interfaz funcional
// Interfaz con un solo método abstracto
@FunctionalInterface
public interface MiInterfaz {
int calcular(int x);
// Puede tener métodos default y static
default void metodoDefault() {}
static void metodoStatic() {}
}
// Interfaces funcionales comunes:
// Function<T, R> - T -> R
// Consumer<T> - T -> void
// Supplier<T> - () -> T
// Predicate<T> - T -> boolean
// BiFunction<T,U,R> - (T, U) -> R

Cómo responder en entrevista:

“Una interfaz funcional tiene exactamente un método abstracto. @FunctionalInterface verifica esto y da error de compilación si no se cumple. Las más usadas del JDK: Function para transformar, Consumer para efectos, Supplier para fábricas, Predicate para filtros. Cualquier SAM puede usarse con lambda aunque no tenga la anotación.”


¿Qué es? Atajo para lambdas que solo llaman a un método existente.

Tipos:

  • Clase::metodoEstatico - Método estático
  • objeto::metodo - Método de instancia específica
  • Clase::metodo - Método de instancia (primer param es el objeto)
  • Clase::new - Constructor

Ejemplo práctico:

Method References
// Method Reference - atajo para lambdas simples
List<String> lista = Arrays.asList("a", "b", "c");
// Lambda
lista.forEach(s -> System.out.println(s));
// Method Reference
lista.forEach(System.out::println);
// Tipos:
// Estático: Clase::metodoEstatico
Integer::parseInt
// Instancia de objeto: objeto::metodo
String s = "hola";
Supplier<Integer> len = s::length;
// Instancia de clase: Clase::metodo
Function<String, Integer> f = String::length;
// Constructor: Clase::new
Supplier<ArrayList<String>> sup = ArrayList::new;

Cómo responder en entrevista:

“Un Method Reference es azúcar sintáctica para una lambda que solo llama a un método. Hay cuatro formas: método estático (Integer::parseInt), método de instancia de objeto específico (objeto::metodo), método de instancia de clase (String::toUpperCase) y constructor (Clase::new). Mejoran la legibilidad cuando la lambda es trivial.”


¿Qué es? API para procesar secuencias de elementos de forma declarativa.

Estructura:

  1. Fuente: Collection, array, generador
  2. Operaciones intermedias: filter, map, sorted (lazy)
  3. Operación terminal: collect, forEach, reduce (ejecuta el pipeline)

Características:

  • No modifica la fuente original
  • Lazy evaluation (solo ejecuta cuando hay terminal)
  • Se consume una sola vez

Ejemplo práctico:

Stream API
List<String> nombres = Arrays.asList("Ana", "Juan", "Pedro", "María");
// Stream pipeline: source -> operaciones intermedias -> operación terminal
List<String> resultado = nombres.stream()
.filter(n -> n.length() > 3) // Intermedia
.map(String::toUpperCase) // Intermedia
.sorted() // Intermedia
.collect(Collectors.toList()); // Terminal
// Operaciones intermedias: filter, map, flatMap, sorted, distinct, limit, skip
// Operaciones terminales: collect, forEach, reduce, count, findFirst, anyMatch

Cómo responder en entrevista:

“La Stream API procesa datos de forma declarativa. Las operaciones intermedias son lazy: no ejecutan hasta que hay una terminal. Las terminales (collect, forEach, reduce) disparan el pipeline. Los streams no modifican la fuente original y se consumen una sola vez. Para primitivos uso IntStream/LongStream para evitar autoboxing.”


40. ¿Cuál es la diferencia entre map() y flatMap()?

Section titled “40. ¿Cuál es la diferencia entre map() y flatMap()?”

¿Qué es? Dos operaciones de transformación en Streams.

Diferencias:

  • map(): Transforma cada elemento 1:1 (T → R)
  • flatMap(): Transforma y APLANA (T → Stream de R)

¿Cuándo usar flatMap? Cuando tienes streams anidados o cada elemento produce múltiples resultados.

Ejemplo práctico:

map() vs flatMap()
// map() - transforma cada elemento
List<String> palabras = Arrays.asList("Hola", "Mundo");
List<Integer> longitudes = palabras.stream()
.map(String::length)
.collect(Collectors.toList()); // [4, 5]
// flatMap() - aplana streams anidados
List<List<Integer>> listas = Arrays.asList(
Arrays.asList(1, 2),
Arrays.asList(3, 4)
);
List<Integer> plana = listas.stream()
.flatMap(List::stream)
.collect(Collectors.toList()); // [1, 2, 3, 4]

Cómo responder en entrevista:

map() transforma elemento a elemento con relación 1:1. flatMap() transforma cada elemento en un Stream y los fusiona: relación 1:N aplanada. Lo uso cuando tengo estructuras anidadas: lista de listas u objetos con colecciones internas. Si map() devuelve un Stream, flatMap() lo aplana automáticamente.”


¿Qué es? Operación terminal que combina todos los elementos en uno solo.

Sintaxis:

reduce(valorInicial, (acumulador, elemento) -> resultado)

Casos de uso:

  • Sumar elementos
  • Encontrar máximo/mínimo
  • Concatenar strings
  • Cualquier operación de agregación

Ejemplo práctico:

reduce()
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
// reduce - combina elementos en uno solo
int suma = numeros.stream()
.reduce(0, (a, b) -> a + b); // 15
int producto = numeros.stream()
.reduce(1, (a, b) -> a * b); // 120
// Sin valor inicial (retorna Optional)
Optional<Integer> max = numeros.stream()
.reduce(Integer::max);

Cómo responder en entrevista:

reduce() agrega todos los elementos en un único resultado. Con valor identidad retorna T directamente; sin él retorna Optional porque el stream puede estar vacío. Para sumas, máximos o promedios de primitivos prefiero mapToInt().sum() o Collectors.summingInt() que son más legibles y evitan autoboxing.”


¿Qué es? Clase de utilidades para la operación terminal collect().

Collectors comunes:

  • toList(), toSet(): Convertir a colección
  • toMap(): Convertir a Map
  • groupingBy(): Agrupar por criterio
  • partitioningBy(): Dividir en dos grupos (true/false)
  • joining(): Concatenar strings
  • counting(), summingInt(): Estadísticas

Ejemplo práctico:

Collectors
List<Persona> personas = // ...
// toList, toSet
List<String> nombres = personas.stream()
.map(Persona::getNombre)
.collect(Collectors.toList());
// toMap
Map<Integer, String> mapa = personas.stream()
.collect(Collectors.toMap(
Persona::getId,
Persona::getNombre
));
// groupingBy
Map<String, List<Persona>> porCiudad = personas.stream()
.collect(Collectors.groupingBy(Persona::getCiudad));
// partitioningBy
Map<Boolean, List<Persona>> mayores = personas.stream()
.collect(Collectors.partitioningBy(p -> p.getEdad() >= 18));
// joining
String nombresUnidos = personas.stream()
.map(Persona::getNombre)
.collect(Collectors.joining(", ")); // "Ana, Juan, Pedro"

Cómo responder en entrevista:

Collectors proporciona implementaciones para collect(). Las más usadas: toList/toSet para colecciones, groupingBy para agrupar (equivalente a GROUP BY de SQL), partitioningBy para dividir en dos grupos, joining para concatenar strings con delimitador, counting y summingInt para estadísticas.”


¿Qué es? Contenedor que puede o no contener un valor no-null.

¿Para qué sirve?

  • Evitar null y NullPointerException
  • Hacer explícito que un valor puede estar ausente
  • API más expresiva

Métodos principales:

  • orElse(), orElseGet(), orElseThrow(): Valor por defecto
  • map(), flatMap(): Transformar
  • filter(): Filtrar
  • ifPresent(): Ejecutar si tiene valor

Ejemplo práctico:

Optional
// Optional - contenedor que puede o no tener valor
Optional<String> opcional = Optional.of("valor");
Optional<String> vacio = Optional.empty();
Optional<String> nullable = Optional.ofNullable(posibleNull);
// Obtener valor
String valor = opcional.get(); // Lanza excepción si vacío
String valorODefault = opcional.orElse("default");
String valorOSupplier = opcional.orElseGet(() -> calcularDefault());
String valorOException = opcional.orElseThrow(() -> new RuntimeException());
// Operaciones
opcional.isPresent(); // true si tiene valor
opcional.ifPresent(v -> System.out.println(v));
opcional.map(String::toUpperCase);
opcional.filter(s -> s.length() > 3);
opcional.flatMap(this::buscarOtro);

Cómo responder en entrevista:

Optional hace explícito que un valor puede estar ausente. Uso ofNullable() para valores potencialmente null, orElse() para valor por defecto barato y orElseGet() con Supplier para valor costoso. Evito get() sin isPresent() previo. Optional es para retornos de método, no para campos de clase ni parámetros.”


¿Qué es? Stream que procesa elementos en paralelo usando múltiples threads.

¿Cómo funciona? Usa el ForkJoinPool común para dividir el trabajo.

¿Cuándo usarlo?

  • Grandes volúmenes de datos
  • Operaciones CPU-intensivas
  • Operaciones independientes entre sí

Cuidado con:

  • Operaciones con estado compartido
  • Colecciones pequeñas (overhead > beneficio)
  • Orden de elementos

Ejemplo práctico:

Parallel Streams
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Stream paralelo - usa múltiples threads
int suma = numeros.parallelStream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
// Convertir a paralelo
numeros.stream().parallel();
// Cuidado con:
// - Operaciones con estado
// - Orden de elementos
// - Overhead para colecciones pequeñas

Cómo responder en entrevista:

“Los parallel streams dividen el trabajo entre threads del ForkJoinPool. Son útiles para operaciones CPU-intensivas con grandes volúmenes de datos independientes. Evito usarlos con colecciones pequeñas (overhead supera el beneficio), con estado compartido (race conditions) o en operaciones I/O. Siempre mido con benchmark antes de adoptar.”


45. ¿Cuál es la diferencia entre Stream y Collection?

Section titled “45. ¿Cuál es la diferencia entre Stream y Collection?”

¿Qué es? Dos formas de trabajar con secuencias de datos con filosofías diferentes.

Diferencias:

StreamCollection
No almacena datosAlmacena datos
Lazy evaluationEager evaluation
Puede ser infinitoFinito
Se consume una vezReutilizable
Operaciones funcionalesOperaciones imperativas

Definición profesional: Java Database Connectivity: API estándar de Java para conectar y ejecutar operaciones sobre bases de datos relacionales mediante un driver específico.

Ejemplo práctico:

JDBC básico
// JDBC - Java Database Connectivity
// 1. Cargar driver
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. Establecer conexión
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/midb",
"usuario",
"password"
);
// 3. Crear statement
Statement stmt = conn.createStatement();
// 4. Ejecutar query
ResultSet rs = stmt.executeQuery("SELECT * FROM usuarios");
// 5. Procesar resultados
while (rs.next()) {
int id = rs.getInt("id");
String nombre = rs.getString("nombre");
}
// 6. Cerrar recursos
rs.close();
stmt.close();
conn.close();

Cómo responder en entrevista:

“JDBC es la API estándar para conectar Java con bases de datos. Los pasos son: obtener conexión del DriverManager/DataSource, crear PreparedStatement, ejecutar, procesar ResultSet y cerrar recursos. Siempre uso try-with-resources para garantizar el cierre. En producción uso un connection pool como HikariCP, no conexiones directas.”


¿Qué es? Statement precompilado con parámetros marcados con ?.

Ventajas:

  • Previene SQL injection (los parámetros se escapan automáticamente)
  • Mejor performance para queries repetidas (se compila una vez)
  • Más legible que concatenar strings

Siempre usar PreparedStatement en lugar de Statement con datos de usuario.

Ejemplo práctico:

PreparedStatement
// PreparedStatement - previene SQL injection
String sql = "SELECT * FROM usuarios WHERE nombre = ? AND edad > ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, nombre);
pstmt.setInt(2, edad);
ResultSet rs = pstmt.executeQuery();
// Para INSERT/UPDATE/DELETE
String insertSql = "INSERT INTO usuarios (nombre, edad) VALUES (?, ?)";
PreparedStatement insertStmt = conn.prepareStatement(insertSql);
insertStmt.setString(1, "Juan");
insertStmt.setInt(2, 25);
int filasAfectadas = insertStmt.executeUpdate();

Cómo responder en entrevista:

PreparedStatement previene SQL injection porque los parámetros se escapan automáticamente antes de enviarse a la BD. Además el driver puede cachear el plan de ejecución para queries repetidas. Nunca concateno valores de usuario en queries SQL directamente.”


¿Qué es? Grupo de operaciones que se ejecutan como una unidad atómica.

Propiedades ACID:

  • Atomicity: Todo o nada
  • Consistency: Estado válido antes y después
  • Isolation: Transacciones no interfieren
  • Durability: Cambios permanentes

Control:

  • setAutoCommit(false): Iniciar transacción
  • commit(): Confirmar cambios
  • rollback(): Revertir cambios

Ejemplo práctico:

Transacciones JDBC
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, pass);
conn.setAutoCommit(false); // Iniciar transacción
// Operaciones
stmt1.executeUpdate("UPDATE cuentas SET saldo = saldo - 100 WHERE id = 1");
stmt2.executeUpdate("UPDATE cuentas SET saldo = saldo + 100 WHERE id = 2");
conn.commit(); // Confirmar transacción
} catch (SQLException e) {
if (conn != null) {
conn.rollback(); // Revertir en caso de error
}
} finally {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
}

Cómo responder en entrevista:

“Una transacción garantiza ACID: Atomicity (todo o nada), Consistency (estado válido antes y después), Isolation (no afecta otras transacciones) y Durability (los cambios persisten). En JDBC desactivo autoCommit, ejecuto las operaciones y hago commit o rollback en el catch. En Spring prefiero @Transactional que gestiona esto automáticamente.”


¿Qué es? Conjunto de conexiones a base de datos que se reutilizan.

¿Para qué sirve?

  • Evitar crear/cerrar conexiones constantemente (costoso)
  • Limitar número máximo de conexiones
  • Mejorar performance significativamente

Implementaciones populares:

  • HikariCP: El más rápido y recomendado
  • Apache DBCP: Clásico
  • C3P0: Legacy

Ejemplo práctico con HikariCP:

Connection Pool con HikariCP
// Connection Pool - reutiliza conexiones
// Con HikariCP (popular)
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/midb");
config.setUsername("usuario");
config.setPassword("password");
config.setMaximumPoolSize(10);
HikariDataSource dataSource = new HikariDataSource(config);
// Obtener conexión del pool
try (Connection conn = dataSource.getConnection()) {
// usar conexión
} // Se devuelve al pool automáticamente

Cómo responder en entrevista:

“Crear una conexión JDBC es costoso: establece comunicación de red, autentica y negocia el protocolo. Un connection pool mantiene conexiones listas para ser prestadas. HikariCP es el más rápido y es el default en Spring Boot. Los parámetros clave son maxPoolSize (límite de conexiones simultáneas) y connectionTimeout.”


¿Qué es? Data Access Object - patrón que separa la lógica de acceso a datos de la lógica de negocio.

¿Para qué sirve?

  • Separación de responsabilidades: La capa de negocio no conoce detalles de BD
  • Testabilidad: Fácil de mockear
  • Flexibilidad: Cambiar BD sin afectar negocio

Estructura:

  • Interface con operaciones CRUD
  • Implementación con lógica de BD

Ejemplo práctico:

Patrón DAO
// DAO - Data Access Object
public interface UsuarioDAO {
Usuario findById(int id);
List<Usuario> findAll();
void save(Usuario usuario);
void update(Usuario usuario);
void delete(int id);
}
public class UsuarioDAOImpl implements UsuarioDAO {
private Connection conn;
@Override
public Usuario findById(int id) {
String sql = "SELECT * FROM usuarios WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return mapResultSetToUsuario(rs);
}
} catch (SQLException e) {
throw new DAOException(e);
}
return null;
}
private Usuario mapResultSetToUsuario(ResultSet rs) throws SQLException {
return new Usuario(
rs.getInt("id"),
rs.getString("nombre"),
rs.getInt("edad")
);
}
}

Cómo responder en entrevista:

“El patrón DAO separa la lógica de acceso a datos de la lógica de negocio. La interfaz define el contrato CRUD y cada implementación puede usar distinta tecnología (JDBC, JPA, MongoDB). Facilita el testing porque puedo mockear el DAO. En Spring Boot suelo usar JpaRepository que implementa este patrón con generics.”


🐝