InterviewHack.ai
Start free
Blog/Java Developer Interview Questions and How to Answer Them (50+ Questions)

Java Developer Interview Questions and How to Answer Them (50+ Questions)

September 16, 2026

javabackend

50+ preguntas de entrevista para desarrolladores Java con respuestas detalladas y código real. Cubre OOP, Collections, Concurrencia, JVM, Spring, Design Patterns y Java moderno (Records, Sealed Classes, Streams). La guía más completa para preparar tu próxima entrevista Java.

Java Developer Interview Questions and How to Answer Them (50+ Questions)

Conseguiste la entrevista. Ahora viene lo difícil: prepararte para las preguntas técnicas sin perder tres semanas leyendo documentación.

Esta guía cubre las 50 preguntas más frecuentes en entrevistas para desarrolladores Java — con respuestas detalladas, código real y la explicación de *por qué* el entrevistador hace cada pregunta.


Cómo usar esta guía

Las preguntas están ordenadas por tema. Si tenés poco tiempo, priorizá los bloques OOP, Collections, Concurrencia y JVM — aparecen en prácticamente todas las entrevistas.

Para cada pregunta vas a encontrar:

  • La respuesta directa (lo que el entrevistador quiere escuchar)
  • Código real que podés estudiar y adaptar
  • Lo que *diferencia* una respuesta buena de una respuesta excelente

Bloque 1: Fundamentos de Java y OOP

1. ¿Cuáles son los cuatro pilares de la programación orientada a objetos en Java?

Respuesta directa: Encapsulamiento, Herencia, Polimorfismo y Abstracción.

La explicación que convence al entrevistador:

Encapsulamiento es ocultar el estado interno y exponer solo lo necesario a través de métodos públicos. No es solo poner private en los campos — es diseñar la clase para que sea imposible dejarla en un estado inválido.

java
public class CuentaBancaria {
    private double saldo; // estado oculto

    public void depositar(double monto) {
        if (monto <= 0) throw new IllegalArgumentException("El monto debe ser positivo");
        this.saldo += monto;
    }

    public double getSaldo() {
        return saldo;
    }
    // No hay setSaldo(). El saldo solo cambia por operaciones válidas.
}

Herencia permite que una clase derive comportamiento de otra. En Java, una clase solo puede heredar de una clase (extends), pero puede implementar múltiples interfaces.

java
public class Animal {
    public void respirar() {
        System.out.println("Respirando...");
    }
}

public class Perro extends Animal {
    public void ladrar() {
        System.out.println("Guau");
    }
}

Polimorfismo permite tratar objetos de diferentes clases a través de una interfaz común. Se divide en polimorfismo en tiempo de compilación (sobrecarga) y en tiempo de ejecución (sobreescritura).

java
public class Figura {
    public double area() { return 0; }
}

public class Circulo extends Figura {
    private double radio;
    public Circulo(double radio) { this.radio = radio; }

    @Override
    public double area() { return Math.PI * radio * radio; }
}

public class Rectangulo extends Figura {
    private double ancho, alto;
    public Rectangulo(double ancho, double alto) {
        this.ancho = ancho;
        this.alto = alto;
    }

    @Override
    public double area() { return ancho * alto; }
}

// El mismo método funciona con cualquier Figura:
List<Figura> figuras = List.of(new Circulo(5), new Rectangulo(3, 4));
figuras.forEach(f -> System.out.println(f.area()));

Abstracción es exponer solo lo esencial y ocultar los detalles de implementación. Se logra con clases abstractas e interfaces.

Respuesta que destaca: Mencioná que en Java moderno (desde Java 8), las interfaces pueden tener métodos default y static, lo que desdibujó un poco la línea entre interfaces y clases abstractas.


2. ¿Cuál es la diferencia entre una clase abstracta y una interfaz?

| Característica | Clase abstracta | Interfaz |

|---|---|---|

| Herencia múltiple | No (solo una clase padre) | Sí (múltiples interfaces) |

| Estado (campos) | Sí, puede tener campos de instancia | Solo constantes (public static final) |

| Métodos concretos | Sí | Desde Java 8: default y static |

| Constructores | Sí | No |

| Modificadores de acceso | Cualquiera | Métodos public por defecto |

Cuándo usar cada uno:

Usá clase abstracta cuando las clases que heredan comparten estado o comportamiento base genuino (template method pattern).

Usá interfaz para definir un contrato de comportamiento que puede ser implementado por clases sin relación de herencia.

java
// Clase abstracta: comparten estado y lógica base
public abstract class ConexionBD {
    protected String url;
    protected String usuario;

    public ConexionBD(String url, String usuario) {
        this.url = url;
        this.usuario = usuario;
    }

    // Template method: define el algoritmo, las subclases llenan los pasos
    public final void conectar() {
        validarCredenciales();
        abrirConexion();
        configurarPool();
    }

    protected abstract void abrirConexion();
    protected abstract void configurarPool();

    private void validarCredenciales() {
        if (url == null || usuario == null) throw new IllegalStateException("Credenciales incompletas");
    }
}

// Interfaz: contrato de comportamiento para clases no relacionadas
public interface Exportable {
    byte[] exportar();
    String getFormato();

    default String getNombreArchivo() {
        return "export_" + System.currentTimeMillis() + "." + getFormato();
    }
}

3. ¿Qué es el principio SOLID? Explicá cada letra.

S — Single Responsibility: Una clase debe tener una sola razón para cambiar.

java
// Mal: esta clase hace demasiado
public class UsuarioService {
    public void guardarUsuario(Usuario u) { /* lógica de BD */ }
    public void enviarEmailBienvenida(Usuario u) { /* lógica de email */ }
    public String generarReporteUsuarios() { /* lógica de reporte */ }
}

// Bien: cada clase tiene una responsabilidad
public class UsuarioRepository { public void guardar(Usuario u) { /* BD */ } }
public class EmailService { public void enviarBienvenida(Usuario u) { /* email */ } }
public class UsuarioReportService { public String generar() { /* reporte */ } }

O — Open/Closed: Abierto para extensión, cerrado para modificación.

java
// En lugar de modificar esta clase para cada nuevo tipo de descuento:
public interface Descuento {
    double calcular(double precio);
}

public class DescuentoEstudiante implements Descuento {
    public double calcular(double precio) { return precio * 0.9; }
}

public class DescuentoVIP implements Descuento {
    public double calcular(double precio) { return precio * 0.75; }
}

L — Liskov Substitution: Las subclases deben poder reemplazar a la clase padre sin romper el programa.

I — Interface Segregation: Mejor varias interfaces específicas que una interfaz grande y genérica.

D — Dependency Inversion: Depender de abstracciones, no de implementaciones concretas.

java
// Mal: depende de la implementación concreta
public class NotificacionService {
    private EmailSender emailSender = new EmailSender(); // acoplado
}

// Bien: depende de la abstracción
public class NotificacionService {
    private final MensajeEnviador enviador;

    public NotificacionService(MensajeEnviador enviador) { // inyectado
        this.enviador = enviador;
    }
}

4. ¿Qué es la diferencia entre `==` y `.equals()` en Java?

== compara referencias (si apuntan al mismo objeto en memoria).

.equals() compara contenido (lo que el método define como "igual").

java
String a = new String("hola");
String b = new String("hola");

System.out.println(a == b);        // false — distintos objetos en heap
System.out.println(a.equals(b));   // true — mismo contenido

// El String pool cambia las cosas con literales:
String c = "hola";
String d = "hola";
System.out.println(c == d);  // true — mismo objeto del pool

Para tipos primitivos: == compara valores directamente (no hay referencias).

Regla de oro al implementar equals(): Siempre sobreescribí hashCode() al mismo tiempo. Si dos objetos son iguales según equals(), deben tener el mismo hashCode(). Si no, los HashMap y HashSet se van a comportar mal.

java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Producto p)) return false;
    return Objects.equals(this.codigo, p.codigo);
}

