Preguntas de entrevista de Java — 50 con código y respuestas
Si estás preparando una entrevista de Java, esta guía es todo lo que necesitás. Armé 50 preguntas reales — las que efectivamente caen en empresas tech, desde startups hasta grandes corporaciones — con respuestas detalladas y código que podés ejecutar vos mismo. Cubrimos desde fundamentos hasta concurrencia, colecciones, JVM y design patterns.
1. Fundamentos del lenguaje
1. ¿Cuál es la diferencia entre `==` y `.equals()` en Java?
== compara referencias (si dos variables apuntan al mismo objeto en memoria). .equals() compara el contenido lógico del objeto.
String a = new String("hola");
String b = new String("hola");
System.out.println(a == b); // false — objetos distintos en heap
System.out.println(a.equals(b)); // true — mismo contenidoTrampa frecuente: con el String Pool, los literales a veces sí tienen la misma referencia:
String x = "hola";
String y = "hola";
System.out.println(x == y); // true — misma entrada en el poolPor eso siempre usá .equals() para comparar cadenas.
2. ¿Qué es el String Pool?
La JVM mantiene un pool de strings internado en el heap. Cuando escribís un literal "texto", Java busca si ya existe ese valor en el pool; si existe, reutiliza el objeto. Esto ahorra memoria porque los strings son inmutables.
String s1 = "java"; // va al pool
String s2 = "java"; // reutiliza el mismo objeto
String s3 = new String("java"); // fuerza un nuevo objeto fuera del pool
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1 == s3.intern()); // true — intern() lo manda al pool3. ¿Qué significa que Java es "pass-by-value"?
Java siempre pasa argumentos por valor, no por referencia. Para tipos primitivos eso es obvio. Para objetos, el valor que se pasa es la copia de la referencia (el puntero), no el objeto en sí.
void cambiar(int n) { n = 99; } // no afecta el original
void modificar(StringBuilder sb) { sb.append(" mundo"); } // sí afecta el objeto
void reasignar(StringBuilder sb) { sb = new StringBuilder("otro"); } // no afecta la var original
int x = 1;
cambiar(x);
System.out.println(x); // 1 — sin cambio
StringBuilder s = new StringBuilder("hola");
modificar(s);
System.out.println(s); // "hola mundo" — el objeto fue mutado
reasignar(s);
System.out.println(s); // "hola mundo" — la reasignación local no se propaga4. ¿Cuál es la diferencia entre `final`, `finally` y `finalize`?
final: modificador. En variables → constante. En métodos → no se puede sobreescribir. En clases → no se puede heredar.finally: bloque que siempre se ejecuta después de untry/catch, haya o no excepción. Ideal para cerrar recursos.finalize: método deObjectllamado por el GC antes de recolectar el objeto. Deprecado desde Java 9 — usátry-with-resourcesoCleaner.
// finally
try {
int r = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("error");
} finally {
System.out.println("siempre se ejecuta"); // siempre
}
// final en variable
final int MAX = 100;
// MAX = 200; // error de compilación5. ¿Qué es autoboxing y unboxing?
Autoboxing es la conversión automática de un tipo primitivo a su wrapper (int → Integer). Unboxing es la inversa.
List<Integer> lista = new ArrayList<>();
lista.add(5); // autoboxing: int 5 → Integer.valueOf(5)
int n = lista.get(0); // unboxing: Integer → int
// Trampa de rendimiento en loops intensivos:
long sum = 0;
List<Long> nums = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) nums.add((long) i);
for (Long v : nums) sum += v; // unboxing en cada iteración — costoso6. Explicá la diferencia entre `ArrayList` y `LinkedList`
| Operación | ArrayList | LinkedList |
|---|---|---|
| Acceso por índice (get) | O(1) | O(n) |
| Inserción al final | O(1) amortizado | O(1) |
| Inserción al principio/medio | O(n) | O(1) (si tenés el nodo) |
| Memoria | Compacta (array interno) | Mayor (nodos con punteros) |
Usá ArrayList para la mayoría de los casos. LinkedList solo si hacés muchas inserciones/eliminaciones en el medio y raramente accedés por índice.
7. ¿Qué es la diferencia entre `HashMap`, `LinkedHashMap` y `TreeMap`?
HashMap: sin orden garantizado. O(1) para get/put. El más rápido para uso general.LinkedHashMap: mantiene el orden de inserción. Levemente más lento queHashMap.TreeMap: ordena las claves por su orden natural (o unComparator). O(log n) para operaciones. ImplementaNavigableMap.
Map<String, Integer> hash = new HashMap<>(); // sin orden
Map<String, Integer> linked = new LinkedHashMap<>(); // orden inserción
Map<String, Integer> tree = new TreeMap<>(); // orden alfabético
// TreeMap: útil para rangos
TreeMap<Integer, String> rangos = new TreeMap<>();
rangos.put(1, "uno"); rangos.put(3, "tres"); rangos.put(5, "cinco");
System.out.println(rangos.headMap(4)); // {1=uno, 3=tres}8. ¿Qué es la diferencia entre `HashSet` y `TreeSet`?
HashSet usa una tabla de hash → O(1) para add/contains/remove, sin orden. TreeSet usa un árbol rojo-negro → O(log n), mantiene orden natural. Usá HashSet salvo que necesites el orden.
9. ¿Cómo funciona `hashCode()` y `equals()` juntos, y por qué es importante su contrato?
Contrato: si dos objetos son iguales según equals(), deben tener el mismo hashCode(). Si rompés esto, las colecciones basadas en hash (HashMap, HashSet) funcionan mal — podés insertar duplicados o no encontrar objetos.
class Punto {
int x, y;
Punto(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Punto)) return false;
Punto p = (Punto) o;
return x == p.x && y == p.y;
}
@Override public int hashCode() {
return Objects.hash(x, y); // usa los mismos campos que equals
}
}
Set<Punto> set = new HashSet<>();
set.add(new Punto(1, 2));
System.out.println(set.contains(new Punto(1, 2))); // true — funcionaSin sobreescribir hashCode(), set.contains(new Punto(1,2)) devuelve false aunque los valores sean iguales.
10. ¿Qué es `Comparable` vs `Comparator`?
Comparable: la clase implementa su propio orden natural concompareTo(). "Soy comparable."Comparator: clase externa que define cómo comparar dos objetos. "Alguien externo me ordena."
// Comparable — orden natural por nombre
class Empleado implements Comparable<Empleado> {
String nombre; int salario;
Empleado(String n, int s) { nombre = n; salario = s; }
@Override public int compareTo(Empleado otro) {
return nombre.compareTo(otro.nombre);
}
}
// Comparator — orden alternativo por salario
Comparator<Empleado> porSalario = Comparator.comparingInt(e -> e.salario);
List<Empleado> lista = Arrays.asList(
new Empleado("Carla", 80000),
new Empleado("Ana", 95000)
);
Collections.sort(lista); // usa Comparable (por nombre)
lista.sort(porSalario); // usa Comparator (por salario)2. Programación Orientada a Objetos
11. Explicá los 4 pilares de la OOP en Java con ejemplos
1. Encapsulamiento: ocultar el estado interno y exponer solo lo necesario.
class CuentaBancaria {
private double saldo; // privado
public void depositar(double monto) {
if (monto > 0) saldo += monto;
}
public double getSaldo() { return saldo; }
}2. Herencia: reutilizar comportamiento de una clase padre.
class Vehiculo { void arrancar() { System.out.println("arrancando"); } }
class Auto extends Vehiculo {
@Override void arrancar() { System.out.println("auto arrancando"); }
}3. Polimorfismo: una referencia del tipo padre puede apuntar a un hijo.
Vehiculo v = new Auto();
v.arrancar(); // "auto arrancando" — dynamic dispatch4. Abstracción: exponer solo la interfaz relevante, ocultar la implementación.
abstract class Forma { abstract double area(); }
class Circulo extends Forma {
double radio;
Circulo(double r) { radio = r; }
@Override double area() { return Math.PI * radio * radio; }
}12. ¿Cuál es la diferencia entre clase abstracta e interfaz?
| Aspecto | Clase abstracta | Interfaz |
|---|---|---|
| Herencia múltiple | No | Sí (implements múltiples) |
| Estado (campos) | Sí | Solo constantes (static final) |
| Constructores | Sí | No |
| Métodos concretos | Sí | Desde Java 8: default y static |
| Cuándo usar | Compartir código + estado | Definir un contrato |
13. ¿Qué es el principio de sustitución de Liskov (LSP)?
Si S es un subtipo de T, entonces objetos de tipo T pueden reemplazarse por objetos de tipo S sin alterar el comportamiento correcto del programa. En la práctica: una subclase no debería sorprender al código que usa la superclase.
// Violación clásica: Rectángulo y Cuadrado
class Rectangulo {
protected int ancho, alto;
void setAncho(int a) { ancho = a; }
void setAlto(int a) { alto = a; }
int area() { return ancho * alto; }
}
class Cuadrado extends Rectangulo {
@Override void setAncho(int a) { ancho = alto = a; } // viola LSP
@Override void setAlto(int a) { ancho = alto = a; }
}
// Si el código asume que puede setear ancho y alto independientemente,
// Cuadrado lo rompe.La solución: no hacer Cuadrado extends Rectangulo; hacerlos implementar una interfaz Forma por separado.
14. ¿Qué es el patrón Singleton y cómo se implementa de forma thread-safe?
// Double-checked locking — thread-safe y lazy
public class Configuracion {
private static volatile Configuracion instancia;
private Configuracion() {}
public static Configuracion getInstance() {
if (instancia == null) {
synchronized (Configuracion.class) {
if (instancia == null) {
instancia = new Configuracion();
}
}
}
return instancia;
}
}La mejor práctica moderna es usar un enum (garantiza thread-safety y resistencia a serialización):
public enum Config {
INSTANCE;
public void doSomething() { ... }
}15. ¿Qué es el patrón Builder y cuándo lo usás?
Cuando un objeto tiene muchos parámetros opcionales. Evita constructores con 10 argumentos y hace el código legible.
class Pedido {
private final String producto;
private final int cantidad;
private final boolean envioExpress;
private final String cupon;
private Pedido(Builder b) {
producto = b.producto; cantidad = b.cantidad;
envioExpress = b.envioExpress; cupon = b.cupon;
}
static class Builder {
private final String producto;
private int cantidad = 1;
private boolean envioExpress = false;
private String cupon = null;
Builder(String producto) { this.producto = producto; }
Builder cantidad(int c) { cantidad = c; return this; }
Builder envioExpress() { envioExpress = true; return this; }
Builder cupon(String c) { cupon = c; return this; }
Pedido build() { return new Pedido(this); }
}
}
// Uso:
Pedido p = new Pedido.Builder("Laptop")
.cantidad(2)
.envioExpress()
.cupon("LATAM20")
.build();3. Excepciones
16. ¿Cuál es la diferencia entre `checked` y `unchecked` exceptions?
- Checked: heredan de
Exception(no deRuntimeException). El compilador obliga a capturarlas o declararlas en la firma del método (throws). Ej:IOException,SQLException. - Unchecked: heredan de
RuntimeException. El compilador no obliga. Ej:NullPointerException,IllegalArgumentException,ArrayIndexOutOfBoundsException.
Regla práctica: usá checked para condiciones recuperables del negocio; unchecked para errores de programación.
17. ¿Qué es `try-with-resources` y por qué es mejor que `try-finally`?
try-with-resources cierra automáticamente cualquier objeto que implemente AutoCloseable, incluso si hay excepción. Más limpio y menos propenso a errores.
// Antes (try-finally — verbose y con bug potencial)
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("archivo.txt"));
System.out.println(br.readLine());
} finally {
if (br != null) br.close();
}
// Con try-with-resources
try (BufferedReader br = new BufferedReader(new FileReader("archivo.txt"))) {
System.out.println(br.readLine());
} // br.close() se llama automáticamente4. Java 8+ y programación funcional
18. ¿Qué son las expresiones lambda y las functional interfaces?
Una functional interface tiene exactamente un método abstracto. Las lambdas las implementan de forma concisa.
// Functional interface custom
@FunctionalInterface
interface Transformador<T> {
T aplicar(T valor);
}
Transformador<String> mayusculas = s -> s.toUpperCase();
System.out.println(mayusculas.aplicar("hola")); // "HOLA"
// Con las de JDK
Function<Integer, Integer> doble = n -> n * 2;
Predicate<String> noVacio = s -> !s.isEmpty();
Consumer<String> imprimir = System.out::println;
Supplier<List<String>> nuevaLista = ArrayList::new;19. Explicá el Stream API con un ejemplo complejo
List<Empleado> empleados = Arrays.asList(
new Empleado("Ana", "IT", 95000),
new Empleado("Bob", "IT", 75000),
new Empleado("Carla", "HR", 60000),
new Empleado("Diego", "IT", 110000),
new Empleado("Eva", "HR", 70000)
);
// Promedio de salario por departamento, solo mayores a 65000
Map<String, Double> resultado = empleados.stream()
.filter(e -> e.salario > 65000)
.collect(Collectors.groupingBy(
e -> e.departamento,
Collectors.averagingInt(e -> e.salario)
));
// {IT=93333.33, HR=70000.0}20. ¿Qué es `Optional` y cuándo lo usás?
Optional es un contenedor que puede o no tener un valor. Comunica explícitamente que un resultado puede ser nulo, sin lanzar NullPointerException.
Optional<String> nombre = Optional.ofNullable(obtenerNombreDeDB()); // puede ser null
// Mal uso — lo usa como if (!)
if (nombre.isPresent()) {
System.out.println(nombre.get());
}
// Buen uso — fluido
nombre
.map(String::toUpperCase)
.filter(n -> n.length() > 3)
.ifPresent(System.out::println);
String fallback = nombre.orElse("Desconocido");
String calculado = nombre.orElseGet(() -> calcularDefault());Usalo como tipo de retorno de métodos, no como parámetro de método ni en campos de clase.
21. ¿Cuál es la diferencia entre `map()` y `flatMap()` en streams?
map(): transforma cada elemento → devuelveStream.flatMap(): cada elemento se convierte en un stream que se "aplana" → devuelveStream.
List<List<Integer>> listas = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5),
Arrays.asList(6)
);
// map: Stream<List<Integer>>
listas.stream().map(l -> l).forEach(System.out::println); // [1,2,3] [4,5] [6]
// flatMap: Stream<Integer>
listas.stream().flatMap(Collection::stream).forEach(System.out::println); // 1 2 3 4 5 6
// Con Optional
Optional<String> opt = Optional.of("hola");
Optional<Integer> len = opt.flatMap(s -> Optional.of(s.length())); // Optional[4]22. ¿Qué son los method references?
Atajo sintáctico para lambdas que solo invocan un método existente.
// Lambda equivalente vs method reference
list.stream().map(s -> s.toUpperCase()); // lambda
list.stream().map(String::toUpperCase); // method ref a instancia
list.stream().forEach(s -> System.out.println(s));
list.stream().forEach(System.out::println);
// Referencia a constructor
Stream<StringBuilder> sbs = Stream.of("a", "b").map(StringBuilder::new);
// Referencia a método estático
Stream<Integer> nums = Stream.of("-1", "2", "-3").map(Integer::parseInt);23. ¿Qué es la interfaz `Predicate` y cómo se compone?
Predicate<Integer> positivo = n -> n > 0;
Predicate<Integer> par = n -> n % 2 == 0;
Predicate<Integer> positivoPar = positivo.and(par);
Predicate<Integer> positivoOImpar = positivo.or(par.negate());
List<Integer> nums = Arrays.asList(-3, -2, 0, 1, 2, 4, 5);
nums.stream().filter(positivoPar).forEach(System.out::println); // 2, 45. Concurrencia
24. ¿Cuál es la diferencia entre `Thread` y `Runnable`?
// Extendiendo Thread — limita herencia múltiple
class MiThread extends Thread {
@Override public void run() { System.out.println("corriendo"); }
}
new MiThread().start();
// Implementando Runnable — preferido
Runnable tarea = () -> System.out.println("corriendo");
new Thread(tarea).start();
// Mejor aún — usar ExecutorService
ExecutorService exec = Executors.newFixedThreadPool(4);
exec.submit(() -> System.out.println("pool"));
exec.shutdown();25. ¿Qué es `synchronized` y cuáles son sus limitaciones?
synchronized asegura que solo un thread a la vez ejecute un bloque o método sobre el mismo monitor (lock).
class Contador {
private int valor = 0;
public synchronized void incrementar() { valor++; }
public synchronized int getValor() { return valor; }
}Limitaciones: no permite lock con timeout, ni intentar adquirirlo sin bloquear, ni múltiples condiciones. Para eso existe ReentrantLock.
26. Explicá `volatile` en Java
volatile garantiza visibilidad: los cambios de un thread son inmediatamente visibles a otros. No garantiza atomicidad.
class Worker {
private volatile boolean corriendo = true;
public void stop() { corriendo = false; }
public void run() {
while (corriendo) {
// sin volatile, este loop podría no ver el cambio de otro thread
}
}
}Para operaciones compuestas (leer-modificar-escribir) usá AtomicInteger o synchronized.
27. ¿Qué es un `deadlock` y cómo lo prevenís?
Un deadlock ocurre cuando dos o más threads esperan mutuamente que el otro libere un lock.
// Deadlock clásico
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) { // orden invertido — deadlock
synchronized (lockA) { /* ... */ }
}
});Prevención: adquirir siempre los locks en el mismo orden en todos los threads. También podés usar tryLock() con timeout con ReentrantLock.
28. ¿Qué es `ExecutorService` y cuándo lo preferís a `new Thread()`?
ExecutorService es un pool de threads que reutiliza hilos, limita concurrencia y gestiona el ciclo de vida. Mucho más eficiente que crear un Thread por tarea.
ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Future<Integer> futuro = pool.submit(() -> {
Thread.sleep(1000);
return 42;
});
System.out.println(futuro.get()); // bloquea hasta que termina → 42
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);29. ¿Qué son `CompletableFuture` y la programación asíncrona?
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> llamarAPI()) // en otro thread
.thenApply(resultado -> resultado.toUpperCase())
.thenCompose(r -> CompletableFuture.supplyAsync(() -> procesar(r)))
.exceptionally(ex -> "error: " + ex.getMessage());
// Combinar dos futuros
CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> "hola");
CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> " mundo");
CompletableFuture<String> combinado = a.thenCombine(b, (x, y) -> x + y);
System.out.println(combinado.get()); // "hola mundo"30. ¿Qué es `ConcurrentHashMap` y cómo difiere de `HashMap` sincronizado?
ConcurrentHashMap usa segmentación: divide el mapa en segmentos y lockea solo el segmento necesario. Collections.synchronizedMap(map) lockea el mapa entero en cada operación → mucho menos throughput.
ConcurrentHashMap<String, Integer> mapa = new ConcurrentHashMap<>();
mapa.put("a", 1);
mapa.computeIfAbsent("b", k -> calcular(k)); // atómico
mapa.merge("a", 1, Integer::sum); // atómico — incremento seguro6. JVM, memoria y rendimiento
31. ¿Cómo funciona la gestión de memoria en Java (heap, stack, GC)?
- Stack: almacena variables locales y referencias. Uno por thread. LIFO. Se libera automáticamente al salir del método.
- Heap: donde viven los objetos. Compartido por todos los threads. Gestionado por el Garbage Collector.
- Metaspace (Java 8+): reemplaza PermGen. Almacena metadata de clases.
El GC usa generational collection: la mayoría de los objetos mueren jóvenes. Hay un Young Generation (Eden + Survivor) y un Old Generation. G1GC es el default desde Java 9.
32. ¿Qué es el problema de memory leak en Java y cómo lo detectás?
Java no es inmune a memory leaks. Ocurren cuando objetos que ya no se necesitan siguen teniendo referencias (el GC no los puede recolectar).
Causas comunes:
- Static collections que crecen sin límite
- Listeners/observers que no se desregistran
- Cachés sin política de evicción
- ThreadLocal sin llamar
remove()
Herramientas: VisualVM, JProfiler, Eclipse MAT, jmap -histo.
33. ¿Qué hace `System.gc()`?
Le "sugiere" al GC que corra, pero la JVM puede ignorarlo. No es una garantía. En producción, nunca lo llamés manualmente — confiá en el GC.
34. ¿Qué es el JIT compiler?
La JVM interpreta bytecode, pero el Just-In-Time compiler identifica hot paths (código muy ejecutado) y los compila a código nativo para mayor velocidad. Tiered compilation: primero interpreta, luego compila con optimizaciones crecientes (C1 → C2).
7. Generics y tipos
35. Explicá `<? extends T>` vs `<? super T>` (PECS)
PECS: Producer Extends, Consumer Super.
extends T>: podés leer T (el wildcard produce T). No podés escribir (no sabés el tipo exacto).super T>: podés escribir T (el wildcard consume T). Leer devuelveObject.
// Leer de una colección — extends
public double suma(List<? extends Number> lista) {
return lista.stream().mapToDouble(Number::doubleValue).sum();
}
suma(List.of(1, 2, 3)); // List<Integer> — ok
suma(List.of(1.5, 2.5)); // List<Double> — ok
// Escribir en una colección — super
public void agregar(List<? super Integer> lista) {
lista.add(1); lista.add(2);
}
agregar(new ArrayList<Number>()); // ok
agregar(new ArrayList<Object>()); // ok36. ¿Qué es type erasure?
Los generics existen solo en tiempo de compilación. En runtime, List y List son ambos simplemente List. El compilador inserta casts automáticos. Por eso no podés hacer new T[] o instanceof List.
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass()); // true — ambos son ArrayList8. Design Patterns
37. ¿Qué es el patrón Observer?
Permite que múltiples objetos (observers) se suscriban a eventos de un sujeto y sean notificados automáticamente.
interface Observer { void actualizar(String evento); }
class EventBus {
private List<Observer> subs = new ArrayList<>();
void suscribir(Observer o) { subs.add(o); }
void publicar(String evento) { subs.forEach(o -> o.actualizar(evento)); }
}
EventBus bus = new EventBus();
bus.suscribir(e -> System.out.println("Logger: " + e));
bus.suscribir(e -> System.out.println("Mailer: " + e));
bus.publicar("usuario.creado"); // ambos reciben el evento38. ¿Qué es el patrón Strategy?
Permite cambiar el algoritmo de un objeto en runtime encapsulando cada algoritmo en su propia clase.
interface EstrategiaPago { void pagar(double monto); }
class PagoTarjeta implements EstrategiaPago {
public void pagar(double m) { System.out.println("Pagando $" + m + " con tarjeta"); }
}
class PagoTransferencia implements EstrategiaPago {
public void pagar(double m) { System.out.println("Transfiriendo $" + m); }
}
class Checkout {
private EstrategiaPago estrategia;
Checkout(EstrategiaPago e) { estrategia = e; }
void procesar(double monto) { estrategia.pagar(monto); }
}
new Checkout(new PagoTarjeta()).procesar(99.99);
new Checkout(new PagoTransferencia()).procesar(200.0);9. Java moderno (Records, Sealed, Pattern Matching)
39. ¿Qué son los `record` en Java 16+?
Los records son clases inmutables de datos. El compilador genera automáticamente constructor, getters, equals, hashCode y toString.
record Punto(double x, double y) {
// Compact constructor para validación
Punto {
if (x < 0 || y < 0) throw new IllegalArgumentException("coordenadas negativas");
}
double distanciaAlOrigen() {
return Math.sqrt(x * x + y * y);
}
}
Punto p = new Punto(3, 4);
System.out.println(p.x()); // 3.0
System.out.println(p.distanciaAlOrigen()); // 5.0
System.out.println(p); // Punto[x=3.0, y=4.0]40. ¿Qué es pattern matching con `instanceof` (Java 16+)?
Elimina el cast explícito después de un instanceof.
// Antes
Object obj = "hola";
if (obj instanceof String) {
String s = (String) obj; // cast manual
System.out.println(s.length());
}
// Con pattern matching
if (obj instanceof String s) {
System.out.println(s.length()); // s ya es String aquí
}
// Con switch (Java 21+)
String descripcion = switch (obj) {
case Integer i -> "entero: " + i;
case String s -> "cadena de largo " + s.length();
case null -> "nulo";
default -> "otro tipo";
};41. ¿Qué son las `sealed classes`?
Permiten controlar qué clases pueden extender/implementar una jerarquía. Útil para modelar dominios cerrados como ASTs o resultados de operaciones.
sealed interface Resultado<T> permits Exito, Falla {}
record Exito<T>(T valor) implements Resultado<T> {}
record Falla<T>(String error) implements Resultado<T> {}
Resultado<Integer> resultado = new Exito<>(42);
String msg = switch (resultado) {
case Exito<Integer> e -> "ok: " + e.valor();
case Falla<Integer> f -> "error: " + f.error();
};10. Preguntas de sistema y diseño
42. ¿Cómo implementarías una caché simple en Java?
import java.util.LinkedHashMap;
import java.util.Map;
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacidad;
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;
}
}
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("a", "1"); cache.put("b", "2"); cache.put("c", "3");
cache.get("a"); // accede a "a" → se vuelve el más reciente
cache.put("d", "4"); // expulsa "b" (el menos reciente)
System.out.println(cache.containsKey("b")); // false43. ¿Cómo diseñarías un sistema de rate limiting en Java?
class RateLimiter {
private final int maxRequests;
private final long ventanaMs;
private final Map<String, Deque<Long>> historial = new ConcurrentHashMap<>();
RateLimiter(int max, long ventanaMs) {
maxRequests = max; this.ventanaMs = ventanaMs;
}
synchronized boolean permitir(String userId) {
long ahora = System.currentTimeMillis();
historial.computeIfAbsent(userId, k -> new ArrayDeque<>());
Deque<Long> timestamps = historial.get(userId);
// Eliminar timestamps fuera de la ventana
while (!timestamps.isEmpty() && ahora - timestamps.peekFirst() > ventanaMs) {
timestamps.pollFirst();
}
if (timestamps.size() < maxRequests) {
timestamps.addLast(ahora);
return true;
}
return false;
}
}44. ¿Cómo implementás un productor-consumidor con `BlockingQueue`?
BlockingQueue<String> cola = new LinkedBlockingQueue<>(10);
// Productor
Thread productor = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
cola.put("tarea-" + i); // bloquea si la cola está llena
System.out.println("Producido: tarea-" + i);
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
// Consumidor
Thread consumidor = new Thread(() -> {
try {
while (true) {
String tarea = cola.take(); // bloquea si la cola está vacía
System.out.println("Consumido: " + tarea);
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
productor.start(); consumidor.start();45. ¿Qué es la inmutabilidad y por qué importa en código concurrente?
Un objeto inmutable no puede cambiar de estado después de crearse. En concurrencia, es intrínsecamente thread-safe sin necesidad de sincronización.
final class Dinero {
private final double monto;
private final String moneda;
Dinero(double monto, String moneda) {
this.monto = monto; this.moneda = moneda;
}
Dinero sumar(Dinero otro) {
if (!moneda.equals(otro.moneda)) throw new IllegalArgumentException();
return new Dinero(monto + otro.monto, moneda); // nuevo objeto, no muta
}
@Override public String toString() { return monto + " " + moneda; }
}46. ¿Qué es la diferencia entre `Serializable` y `Externalizable`?
Serializable: el mecanismo por default de Java serializa todos los campos no-transient. Simple pero lento y sin control.Externalizable: implementáswriteExternal()yreadExternal()vos mismo. Control total y mejor rendimiento.
47. ¿Cómo funciona la reflexión en Java y cuáles son sus riesgos?
La reflexión permite inspeccionar y modificar el comportamiento de clases, métodos y campos en runtime.
Class<?> clazz = Class.forName("java.util.ArrayList");
Method metodo = clazz.getMethod("size");
Object lista = clazz.getDeclaredConstructor().newInstance();
System.out.println(metodo.invoke(lista)); // 0
// Acceder a campo privado
Field campo = String.class.getDeclaredField("value");
campo.setAccessible(true); // ⚠ bypasea encapsulamientoRiesgos: rompe encapsulamiento, dificulta refactoring, genera overhead de rendimiento, puede fallar con módulos en Java 9+ (module system).
48. ¿Qué es la diferencia entre `String`, `StringBuilder` y `StringBuffer`?
String: inmutable. Cada concatenación crea un nuevo objeto.StringBuilder: mutable, no thread-safe. Usalo para concatenaciones en un solo thread.StringBuffer: mutable, thread-safe (sincronizado). Más lento queStringBuilder. Hoy raramente necesario.
// Ineficiente en loops
String resultado = "";
for (int i = 0; i < 10000; i++) resultado += i; // crea 10000 objetos
// Eficiente
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) sb.append(i);
String resultado2 = sb.toString(); // un solo objeto final49. ¿Qué son los módulos en Java 9+ (JPMS)?
El Java Platform Module System permite declarar explícitamente qué paquetes expone un módulo y de qué módulos depende.
// module-info.java
module com.miapp.servicio {
requires com.miapp.dominio; // dependencia
exports com.miapp.servicio.api; // expone este paquete
opens com.miapp.servicio.internal; // abre para reflexión (Spring, etc.)
}Beneficios: encapsulamiento a nivel de módulo, JVMs más pequeñas con jlink, mejor gestión de classpath.
50. ¿Cómo depurás un problema de alto uso de CPU en producción?
- 1Identificar el proceso Java con alto CPU:
topops aux - 2Obtener el PID y listar threads:
jstack> thread-dump.txt - 3Ver qué threads están consumiendo CPU:
top -H -p(muestra threads) - 4Convertir el TID de decimal a hex:
printf '%x\n' - 5Buscar ese hex en el thread dump para identificar el stack trace problemático
Causas comunes: loops infinitos, GC en modo "stop the world" continuo (heap lleno), deadlock que genera spin-wait, deserialización masiva.
# Script rápido de diagnóstico
jstack $(pgrep -f MiApp) | grep -A 20 "RUNNABLE" | head -60Tips finales antes de la entrevista
Estas 50 preguntas cubren el 90% de lo que cae en entrevistas Java de nivel junior a senior. Pero el conocimiento solo no alcanza: practicá explicarlas en voz alta, como si le estuvieras enseñando a alguien. Los entrevistadores evalúan claridad de comunicación tanto como profundidad técnica.
Temas que complementan esta guía: Spring Boot (IoC, AOP, auto-configuration), Hibernate/JPA (lazy loading, N+1), testing con JUnit 5 y Mockito, y system design orientado a microservicios con Java.