@Override
public int hashCode() {
    return Objects.hash(codigo);
}

5. ¿Qué es `final` en Java? ¿Qué diferencia hay entre una variable, método y clase `final`?

  • Variable final: Solo se puede asignar una vez. Para primitivos es una constante. Para objetos, la referencia no puede cambiar pero el objeto sí puede mutar.
  • Método final: No puede ser sobreescrito en subclases.
  • Clase final: No puede tener subclases. Ejemplo: String, Integer.
java
public final class Inmutable {
    private final int valor;          // primitivo: constante real
    private final List<String> items; // referencia constante, pero la lista puede mutar

    public Inmutable(int valor) {
        this.valor = valor;
        this.items = new ArrayList<>();
    }

    public void agregarItem(String item) {
        items.add(item); // esto es válido — no cambia la referencia
    }

    // items = new ArrayList<>(); // esto NO compila
}

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

  • String: Inmutable. Cada operación de concatenación crea un nuevo objeto.
  • StringBuilder: Mutable, no thread-safe. Usar en código de un solo hilo.
  • StringBuffer: Mutable, thread-safe (métodos sincronizados). Más lento que StringBuilder.
java
// MAL: en un loop, esto crea N strings intermedios
String resultado = "";
for (int i = 0; i < 10000; i++) {
    resultado += i; // O(n²) en tiempo y memoria
}

// BIEN: usa StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String resultado = sb.toString(); // O(n)

Nota: El compilador convierte "a" + "b" + "c" en un StringBuilder automáticamente, pero no dentro de loops.


Bloque 2: Collections y Generics

7. ¿Cuál es la diferencia entre `ArrayList` y `LinkedList`?

| Operación | ArrayList | LinkedList |

|---|---|---|

| Acceso por índice | O(1) | O(n) |

| Inserción al final | O(1) amortizado | O(1) |

| Inserción en medio | O(n) | O(1) si tenés el nodo |

| Memoria | Menos overhead | Más overhead (nodos con punteros) |

java
// ArrayList: bueno para acceso aleatorio frecuente
List<String> nombres = new ArrayList<>();
nombres.add("Ana");
nombres.get(0); // O(1) — directo por índice

// LinkedList: bueno como deque (insertar/sacar de los extremos)
Deque<String> cola = new LinkedList<>();
cola.addFirst("primero");
cola.addLast("ultimo");
cola.pollFirst(); // O(1)

En la práctica: ArrayList gana en casi todos los casos por mejor localidad de caché. Usá LinkedList solo cuando necesitás insertar/eliminar frecuentemente en los extremos y no accedés por índice.


8. ¿Cómo funciona `HashMap` internamente?

HashMap usa un array de "buckets" (cuadros). Cuando guardás un par clave-valor:

  1. 1Se calcula hashCode() de la clave.
  2. 2El hash se mapea a un índice del array.
  3. 3Si hay colisión (dos claves con el mismo índice), se usa una lista enlazada (o un árbol rojo-negro si hay 8+ elementos, desde Java 8).

Conceptos clave:

  • Load factor (0.75 por defecto): Cuando el mapa supera el 75% de capacidad, se rehashea (duplica el array).
  • Capacidad inicial (16 por defecto): Podés optimizar si sabés cuántos elementos vas a guardar.
java
// Optimizado: evita rehashing si sabés el tamaño
Map<String, Integer> mapa = new HashMap<>(1000, 0.75f);

// Por qué hashCode y equals importan:
Map<Punto, String> puntos = new HashMap<>();
puntos.put(new Punto(1, 2), "origen");

// Si Punto no implementa hashCode correctamente:
puntos.get(new Punto(1, 2)); // puede devolver null aunque la clave "exista"

9. ¿Cuál es la diferencia entre `HashMap`, `LinkedHashMap` y `TreeMap`?

  • HashMap: Sin orden garantizado. O(1) promedio para get/put.
  • LinkedHashMap: Mantiene orden de inserción (o de último acceso con accessOrder=true). Útil para implementar LRU cache.
  • TreeMap: Ordenado por clave natural o por un Comparator. O(log n) para todas las operaciones.
java
// LinkedHashMap como LRU cache simple
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacidad;

    public LRUCache(int capacidad) {
        super(capacidad, 0.75f, true); // accessOrder = true
        this.capacidad = capacidad;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacidad;
    }
}

// TreeMap: iterar claves en orden
TreeMap<String, Integer> precios = new TreeMap<>();
precios.put("manzana", 150);
precios.put("banana", 80);
precios.put("naranja", 120);

precios.forEach((k, v) -> System.out.println(k + ": " + v));
// banana: 80, manzana: 150, naranja: 120 (orden alfabético)

10. ¿Qué es `ConcurrentHashMap` y cuándo lo usás?

ConcurrentHashMap es una implementación thread-safe de Map optimizada para alta concurrencia. A diferencia de Hashtable (que sincroniza todos los métodos), usa segment locking (hasta Java 7) y CAS + synchronized por bucket (desde Java 8).

java
// NO hacer esto (HashMap no es thread-safe):
Map<String, Integer> contador = new HashMap<>();
// En múltiples threads: condición de carrera

// Opción 1: ConcurrentHashMap para operaciones atómicas
Map<String, Integer> contadorConcurrente = new ConcurrentHashMap<>();
contadorConcurrente.merge("clave", 1, Integer::sum); // atómico

// Opción 2: computeIfAbsent para inicialización lazy thread-safe
Map<String, List<String>> grupos = new ConcurrentHashMap<>();
grupos.computeIfAbsent("admin", k -> new ArrayList<>()).add("ana");

11. ¿Cuál es la diferencia entre `Iterator` y `ListIterator`?

  • Iterator: Funciona con cualquier Collection. Solo puede avanzar hacia adelante. Tiene hasNext(), next(), remove().
  • ListIterator: Solo para List. Puede avanzar y retroceder. Puede modificar elementos, obtener índices, y agregar elementos.
java
List<String> lista = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));

// Eliminar elementos durante iteración (forma correcta):
Iterator<String> it = lista.iterator();
while (it.hasNext()) {
    String elem = it.next();
    if (elem.equals("b")) it.remove(); // seguro; no ConcurrentModificationException
}

// ListIterator: recorrer en reversa
ListIterator<String> lit = lista.listIterator(lista.size());
while (lit.hasPrevious()) {
    System.out.println(lit.previous());
}

12. ¿Qué son los Generics? ¿Por qué existen?

Los Generics permiten escribir código que funciona con cualquier tipo, con verificación en tiempo de compilación.

Sin generics (antes de Java 5):

java
List lista = new ArrayList();
lista.add("hola");
lista.add(42);
String s = (String) lista.get(1); // ClassCastException en runtime

Con generics:

java
List<String> lista = new ArrayList<>();
lista.add("hola");
// lista.add(42); // error de compilación — falla antes de llegar a producción
String s = lista.get(0); // no necesita cast

Wildcards:

java
// ? extends T: lectura (covariante)
public double sumarNumeros(List<? extends Number> numeros) {
    return numeros.stream().mapToDouble(Number::doubleValue).sum();
}

// ? super T: escritura (contravariante)
public void agregarEnteros(List<? super Integer> lista) {
    lista.add(1);
    lista.add(2);
}

Regla nemónica PECS — Producer Extends, Consumer Super.


Bloque 3: Manejo de excepciones

13. ¿Cuál es la diferencia entre `checked` y `unchecked` exceptions?

  • Checked exceptions: El compilador exige que las manejes (con try-catch o throws). Ejemplo: IOException, SQLException. Representan condiciones recuperables que están fuera del control del programador.
  • Unchecked exceptions: Son subclases de RuntimeException. El compilador no las exige. Representan bugs en el código (índice fuera de rango, NPE, etc.).
java
// Checked: el compilador te obliga a manejarla
public String leerArchivo(String path) throws IOException {
    return Files.readString(Path.of(path)); // IOException es checked
}

// Unchecked: el compilador no obliga
public int dividir(int a, int b) {
    if (b == 0) throw new IllegalArgumentException("Divisor no puede ser cero");
    return a / b;
}

En Java moderno: Muchos frameworks como Spring prefieren unchecked exceptions para no contaminar las firmas de métodos. Es una decisión de diseño válida — las checked exceptions son útiles cuando el llamador *puede hacer algo* al respecto.


14. ¿Qué es `try-with-resources`?

try-with-resources cierra automáticamente cualquier recurso que implemente AutoCloseable al terminar el bloque, incluso si hay una excepción.

java
// Antes: cerrar manualmente en finally
Connection conn = null;
try {
    conn = dataSource.getConnection();
    // ... usar conn
} finally {
    if (conn != null) {
        try { conn.close(); } catch (SQLException e) { /* ignorar? */ }
    }
}

// Con try-with-resources: más limpio y seguro
try (Connection conn = dataSource.getConnection();
     PreparedStatement stmt = conn.prepareStatement("SELECT * FROM usuarios")) {

    ResultSet rs = stmt.executeQuery();
    while (rs.next()) {
        System.out.println(rs.getString("nombre"));
    }
} // conn y stmt se cierran automáticamente, incluso si hay excepción

Podés crear tus propios recursos:

java
public class Temporizador implements AutoCloseable {
    private final long inicio = System.currentTimeMillis();
    private final String nombre;

    public Temporizador(String nombre) { this.nombre = nombre; }

    @Override
    public void close() {
        long duracion = System.currentTimeMillis() - inicio;
        System.out.println(nombre + " tardó " + duracion + "ms");
    }
}

try (Temporizador t = new Temporizador("consulta")) {
    // ... código a medir
}

15. ¿Cuándo crear excepciones propias?

Creás excepciones propias cuando:

  1. 1Querés dar contexto semántico específico del dominio.
  2. 2Querés agrupar excepciones relacionadas.
  3. 3Necesitás agregar información extra (códigos de error, contexto del negocio).
java
public class PagoException extends RuntimeException {
    private final String codigoError;
    private final String ultimosCuatroDigitos;

    public PagoException(String mensaje, String codigoError, String ultimosCuatroDigitos) {
        super(mensaje);
        this.codigoError = codigoError;
        this.ultimosCuatroDigitos = ultimosCuatroDigitos;
    }

    public PagoException(String mensaje, Throwable causa, String codigoError) {
        super(mensaje, causa); // preservar la causa original es importante
        this.codigoError = codigoError;
        this.ultimosCuatroDigitos = "????";
    }

    // getters...
}

Bloque 4: Java 8+ — Streams, Lambdas, Optional

16. ¿Qué son las lambdas en Java? ¿Para qué sirven?

Las lambdas son funciones anónimas que se pueden pasar como argumentos. Requieren una interfaz funcional (interfaz con un solo método abstracto).

java
// Sin lambda (clase anónima):
List<String> nombres = Arrays.asList("Carlos", "Ana", "Beatriz");
nombres.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

// Con lambda:
nombres.sort((a, b) -> a.compareTo(b));

// Con method reference (aún más conciso):
nombres.sort(String::compareTo);

// Interfaces funcionales del JDK:
Predicate<String> esLargo = s -> s.length() > 5;
Function<String, Integer> longitud = String::length;
Consumer<String> imprimir = System.out::println;
Supplier<List<String>> nuevaLista = ArrayList::new;

17. Explicá la Stream API con ejemplos reales

La Stream API permite procesar colecciones de forma declarativa y funcional. Las operaciones son lazy — no se ejecutan hasta que hay una operación terminal.

java
List<Empleado> empleados = obtenerEmpleados();

// Ejemplo complejo: salario promedio por departamento de empleados con más de 3 años
Map<String, Double> salarioPorDepto = empleados.stream()
    .filter(e -> e.getAnosEnEmpresa() > 3)
    .collect(Collectors.groupingBy(
        Empleado::getDepartamento,
        Collectors.averagingDouble(Empleado::getSalario)
    ));

// flatMap: aplanar colecciones anidadas
List<List<String>> listas = List.of(
    List.of("a", "b"),
    List.of("c", "d"),
    List.of("e")
);
List<String> plana = listas.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());
// ["a", "b", "c", "d", "e"]

// Streams paralelos: úsalos cuando el procesamiento es costoso y los datos son independientes
long count = empleados.parallelStream()
    .filter(e -> e.getSalario() > 100_000)
    .count();

Operaciones intermedias (lazy): filter, map, flatMap, distinct, sorted, limit, peek

Operaciones terminales (ejecutan el pipeline): collect, forEach, count, findFirst, anyMatch, reduce


18. ¿Qué es `Optional` y cuándo usarlo?

Optional es un contenedor que puede o no tener un valor. Su propósito es hacer explícita la posibilidad de ausencia de valor en las firmas de métodos.

java
// MAL: devolver null obliga al llamador a saber que puede ser null
public Usuario buscarPorEmail(String email) {
    return repo.findByEmail(email); // puede ser null — el llamador no lo sabe
}

// BIEN: Optional comunica la posibilidad de ausencia
public Optional<Usuario> buscarPorEmail(String email) {
    return Optional.ofNullable(repo.findByEmail(email));
}

// Usar Optional correctamente:
Optional<Usuario> usuario = buscarPorEmail("ana@ejemplo.com");

// orElse: valor por defecto
Usuario u = usuario.orElse(Usuario.invitado());

// orElseGet: lazy (solo evalúa si está vacío)
Usuario u2 = usuario.orElseGet(() -> crearUsuarioNuevo(email));

// orElseThrow: lanzar excepción personalizada
Usuario u3 = usuario.orElseThrow(() -> new UsuarioNoEncontradoException(email));

// map + filter: encadenar transformaciones
String nombre = usuario
    .filter(User::estaActivo)
    .map(Usuario::getNombre)
    .orElse("Invitado");

Reglas de uso:

  • Usá Optional como tipo de retorno cuando la ausencia es un resultado legítimo.
  • No usés Optional como parámetro de método (usá sobrecarga o un boolean).
  • No usés Optional en campos de clase.
  • Nunca llamés .get() sin verificar .isPresent() antes.

19. ¿Qué son los métodos default en interfaces?

Desde Java 8, las interfaces pueden tener métodos con implementación usando la keyword default. Esto permite agregar métodos a interfaces sin romper las implementaciones existentes.

java
public interface Repositorio<T, ID> {
    Optional<T> findById(ID id);
    void save(T entity);
    void delete(ID id);

    // Método default: implementación base que las clases pueden sobreescribir
    default boolean exists(ID id) {
        return findById(id).isPresent();
    }

    default void saveAll(List<T> entities) {
        entities.forEach(this::save);
    }
}

Conflicto de diamante: Si una clase implementa dos interfaces con el mismo método default, debe sobreescribirlo.

java
interface A { default String saludo() { return "Hola desde A"; } }
interface B { default String saludo() { return "Hola desde B"; } }

class C implements A, B {
    @Override
    public String saludo() {
        return A.super.saludo(); // especificar cuál usar
    }
}

20. ¿Cómo funciona `CompletableFuture`?

CompletableFuture permite composición de operaciones asíncronas sin bloquear threads.

java
// Operación asíncrona básica
CompletableFuture<Usuario> futureUsuario = CompletableFuture
    .supplyAsync(() -> repoUsuarios.findById(1L));

// Encadenar transformaciones
CompletableFuture<String> futureNombre = futureUsuario
    .thenApply(usuario -> usuario.getNombre().toUpperCase())
    .thenApply(nombre -> "Hola, " + nombre);

// Combinar dos futuros independientes
CompletableFuture<Usuario> futureU = CompletableFuture.supplyAsync(() -> obtenerUsuario(id));
CompletableFuture<List<Orden>> futureO = CompletableFuture.supplyAsync(() -> obtenerOrdenes(id));

CompletableFuture<ResumenUsuario> resumen = futureU.thenCombine(futureO,
    (usuario, ordenes) -> new ResumenUsuario(usuario, ordenes)
);

// Manejar errores
CompletableFuture<String> resultado = CompletableFuture
    .supplyAsync(() -> llamadaQuePodriaFallar())
    .exceptionally(ex -> "valor por defecto cuando falla: " + ex.getMessage())
    .thenApply(String::toUpperCase);

// Esperar múltiples futuros
CompletableFuture.allOf(futureU, futureO).join();

Bloque 5: Concurrencia

21. ¿Cuál es la diferencia entre un proceso y un thread?

  • Proceso: Instancia de un programa. Tiene su propio espacio de memoria, archivos, y recursos. La comunicación entre procesos (IPC) es costosa.
  • Thread: Unidad de ejecución dentro de un proceso. Comparte memoria con otros threads del mismo proceso. La comunicación es directa pero requiere sincronización.

En Java:

java
// Thread básico — dos formas:

// 1. Extender Thread
class MiThread extends Thread {
    @Override
    public void run() {
        System.out.println("Ejecutando en: " + Thread.currentThread().getName());
    }
}

// 2. Implementar Runnable (preferido — no gasta la herencia)
Runnable tarea = () -> System.out.println("Ejecutando en: " + Thread.currentThread().getName());
Thread t = new Thread(tarea);
t.start();

// 3. Usar ExecutorService (recomendado en producción)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> procesarPedido(pedidoId));
executor.shutdown();

22. ¿Qué es `synchronized` y qué problemas resuelve?

synchronized garantiza que solo un thread a la vez ejecuta un bloque de código, usando el monitor del objeto como lock.

java
public class Contador {
    private int valor = 0;

    // Sin synchronized: condición de carrera
    // public void incrementar() { valor++; } // NO es atómico: read → increment → write

    // Con synchronized en método:
    public synchronized void incrementar() {
        valor++;
    }

    // Con synchronized en bloque (más granular):
    public void incrementarBloque() {
        synchronized (this) {
            valor++;
        }
    }

    // Static synchronized: usa el lock de la clase, no del objeto
    private static int contadorGlobal = 0;
    public static synchronized void incrementarGlobal() {
        contadorGlobal++;
    }
}

Problema: synchronized bloquea el thread. Si el lock tarda, todos los threads que lo esperan se bloquean — reduce el throughput.

Alternativa moderna:

java
// AtomicInteger: operaciones atómicas sin locks (CAS)
AtomicInteger contador = new AtomicInteger(0);
contador.incrementAndGet(); // atómico y más rápido que synchronized

23. ¿Qué es un deadlock? ¿Cómo lo prevenís?

Un deadlock ocurre cuando dos (o más) threads esperan recursos que el otro tiene, indefinidamente.

java
// Ejemplo de deadlock:
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        Thread.sleep(100); // simula trabajo
        synchronized (lockB) { /* necesita B */ }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lockB) {
        Thread.sleep(100);
        synchronized (lockA) { /* necesita A */ }
    }
});
// t1 tiene A, espera B. t2 tiene B, espera A. Deadlock.

Estrategias de prevención:

  1. 1Orden consistente de locks: Siempre adquirí los locks en el mismo orden.
java
// Ambos threads adquieren en orden A → B
synchronized (lockA) {
    synchronized (lockB) { /* seguro */ }
}
  1. 2Timeout con tryLock:
java
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

boolean adquirido = lockA.tryLock(1, TimeUnit.SECONDS);
if (adquirido) {
    try {
        if (lockB.tryLock(1, TimeUnit.SECONDS)) {
            try { /* trabajo */ } finally { lockB.unlock(); }
        }
    } finally { lockA.unlock(); }
}

24. ¿Qué es `volatile` en Java?

volatile garantiza que las escrituras a una variable sean visibles para todos los threads inmediatamente — no usa caché de CPU local.

java
public class Configuracion {
    private volatile boolean activo = true;

    // Sin volatile: otro thread podría ver el valor cacheado en su CPU
    // Con volatile: siempre lee de la memoria principal

    public void detener() {
        activo = false;
    }

    public void ejecutar() {
        while (activo) { // siempre ve el valor actualizado
            procesarTarea();
        }
    }
}

volatile NO es suficiente para operaciones compuestas: activo = !activo sigue siendo una condición de carrera. Para eso usás AtomicBoolean.


25. ¿Qué es `ExecutorService` y por qué usarlo en lugar de crear threads directamente?

Crear threads directamente es costoso y peligroso (no hay límite, consume recursos sin control). ExecutorService gestiona un pool de threads reutilizables.

java
// Pool de tamaño fijo
ExecutorService fixedPool = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);

// Pool que crece según demanda (cuidado: puede crear demasiados threads)
ExecutorService cachedPool = Executors.newCachedThreadPool();

// Thread único — garantiza ejecución secuencial
ExecutorService singleThread = Executors.newSingleThreadExecutor();

// Para tareas programadas
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
scheduled.scheduleAtFixedRate(() -> verificarSalud(), 0, 30, TimeUnit.SECONDS);

// Enviar tarea y obtener Future
Future<String> resultado = fixedPool.submit(() -> {
    return llamadaAPISincrona(); // bloquea el thread del pool, no el principal
});

String valor = resultado.get(5, TimeUnit.SECONDS); // espera máximo 5 segundos

// Siempre cerrar el pool al terminar
fixedPool.shutdown();
fixedPool.awaitTermination(60, TimeUnit.SECONDS);

Bloque 6: JVM y Gestión de memoria

26. ¿Cómo funciona la JVM? Explicá sus componentes principales.

La JVM (Java Virtual Machine) tiene tres subsistemas principales:

1. Class Loader: Carga clases .class en memoria. Sigue el modelo de delegación al parent classloader primero.

2. Memory Areas (Runtime Data Areas):

  • Heap: Almacena objetos e instancias. Dividido en Young Generation (Eden + Survivor) y Old Generation. Aquí actúa el GC.
  • Stack: Cada thread tiene su propio stack. Almacena frames con variables locales y operandos.
  • Method Area (Metaspace en Java 8+): Metadata de clases, constantes, código de métodos.
  • PC Register: Contador de programa por thread.
  • Native Method Stack: Para métodos nativos (JNI).

3. Execution Engine:

  • Interpreter: Ejecuta bytecode instrucción por instrucción.
  • JIT Compiler: Compila a código nativo el código "hot" (frecuentemente ejecutado) para mayor velocidad.
  • Garbage Collector: Libera memoria de objetos no referenciados.
Proceso simplificado:
Código .java → javac → bytecode .class → JVM → código nativo (vía JIT)

27. ¿Cómo funciona el Garbage Collector en Java?

El GC gestiona automáticamente la memoria del heap. El algoritmo generacional se basa en la observación de que la mayoría de objetos mueren jóvenes.

Fases:

  1. 1Minor GC: Limpia la Young Generation. Los objetos que sobreviven se mueven al Survivor Space, y eventualmente a Old Generation.
  2. 2Major/Full GC: Limpia la Old Generation. Más lento — causa stop-the-world pauses.

Tipos de GC:

  • Serial GC: Un solo thread. Para aplicaciones pequeñas.
  • Parallel GC: Múltiples threads para GC. Throughput optimizado.
  • G1 GC (default desde Java 9): Divide el heap en regiones. Predecible en latencia.
  • ZGC / Shenandoah: Pauses sub-milisegundo para heaps grandes (Java 15+).
java
// Hints (el GC puede ignorarlos):
System.gc(); // sugiere una colección — no garantiza nada

// Monitoreo:
// Flags de JVM útiles:
// -Xms512m -Xmx2g — heap mínimo y máximo
// -XX:+UseG1GC — activar G1
// -XX:+PrintGCDetails — logs de GC
// -Xss512k — tamaño del stack por thread

28. ¿Qué es una memory leak en Java? ¿Cómo la detectás?

En Java una memory leak ocurre cuando los objetos no se usan pero siguen referenciados, impidiendo que el GC los limpie.

Causas comunes:

java
// 1. Colecciones estáticas que crecen sin límite
public class Cache {
    private static final Map<String, Object> CACHE = new HashMap<>();

    public static void guardar(String key, Object val) {
        CACHE.put(key, val); // nunca se limpia — leak
    }
}

// 2. Listeners no removidos
button.addActionListener(this);
// Si this tiene referencia al button y no se remueve, ninguno puede ser GC'd

// 3. Closures que capturan referencias grandes
// 4. ThreadLocal no removido en thread pools
try {
    threadLocal.set(nuevoDato);
    // ... trabajo
} finally {
    threadLocal.remove(); // IMPORTANTE en thread pools
}

Detección: VisualVM, JProfiler, Eclipse MAT, -XX:+HeapDumpOnOutOfMemoryError.


29. ¿Qué son las referencias débiles (`WeakReference`, `SoftReference`, `PhantomReference`)?

  • StrongReference: La referencia normal. El objeto no se colecta mientras haya una strong reference.
  • WeakReference: El GC puede colectar el objeto incluso si hay weak references. Útil para caches que no deben impedir la colección.
  • SoftReference: Se colecta solo cuando hay presión de memoria. Ideal para caches que prefieren mantener objetos si hay memoria disponible.
  • PhantomReference: Se usa para limpiezas post-mortem. El objeto ya fue finalizado.
java
// WeakHashMap: clave se colecta automáticamente cuando no hay strong refs
WeakHashMap<Object, String> cache = new WeakHashMap<>();

Object clave = new Object();
cache.put(clave, "datos");
System.out.println(cache.get(clave)); // "datos"

clave = null; // no hay más strong references a la clave
System.gc(); // sugiere GC
// La entrada puede haberse eliminado del mapa

Bloque 7: Design Patterns

30. Implementá el patrón Singleton correctamente en Java

El Singleton garantiza una única instancia de una clase.

java
// Opción 1: Initialization-on-demand holder (thread-safe, lazy, sin synchronized)
public class Configuracion {
    private Configuracion() {}

    private static class Holder {
        private static final Configuracion INSTANCIA = new Configuracion();
    }

    public static Configuracion getInstancia() {
        return Holder.INSTANCIA;
    }
}

// Opción 2: Enum (la forma más robusta — serialization-safe, reflection-safe)
public enum DatabasePool {
    INSTANCIA;

    private final ConnectionPool pool = new ConnectionPool();

    public Connection getConexion() {
        return pool.obtener();
    }
}
DatabasePool.INSTANCIA.getConexion();

// Opción 3: Double-checked locking (cuando necesitás lazy init con herencia)
public class CacheServicio {
    private volatile static CacheServicio instancia; // volatile es crítico

    private CacheServicio() {}

    public static CacheServicio getInstancia() {
        if (instancia == null) {
            synchronized (CacheServicio.class) {
                if (instancia == null) { // segundo check dentro del synchronized
                    instancia = new CacheServicio();
                }
            }
        }
        return instancia;
    }
}

31. Explicá el patrón Builder con código

Builder separa la construcción de un objeto complejo de su representación.

java
public class Pedido {
    private final String clienteId;
    private final List<LineaPedido> lineas;
    private final String direccionEntrega;
    private final MetodoPago metodoPago;
    private final LocalDate fechaEntregaDeseada;
    private final String notasEspeciales;

    private Pedido(Builder builder) {
        this.clienteId = builder.clienteId;
        this.lineas = List.copyOf(builder.lineas);
        this.direccionEntrega = builder.direccionEntrega;
        this.metodoPago = builder.metodoPago;
        this.fechaEntregaDeseada = builder.fechaEntregaDeseada;
        this.notasEspeciales = builder.notasEspeciales;
    }

    public static class Builder {
        // Campos obligatorios
        private final String clienteId;
        private final List<LineaPedido> lineas;

        // Campos opcionales con defaults
        private MetodoPago metodoPago = MetodoPago.TARJETA;
        private String direccionEntrega;
        private LocalDate fechaEntregaDeseada = LocalDate.now().plusDays(3);
        private String notasEspeciales = "";

        public Builder(String clienteId, List<LineaPedido> lineas) {
            this.clienteId = Objects.requireNonNull(clienteId);
            this.lineas = Objects.requireNonNull(lineas);
        }

        public Builder metodoPago(MetodoPago metodoPago) {
            this.metodoPago = metodoPago;
            return this;
        }

        public Builder direccionEntrega(String direccion) {
            this.direccionEntrega = direccion;
            return this;
        }

        public Builder fechaEntrega(LocalDate fecha) {
            this.fechaEntregaDeseada = fecha;
            return this;
        }

        public Builder notas(String notas) {
            this.notasEspeciales = notas;
            return this;
        }

        public Pedido build() {
            if (direccionEntrega == null) throw new IllegalStateException("Dirección requerida");
            return new Pedido(this);
        }
    }
}

// Uso:
Pedido pedido = new Pedido.Builder("cliente-123", lineas)
    .direccionEntrega("Av. Corrientes 1234, CABA")
    .metodoPago(MetodoPago.MERCADOPAGO)
    .notas("Dejar con el portero")
    .build();

32. ¿Cómo funciona el patrón Observer en Java?

Observer define una relación uno-a-muchos: cuando un objeto cambia de estado, notifica a todos sus dependientes automáticamente.

java
// Interfaz Observer
public interface Observer<T> {
    void onUpdate(T valor);
}

// Observable genérico
public class Observable<T> {
    private T valor;
    private final List<Observer<T>> observers = new CopyOnWriteArrayList<>();

    public void agregar(Observer<T> o) { observers.add(o); }
    public void remover(Observer<T> o) { observers.remove(o); }

    public void setValor(T nuevoValor) {
        this.valor = nuevoValor;
        notificar();
    }

    private void notificar() {
        observers.forEach(o -> o.onUpdate(valor));
    }
}

// Uso con lambdas:
Observable<Double> precioAccion = new Observable<>();

precioAccion.agregar(precio -> {
    if (precio > 100) System.out.println("VENDER a " + precio);
});
precioAccion.agregar(precio -> {
    if (precio < 50) System.out.println("COMPRAR a " + precio);
});

precioAccion.setValor(120.0); // VENDER a 120.0
precioAccion.setValor(45.0);  // COMPRAR a 45.0

Bloque 8: Spring Framework (preguntas frecuentes)

33. ¿Qué es la inyección de dependencias (DI) y cómo la implementa Spring?

DI es un patrón donde un objeto recibe sus dependencias desde afuera en lugar de crearlas internamente. Spring gestiona esto a través de su contenedor IoC (Inversion of Control).

java
// Sin DI: acoplamiento fuerte
public class PedidoService {
    private EmailService emailService = new EmailService(); // acoplado
    private PedidoRepository repo = new PedidoRepository(); // acoplado
}

// Con DI (Spring):
@Service
public class PedidoService {
    private final EmailService emailService;
    private final PedidoRepository repo;

    // Constructor injection: preferida — hace las dependencias explícitas
    @Autowired // opcional desde Spring 4.3 si hay un solo constructor
    public PedidoService(EmailService emailService, PedidoRepository repo) {
        this.emailService = emailService;
        this.repo = repo;
    }

    public Pedido crearPedido(PedidoRequest request) {
        Pedido pedido = repo.save(new Pedido(request));
        emailService.enviarConfirmacion(pedido);
        return pedido;
    }
}

Tipos de inyección en Spring:

  • Constructor: Recomendada. Las dependencias son inmutables, el objeto no puede construirse en estado inválido.
  • Setter: Para dependencias opcionales.
  • Campo (@Autowired directo): Conveniente pero dificulta los tests.

34. ¿Cuáles son los scopes de los beans en Spring?

  • singleton (default): Una instancia por ApplicationContext.
  • prototype: Nueva instancia cada vez que se solicita.
  • request: Una instancia por HTTP request (web).
  • session: Una instancia por HTTP session (web).
java
@Component
@Scope("prototype") // nueva instancia cada vez
public class ReporteBuilder {
    private final StringBuilder contenido = new StringBuilder();
    // Si fuera singleton, el contenido se compartiría entre threads — bug
}

// Inyectar un prototype en un singleton correctamente:
@Component
public class ReporteService {
    @Autowired
    private ApplicationContext context;

    public String generarReporte(String tipo) {
        ReporteBuilder builder = context.getBean(ReporteBuilder.class); // nueva instancia
        return builder.construir(tipo);
    }
}

35. ¿Cómo funciona `@Transactional` en Spring?

@Transactional dice a Spring que envuelva el método en una transacción de base de datos. Spring usa un proxy AOP para esto.

java
@Service
public class TransferenciaService {

    @Transactional // toda la operación es atómica
    public void transferir(Long origen, Long destino, BigDecimal monto) {
        Cuenta cuentaOrigen = cuentaRepo.findById(origen)
            .orElseThrow(() -> new CuentaNoEncontradaException(origen));

        Cuenta cuentaDestino = cuentaRepo.findById(destino)
            .orElseThrow(() -> new CuentaNoEncontradaException(destino));

        if (cuentaOrigen.getSaldo().compareTo(monto) < 0) {
            throw new SaldoInsuficienteException(); // hace rollback automáticamente
        }

        cuentaOrigen.debitar(monto);
        cuentaDestino.acreditar(monto);

        cuentaRepo.save(cuentaOrigen);
        cuentaRepo.save(cuentaDestino);
        // Si cualquier operación falla, todo se revierte
    }
}

Gotchas importantes:

  • @Transactional solo funciona en métodos públicos.
  • Si un método @Transactional llama a otro método @Transactional en la *misma clase*, el segundo no crea una transacción nueva (problema del proxy self-invocation).
  • Por defecto solo hace rollback en RuntimeException. Para checked exceptions: @Transactional(rollbackFor = Exception.class).

Bloque 9: Preguntas de código y algoritmos

36. Encontrá el primer elemento no repetido en un String

java
public char primerNoRepetido(String s) {
    Map<Character, Integer> frecuencia = new LinkedHashMap<>(); // mantiene orden de inserción

    for (char c : s.toCharArray()) {
        frecuencia.merge(c, 1, Integer::sum);
    }

    return frecuencia.entrySet().stream()
        .filter(e -> e.getValue() == 1)
        .map(Map.Entry::getKey)
        .findFirst()
        .orElseThrow(() -> new NoSuchElementException("Todos los caracteres se repiten"));
}

// primerNoRepetido("aabcdd") → 'b'
// primerNoRepetido("aabb") → NoSuchElementException

37. Invertí una lista enlazada

java
// Nodo de lista enlazada
public class Nodo<T> {
    T valor;
    Nodo<T> siguiente;
    Nodo(T valor) { this.valor = valor; }
}

// Inversión iterativa: O(n) tiempo, O(1) espacio
public <T> Nodo<T> invertir(Nodo<T> cabeza) {
    Nodo<T> anterior = null;
    Nodo<T> actual = cabeza;

    while (actual != null) {
        Nodo<T> siguiente = actual.siguiente;
        actual.siguiente = anterior;
        anterior = actual;
        actual = siguiente;
    }

    return anterior; // nueva cabeza
}

// Inversión recursiva: O(n) tiempo, O(n) espacio por el stack
public <T> Nodo<T> invertirRecursivo(Nodo<T> cabeza) {
    if (cabeza == null || cabeza.siguiente == null) return cabeza;

    Nodo<T> nuevaCabeza = invertirRecursivo(cabeza.siguiente);
    cabeza.siguiente.siguiente = cabeza;
    cabeza.siguiente = null;

    return nuevaCabeza;
}

38. Implementá un productor-consumidor con `BlockingQueue`

java
public class ProductorConsumidor {
    private static final int CAPACIDAD = 10;
    private final BlockingQueue<Integer> cola = new ArrayBlockingQueue<>(CAPACIDAD);
    private final AtomicBoolean corriendo = new AtomicBoolean(true);

    class Productor implements Runnable {
        @Override
        public void run() {
            int i = 0;
            while (corriendo.get()) {
                try {
                    cola.put(i++); // bloquea si la cola está llena
                    System.out.println("Producido: " + (i - 1));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
    }

    class Consumidor implements Runnable {
        @Override
        public void run() {
            while (corriendo.get() || !cola.isEmpty()) {
                try {
                    Integer item = cola.poll(500, TimeUnit.MILLISECONDS); // timeout para no bloquear indefinido
                    if (item != null) {
                        System.out.println("Consumido: " + item);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ProductorConsumidor pc = new ProductorConsumidor();
        ExecutorService executor = Executors.newFixedThreadPool(3);

        executor.submit(pc.new Productor());
        executor.submit(pc.new Consumidor());
        executor.submit(pc.new Consumidor());

        Thread.sleep(5000);
        pc.corriendo.set(false);
        executor.shutdown();
    }
}

39. Implementá un LRU Cache desde cero

java
public class LRUCache<K, V> {
    private final int capacidad;
    private final Map<K, Node<K, V>> mapa = new HashMap<>();
    private final Node<K, V> cabeza = new Node<>(null, null); // dummy
    private final Node<K, V> cola = new Node<>(null, null);   // dummy

    public LRUCache(int capacidad) {
        this.capacidad = capacidad;
        cabeza.siguiente = cola;
        cola.anterior = cabeza;
    }

    public V get(K key) {
        Node<K, V> nodo = mapa.get(key);
        if (nodo == null) return null;
        moverAlFrente(nodo);
        return nodo.valor;
    }

    public void put(K key, V valor) {
        Node<K, V> nodo = mapa.get(key);
        if (nodo != null) {
            nodo.valor = valor;
            moverAlFrente(nodo);
        } else {
            Node<K, V> nuevo = new Node<>(key, valor);
            mapa.put(key, nuevo);
            agregarAlFrente(nuevo);
            if (mapa.size() > capacidad) {
                Node<K, V> eliminado = removerDesdeAtras();
                mapa.remove(eliminado.clave);
            }
        }
    }

    private void moverAlFrente(Node<K, V> nodo) {
        remover(nodo);
        agregarAlFrente(nodo);
    }

    private void agregarAlFrente(Node<K, V> nodo) {
        nodo.anterior = cabeza;
        nodo.siguiente = cabeza.siguiente;
        cabeza.siguiente.anterior = nodo;
        cabeza.siguiente = nodo;
    }

    private void remover(Node<K, V> nodo) {
        nodo.anterior.siguiente = nodo.siguiente;
        nodo.siguiente.anterior = nodo.anterior;
    }

    private Node<K, V> removerDesdeAtras() {
        Node<K, V> nodo = cola.anterior;
        remover(nodo);
        return nodo;
    }

    private static class Node<K, V> {
        K clave;
        V valor;
        Node<K, V> anterior, siguiente;
        Node(K clave, V valor) { this.clave = clave; this.valor = valor; }
    }
}

40. Encontrá todos los pares de una lista que sumen un objetivo dado

java
public List<int[]> pareSuma(int[] nums, int objetivo) {
    List<int[]> resultado = new ArrayList<>();
    Set<Integer> vistos = new HashSet<>();

    for (int num : nums) {
        int complemento = objetivo - num;
        if (vistos.contains(complemento)) {
            resultado.add(new int[]{complemento, num});
        }
        vistos.add(num);
    }

    return resultado;
}

// pareSuma([1, 5, 3, 7, 4, 6], 10) → [[3,7], [4,6]]
// O(n) tiempo, O(n) espacio

Bloque 10: Preguntas avanzadas

41. ¿Qué es la programación funcional en Java? ¿Cómo se relaciona con las lambdas?

Java adoptó elementos de programación funcional desde Java 8. Los conceptos clave son:

  • Funciones como ciudadanos de primera clase: Podés pasar funciones como argumentos y retornarlas.
  • Inmutabilidad: Preferir objetos que no mutan.
  • Sin efectos secundarios (pure functions): El output depende solo del input.
java
// Higher-order functions: funciones que toman o devuelven funciones
public static <T, R> Function<T, R> memorizar(Function<T, R> funcion) {
    Map<T, R> cache = new ConcurrentHashMap<>();
    return input -> cache.computeIfAbsent(input, funcion);
}

Function<Integer, Long> fibonacci = memorizar(n -> {
    if (n <= 1) return (long) n;
    return /* llamada recursiva */ (long) n; // simplificado
});

// Composición de funciones
Function<String, String> trim = String::trim;
Function<String, String> mayusculas = String::toUpperCase;
Function<String, String> limpiar = trim.andThen(mayusculas);

System.out.println(limpiar.apply("  hola mundo  ")); // "HOLA MUNDO"

// Function composition en pipelines
List<String> emails = usuarios.stream()
    .map(Usuario::getEmail)
    .filter(email -> email.endsWith(".com"))
    .map(String::toLowerCase)
    .distinct()
    .sorted()
    .collect(Collectors.toList());

42. ¿Qué es la programación reactiva? ¿Cómo se relaciona con Java?

La programación reactiva maneja flujos de datos asíncronos con backpressure. En el ecosistema Java, las implementaciones más comunes son Project Reactor (usado en Spring WebFlux) y RxJava.

java
// Con Spring WebFlux (Reactor):
@GetMapping("/usuarios/{id}/pedidos")
public Flux<Pedido> obtenerPedidos(@PathVariable Long id) {
    return usuarioService.buscarPorId(id)        // Mono<Usuario>
        .flatMapMany(u -> pedidoService.buscarPorUsuario(u.getId())) // Flux<Pedido>
        .filter(p -> p.getEstado() == EstadoPedido.ACTIVO)
        .take(20)
        .onErrorResume(ex -> Flux.empty()); // error handling sin bloquear
}

// Diferencia clave con CompletableFuture:
// Mono<T> = 0 o 1 elemento (equivalente a CompletableFuture<Optional<T>>)
// Flux<T> = 0 a N elementos con backpressure

43. ¿Qué es el patrón Circuit Breaker? ¿Por qué importa en microservicios?

El Circuit Breaker previene que errores en cascada destruyan un sistema distribuido. Tiene tres estados: Cerrado (funciona normal), Abierto (rechaza requests sin intentarlos), Semi-abierto (prueba si el servicio se recuperó).

java
// Con Resilience4j:
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)           // abre si >50% de calls fallan
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .permittedNumberOfCallsInHalfOpenState(5)
    .build();

CircuitBreaker cb = CircuitBreaker.of("pagos", config);

// Decorar el servicio:
Supplier<Pago> callProtegido = CircuitBreaker.decorateSupplier(cb,
    () -> servicioPagoExterno.procesar(pago)
);

Try<Pago> resultado = Try.ofSupplier(callProtegido)
    .recover(CallNotPermittedException.class, ex -> Pago.fallback());

44. ¿Cómo implementarías un rate limiter en Java?

java
// Token Bucket algorithm
public class RateLimiter {
    private final int maxTokens;
    private final double tokensPerSegundo;
    private double tokens;
    private long ultimaRecarga;
    private final Object lock = new Object();

    public RateLimiter(int maxTokens, double tokensPerSegundo) {
        this.maxTokens = maxTokens;
        this.tokensPerSegundo = tokensPerSegundo;
        this.tokens = maxTokens;
        this.ultimaRecarga = System.nanoTime();
    }

    public boolean permitir() {
        synchronized (lock) {
            recargar();
            if (tokens >= 1) {
                tokens -= 1;
                return true;
            }
            return false;
        }
    }

    private void recargar() {
        long ahora = System.nanoTime();
        double segundosTranscurridos = (ahora - ultimaRecarga) / 1e9;
        tokens = Math.min(maxTokens, tokens + segundosTranscurridos * tokensPerSegundo);
        ultimaRecarga = ahora;
    }
}

// Con Guava (production-ready):
RateLimiter limiter = RateLimiter.create(100); // 100 requests/segundo
if (limiter.tryAcquire()) {
    procesarRequest();
} else {
    responderConError(429, "Demasiadas peticiones");
}

45. ¿Cómo funciona la serialización en Java? ¿Cuáles son sus problemas?

java
// Serialización básica
public class Sesion implements Serializable {
    private static final long serialVersionUID = 1L; // control de versiones

    private String usuarioId;
    private Date expiracion;
    private transient String tokenSensible; // no se serializa

    // ... constructores, getters
}

// Guardar a archivo:
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("sesion.dat"))) {
    oos.writeObject(sesion);
}

// Leer:
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("sesion.dat"))) {
    Sesion sesionLeida = (Sesion) ois.readObject();
}

Problemas de la serialización Java:

  1. 1Performance: Muy lenta comparada con JSON/Protobuf.
  2. 2Seguridad: La deserialización puede ejecutar código arbitrario (vulnerabilidad conocida).
  3. 3Frágil ante cambios de clase.

Alternativas modernas: Jackson (JSON), Protocol Buffers, Kryo.


46. ¿Qué es la reflexión (Reflection) en Java? ¿Cuándo usarla?

Reflection permite inspeccionar y manipular clases, métodos y campos en tiempo de ejecución.

java
// Inspeccionar una clase:
Class<?> clazz = Class.forName("com.ejemplo.Usuario");

// Listar métodos públicos
Arrays.stream(clazz.getMethods())
    .map(Method::getName)
    .forEach(System.out::println);

// Invocar método privado (útil en tests):
Method metodoPrivado = MiClase.class.getDeclaredMethod("calcularDescuento", double.class);
metodoPrivado.setAccessible(true);
double resultado = (double) metodoPrivado.invoke(instancia, 100.0);

// Crear instancia sin constructor conocido:
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object instancia = constructor.newInstance("argumento");

Cuándo usarla: Frameworks (Spring, Hibernate, Jackson), plugins dinámicos, herramientas de testing. Evitarla en código de negocio: rompe encapsulamiento, es más lenta, no es type-safe.


47. ¿Qué son los módulos en Java (Java 9+)?

El sistema de módulos (Project Jigsaw) divide el JDK y las aplicaciones en módulos con dependencias explícitas y encapsulamiento fuerte.

java
// module-info.java — declara el módulo
module com.ejemplo.app {
    requires java.net.http;         // dependencia del JDK
    requires com.fasterxml.jackson; // dependencia externa

    exports com.ejemplo.app.api;    // qué packages expone
    exports com.ejemplo.app.model to com.ejemplo.test; // export dirigido

    opens com.ejemplo.app.model to com.fasterxml.jackson.databind; // para reflection
}

Beneficios: Encapsulamiento real (los paquetes no exportados son inaccesibles), JVM más rápida (sabe exactamente qué clases se necesitan), imágenes de runtime más pequeñas con jlink.


48. ¿Qué son los Records en Java (Java 16+)?

Records son clases de datos inmutables que reducen el boilerplate.

java
// Antes:
public class Punto {
    private final int x;
    private final int y;
    public Punto(int x, int y) { this.x = x; this.y = y; }
    public int getX() { return x; }
    public int getY() { return y; }
    @Override public boolean equals(Object o) { /* 10 líneas */ }
    @Override public int hashCode() { /* 5 líneas */ }
    @Override public String toString() { /* 5 líneas */ }
}

// Con Record:
public record Punto(int x, int y) {}
// El compilador genera automáticamente: constructor canónico, getters (x(), y()), equals, hashCode, toString

Punto p = new Punto(3, 4);
System.out.println(p.x());  // 3
System.out.println(p);      // Punto[x=3, y=4]

// Compact constructor para validación:
public record Rango(int min, int max) {
    public Rango {
        if (min > max) throw new IllegalArgumentException("min debe ser <= max");
    }
}

49. ¿Qué son los Sealed Classes en Java (Java 17+)?

Las clases selladas restringen qué clases pueden extenderlas, modelando jerarquías algebraicas de tipos.

java
// Jerarquía sellada para resultados de operación
public sealed interface ResultadoOperacion<T>
    permits Exito, Fallo, EnProceso {
}

public record Exito<T>(T valor) implements ResultadoOperacion<T> {}
public record Fallo<T>(String error, Throwable causa) implements ResultadoOperacion<T> {}
public record EnProceso<T>(String operacionId) implements ResultadoOperacion<T> {}

// Pattern matching con switch (Java 21):
String mensaje = switch (resultado) {
    case Exito<String> e -> "Completado: " + e.valor();
    case Fallo<String> f -> "Error: " + f.error();
    case EnProceso<String> p -> "Procesando: " + p.operacionId();
};

50. ¿Cómo optimizarías una query que tarda demasiado en una app Java?

Esta es una pregunta de sistema, no solo de Java. El proceso es:

1. Medir primero: No optimizar a ciegas.

java
// Loguear queries lentas con Hibernate:
// spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=100

2. N+1 Query Problem (el más común):

java
// MAL: Hibernate hace 1 query para usuarios + N queries para pedidos
List<Usuario> usuarios = usuarioRepo.findAll();
usuarios.forEach(u -> u.getPedidos().size()); // cada uno dispara una query

// BIEN: fetch join
@Query("SELECT DISTINCT u FROM Usuario u LEFT JOIN FETCH u.pedidos WHERE u.activo = true")
List<Usuario> findAllConPedidos();

// O con EntityGraph:
@EntityGraph(attributePaths = {"pedidos", "pedidos.lineas"})
List<Usuario> findByActivoTrue();

3. Paginación:

java
// Nunca cargar toda la tabla en memoria
Page<Producto> page = productoRepo.findAll(PageRequest.of(0, 20, Sort.by("nombre")));

4. Proyecciones en lugar de entidades completas:

java
// Interface-based projection: solo los campos necesarios
public interface UsuarioResumen {
    String getNombre();
    String getEmail();
}

List<UsuarioResumen> resumenes = usuarioRepo.findProjectedBy();

Preguntas bonus: Qué preguntar al entrevistador

El cierre de la entrevista importa tanto como las respuestas técnicas. Estas preguntas muestran que pensás en profundidad:

  • ¿Cuáles son los mayores desafíos técnicos del equipo hoy?
  • ¿Cómo manejan la deuda técnica y cuánto tiempo le dedican?
  • ¿Qué stack de monitoreo y observabilidad usan?
  • ¿Cómo es el proceso de code review?
  • ¿Qué versión de Java están usando y tienen planes de actualizar?

Recursos para seguir practicando

  • LeetCode / HackerRank: Para los algoritmos del Bloque 9.
  • Spring Guides (spring.io): Tutoriales oficiales, todos con código ejecutable.
  • Java Memory Model: jcp.org/en/jsr/detail?id=133 — si aplicás a posiciones senior.
  • Effective Java (Joshua Bloch): El libro que todo desarrollador Java debería leer una vez.

Practicar las preguntas en voz alta cambia completamente la preparación. Escribir código en una pizarra (o compartiendo pantalla) es diferente a escribirlo en tu IDE favorito — entrenate para eso.

FAQ

¿Cuál es la diferencia entre ArrayList y LinkedList en Java?+

ArrayList usa un array interno y permite acceso por índice en O(1), pero insertar en el medio cuesta O(n). LinkedList usa nodos enlazados: insertar al inicio o final es O(1), pero acceder por índice cuesta O(n). En la práctica, ArrayList gana en casi todos los casos por mejor localidad de caché. Usá LinkedList solo cuando necesitás insertar y eliminar frecuentemente en los extremos sin acceder por índice.

¿Por qué es importante implementar hashCode() cuando se sobreescribe equals()?+

Porque el contrato de Java dice: si dos objetos son iguales según equals(), deben tener el mismo hashCode(). Las estructuras como HashMap y HashSet usan hashCode() para ubicar el bucket donde buscar el objeto. Si sobreescribís equals() sin sobreescribir hashCode(), podés tener dos objetos 'iguales' con hashCodes distintos — el HashMap no los encontraría aunque estén presentes.

¿Cuál es la diferencia entre Callable y Runnable?+

Runnable.run() no puede retornar un valor ni lanzar checked exceptions. Callable.call() puede retornar un valor de cualquier tipo (usando generics) y puede lanzar checked exceptions. Callable se usa con ExecutorService.submit() y retorna un Future que te permite obtener el resultado o manejar excepciones.

¿Qué es el problema N+1 en JPA/Hibernate y cómo lo resolvés?+

El problema N+1 ocurre cuando cargas N entidades y luego accedés a una relación lazy de cada una, disparando N queries adicionales (1 para la lista + N para las relaciones). Se resuelve con JOIN FETCH en JPQL, con @EntityGraph, o configurando fetch = FetchType.EAGER solo cuando sea apropiado. La solución más limpia es un JOIN FETCH en la query específica que lo necesita.

¿Cuándo usarías volatile en lugar de synchronized?+

Usás volatile cuando solo necesitás garantizar visibilidad entre threads (que las lecturas siempre vean el último valor escrito) y la operación es atómica por sí misma (una asignación simple). Synchronized es necesario cuando la operación es compuesta (read-modify-write como i++) porque volatile no previene condiciones de carrera en operaciones que no son atómicas. Para operaciones aritméticas atómicas, AtomicInteger es la mejor opción.

¿Qué son los Records en Java y cuándo los usás?+

Los Records (Java 16+) son clases de datos inmutables que el compilador genera automáticamente: constructor canónico, getters (sin prefijo 'get'), equals(), hashCode() y toString(). Los usás para DTOs, value objects, resultados de queries, y cualquier clase cuya única responsabilidad sea transportar datos. No son adecuados para entidades con estado mutable o lógica de negocio compleja.

¿Cuál es la diferencia entre Optional.orElse() y Optional.orElseGet()?+

orElse(valor) evalúa el argumento siempre, incluso si el Optional tiene valor. orElseGet(supplier) solo evalúa el Supplier si el Optional está vacío. Si el valor por defecto es costoso de calcular (una query, un objeto nuevo), siempre preferí orElseGet() para evitar trabajo innecesario.

¿Cómo evitás deadlocks en Java?+

La estrategia más efectiva es establecer un orden consistente de adquisición de locks: si todos los threads siempre adquieren los locks en el mismo orden (por ejemplo, siempre A antes que B), no puede haber deadlock circular. Otras estrategias incluyen usar tryLock() con timeout (de ReentrantLock), minimizar el scope de los locks, y preferir estructuras thread-safe del paquete java.util.concurrent que ya resuelven estos problemas internamente.

Related articles

How to Answer Conflict-With-a-Coworker Interview Questions

Learn how to answer conflict-with-a-coworker interview questions with real examples and proven techniques. Stand out in tech and remote job interviews.

How to Answer 'Why Do You Want to Work Here' in Interviews

Discover expert strategies for answering 'why do you want to work here,' tailored for remote tech roles and dollar opportunities. Real, practical interview tips.

Frontend Developer Interview Questions and How to Answer Them (50+)

Complete SEO article covering 54 frontend developer interview questions with detailed answers, real code snippets across HTML, CSS, JavaScript, React, TypeScript, accessibility, security, build tools, and testing.

Full-Stack Developer Interview Questions: How to Answer Like a Pro (45+)

Comprehensive full-stack developer interview guide with 46 numbered questions covering JavaScript/TypeScript, React, CSS, REST APIs, databases, Node.js, system design, security, testing, DevOps, and advanced architecture topics. Each answer includes working code examples and production-level context.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY