Java Preguntas de entrevista
Las entrevistas de Java evalúan tu profundidad en las características principales del lenguaje, diseño orientado a objetos, comprensión de concurrencia y habilidades de resolución de problemas. Se espera que los ingenieros senior no solo conozcan la sintaxis sino también razonen sobre rendimiento, memoria y mejores prácticas. Esta guía cubre las preguntas más comunes y desafiantes de entrevistas de Java.
Lo que cubren las entrevistas de Java
Java Core y POO
Herencia, polimorfismo, encapsulamiento, abstracción, interfaces vs clases abstractas y ciclo de vida del objeto.
Framework de Colecciones
Internals de HashMap, ConcurrentHashMap, ArrayList vs LinkedList, comparator vs comparable, e iteradores fail-fast vs fail-safe.
Concurrencia y Multithreading
Ciclo de vida de hilos, synchronized, volatile, locks (ReentrantLock, ReadWriteLock), executors, fork/join y colecciones concurrentes.
Internals de la JVM y Rendimiento
Carga de clases, modelo de memoria (heap, stack, metaspace), algoritmos de recolección de basura (G1, CMS, ZGC), profiling y tuning.
Ejemplos de preguntas de entrevista sobre Java
- Explica la diferencia entre clase abstracta e interfaz en Java. ¿Cuándo usarías cada una?Lo que cubre una buena respuesta
- Las interfaces solo pueden contener métodos abstractos (hasta Java 8) y métodos default/static; las clases abstractas pueden tener métodos concretos, atributos y constructores.
- Una clase puede implementar múltiples interfaces, pero solo heredar de una clase abstracta (herencia simple).
- Las interfaces definen contratos de comportamiento; las clases abstractas modelan jerarquías con estado compartido.
- Desde Java 8, las interfaces pueden tener métodos privados; las clases abstractas pueden tener métodos protected y final.
- Se usa interfaz para polimorfismo entre tipos no relacionados; clase abstracta para reutilizar código entre clases relacionadas.
Ver respuesta de ejemplo
La principal diferencia radica en que una interfaz solo declara la firma de métodos (públicos abstractos) y desde Java 8 puede incluir métodos default y static con implementación, mientras que una clase abstracta puede contener tanto métodos abstractos como concretos, atributos con cualquier modificador y constructores. Además, una clase puede implementar varias interfaces, pero solo extender una clase abstracta, lo que refleja la limitación de herencia simple en Java. Las interfaces se utilizan para definir un contrato que pueden cumplir clases no relacionadas entre sí, facilitando el polimorfismo y la separación de responsabilidades. Las clases abstractas, en cambio, son ideales cuando se desea compartir código común (atributos, métodos) entre clases que pertenecen a una misma jerarquía. Un error común es creer que los métodos default en interfaces permiten reutilización completa, pero su uso debe limitarse a funcionalidades transversales, no a lógica de negocio central, ya que pueden causar conflictos de herencia múltiple.
- ¿Cómo funciona HashMap internamente? ¿Cómo afecta el redimensionamiento al rendimiento?Lo que cubre una buena respuesta
- HashMap interno: array de buckets (Node<K,V>[]), cada bucket es una lista enlazada o árbol rojo-negro cuando hay muchas colisiones.
- El hash del key reduce a un índice del array usando (n-1) & hash, donde n es la potencia de dos.
- Inicialmente capacidad 16 y factor de carga 0.75; al superar el umbral (capacidad * factor de carga) se redimensiona al doble.
- El redimensionamiento implica crear un nuevo array de mayor tamaño y rehashear todas las entradas, lo que tiene coste O(n) y puede causar pérdida temporal de rendimiento.
- Para mitigar el impacto, se pueden establecer una capacidad inicial adecuada y un factor de carga óptimo según la concurrencia esperada.
Ver respuesta de ejemplo
HashMap almacena pares clave-valor en un array de buckets, donde cada bucket es inicialmente una lista enlazada que se convierte en árbol binario balanceado (rojo-negro) cuando la lista supera el umbral TREEIFY_THRESHOLD (8) y la capacidad total es mayor o igual a MIN_TREEIFY_CAPACITY (64). Para obtener el índice del bucket, se aplica una función hash al key (cálculo de hashCode y XOR shifting) y luego se usa (n-1) & hash, aprovechando que la capacidad es potencia de dos. El factor de carga (0.75 por defecto) determina cuándo redimensionar: cuando el número de entradas supera capacidad * factor de carga, se duplica la capacidad y se reubican todos los elementos (rehash). Este proceso tiene complejidad O(n) y puede provocar pausas en aplicaciones que requieren alta capacidad de respuesta. Además, durante el redimensionamiento, los hilos concurrentes pueden enfrentar problemas de consistencia si no se sincroniza adecuadamente. Por tanto, es importante elegir una capacidad inicial cercana al tamaño esperado y ajustar el factor de carga para aplicaciones críticas de rendimiento.
- Escribe un singleton thread-safe en Java (double-checked locking, método Bill Pugh).Lo que cubre una buena respuesta
- El patrón singleton garantiza una única instancia de una clase en toda la JVM.
- Double-checked locking con volatile reduce la sincronización: primero se verifica la instancia sin bloqueo, luego se sincroniza y se verifica de nuevo antes de crear la instancia.
- El método Bill Pugh (Inner Static Holder Class) aprovecha que las clases internas estáticas se cargan bajo demanda y son thread-safe por el cargador de clases.
- Ambos enfoques son perezosos (lazy initialization) y eficientes en entornos multihilo.
- Double-checked locking requiere volatile para evitar problemas de reordenamiento de instrucciones; el método Bill Pugh es más simple y recomendado.
Ver respuesta de ejemplo
Para implementar un singleton thread-safe, podemos usar double-checked locking con el modificador volatile en la variable estática, que asegura que la inicialización de la instancia sea atómica y visible para todos los hilos. En este patrón, el método getInstance primero verifica si la instancia es null sin sincronizar; si lo es, sincroniza sobre la clase, vuelve a verificar, y solo entonces crea la instancia. Esto evita la sobrecarga de sincronizar en cada llamada. Por otro lado, el método Bill Pugh utiliza una clase interna estática que contiene la instancia; el cargador de clases de Java garantiza que la clase interna no se cargue hasta que se acceda a ella, por lo que la instancia se crea de forma perezosa y thread-safe sin necesidad de bloqueos explícitos. El método Bill Pugh es preferible porque es más conciso y evita posibles errores de reordenamiento incluso sin volatile. A continuación se muestran ambas implementaciones en código Java.
Solución de referenciajava // Double-checked locking con volatile public class SingletonDCL { // volatile garantiza visibilidad y ordenamiento private static volatile SingletonDCL instancia; private SingletonDCL() {} public static SingletonDCL getInstancia() { if (instancia == null) { // Primera verificación sin bloqueo synchronized (SingletonDCL.class) { if (instancia == null) { // Segunda verificación con bloqueo instancia = new SingletonDCL(); } } } return instancia; } } // Método Bill Pugh (Inner Static Holder Class) public class SingletonBillPugh { private SingletonBillPugh() {} // Clase interna estática: se carga solo cuando se referencia private static class SingletonHolder { private static final SingletonBillPugh INSTANCIA = new SingletonBillPugh(); } public static SingletonBillPugh getInstancia() { return SingletonHolder.INSTANCIA; } } // Complejidad temporal: O(1) en ambas implementaciones. // Complejidad espacial: O(1) almacena una única instancia. - ¿Cuál es la diferencia entre synchronized, ReentrantLock y ReadWriteLock? Proporciona un caso de uso para cada uno.Lo que cubre una buena respuesta
- synchronized bloquea intrínsecamente un monitor de objeto; es automático y no se puede interrumpir.
- ReentrantLock ofrece bloqueo explícito con métodos lock(), unlock() y tryLock(), soporta interrupción y fairness.
- ReadWriteLock separa bloqueo de lectura (compartido) y escritura (exclusivo), mejorando concurrencia cuando hay más lecturas que escrituras.
- synchronized es simple pero menos flexible; ReentrantLock permite temporización y modo justo; ReadWriteLock optimiza escenarios de lectura intensiva.
- Casos de uso: synchronized para bloques pequeños; ReentrantLock para control fino (ej. timeout); ReadWriteLock para cachés con pocas escrituras.
Ver respuesta de ejemplo
La palabra clave synchronized proporciona un bloqueo implícito en un objeto o clase, liberándose automáticamente al salir del bloque. Es fácil de usar pero no permite interrumpir un hilo que espera el bloqueo ni establecer un tiempo de espera. ReentrantLock, por su parte, es un bloqueo explícito que ofrece métodos como tryLock(long timeout, TimeUnit unit) para intentar adquirir el bloqueo con tiempo límite, y se puede configurar como justo (fair) para que los hilos obtengan el bloqueo en orden FIFO. ReadWriteLock (implementado por ReentrantReadWriteLock) permite que múltiples hilos lean simultáneamente (bloqueo compartido), pero la escritura es exclusiva; esto es eficiente cuando las operaciones de lectura son mucho más frecuentes que las escrituras. Un caso de uso típico de synchronized es un contador compartido simple; ReentrantLock es ideal para un pool de conexiones donde se necesita timeout en la adquisición; y ReadWriteLock se usa en caches o diccionarios de configuración donde las lecturas superan ampliamente a las escrituras.
- Describe el modelo de memoria de Java. ¿Cómo garantiza volatile la visibilidad?Lo que cubre una buena respuesta
- El modelo de memoria de Java (JMM) define cómo los hilos interactúan a través de la memoria principal: variables compartidas almacenadas en la memoria principal y copias locales en cachés de la CPU.
- Las acciones de los hilos (lectura/escritura) no son instantáneamente visibles para otros hilos debido a la optimización del compilador, reordenamiento y cachés.
- volatile actúa como barrera de memoria: las escrituras en una variable volatile son visibles inmediatamente para otros hilos, y las lecturas ven el valor más reciente.
- volatile no garantiza atomicidad en operaciones compuestas (ej. i++), solo visibilidad y orden.
- La regla happens-before garantiza que una escritura en volatile ocurre antes que cualquier lectura posterior de esa misma variable.
Ver respuesta de ejemplo
El modelo de memoria de Java (JMM) especifica cómo se sincronizan los hilos cuando acceden a variables compartidas. Cada hilo puede tener una copia local de las variables en su caché, y la memoria principal contiene el valor oficial. Las optimizaciones del compilador y la CPU pueden reordenar las instrucciones, lo que provoca que un hilo no vea los cambios realizados por otro hilo. Para solucionarlo, volatile establece una relación happens-before: una escritura en una variable volatile ocurre antes que cualquier lectura de esa misma variable por otro hilo. Esto se implementa mediante barreras de memoria que evitan el reordenamiento y fuerzan la escritura inmediata en memoria principal. Sin embargo, volatile no hace que las operaciones compuestas sean atómicas; por ejemplo, count++ sigue siendo inseguro porque implica leer, incrementar y escribir. Por tanto, volatile es adecuado para flags de estado o variables que solo son escritas por un hilo y leídas por varios.
- Escribe un método para encontrar el primer carácter no repetido en una cadena usando Java Streams o sin ellos.Lo que cubre una buena respuesta
- El primer carácter no repetido es aquel que aparece exactamente una vez en la cadena, considerando el orden de aparición.
- Podemos usar un HashMap para contar frecuencias y luego iterar la cadena para encontrar el primero con cuenta 1.
- Con Java Streams, se puede usar groupingBy con LinkedHashMap para mantener el orden de inserción y luego filtrar.
- La solución sin streams es más eficiente en rendimiento, usando un recorrido simple.
- Complejidad temporal O(n) y espacial O(k) donde k es el número de caracteres distintos.
Ver respuesta de ejemplo
Para encontrar el primer carácter no repetido, una solución eficiente sin streams consiste en realizar dos recorridos: primero construir un mapa de frecuencias (HashMap<Character, Integer>) y luego recorrer la cadena original y devolver el primer carácter cuya frecuencia sea 1. Esto tiene complejidad temporal O(n) y espacial O(n) en el peor caso, aunque normalmente O(k) con k el alfabeto. Con Java Streams, se puede convertir la cadena a un flujo de caracteres, agrupar por carácter usando Collectors.groupingBy con LinkedHashMap para conservar el orden de inserción, filtrar los que tienen frecuencia 1 y obtener el primero. La versión con streams es más declarativa pero puede tener overhead adicional. A continuación se muestran ambas implementaciones.
Solución de referenciajava import java.util.*; import java.util.stream.*; public class PrimerCaracterNoRepetido { // Sin streams public static Character primerNoRepetido(String s) { if (s == null || s.isEmpty()) return null; Map<Character, Integer> frecuencias = new HashMap<>(); // Contar frecuencias for (char c : s.toCharArray()) { frecuencias.put(c, frecuencias.getOrDefault(c, 0) + 1); } // Encontrar primer carácter con frecuencia 1 for (char c : s.toCharArray()) { if (frecuencias.get(c) == 1) { return c; } } return null; // No hay caracteres no repetidos } // Con streams public static Character primerNoRepetidoStream(String s) { if (s == null || s.isEmpty()) return null; return s.chars() .mapToObj(c -> (char) c) .collect(Collectors.groupingBy( c -> c, LinkedHashMap::new, Collectors.counting() )) .entrySet().stream() .filter(entry -> entry.getValue() == 1) .map(Map.Entry::getKey) .findFirst() .orElse(null); } public static void main(String[] args) { String test = "estres"; System.out.println(primerNoRepetido(test)); // 'r' System.out.println(primerNoRepetidoStream(test)); // 'r' } } // Complejidad temporal: O(n) en ambos casos. // Complejidad espacial: O(k) donde k es el número de caracteres distintos. - ¿Cómo funciona la recolección de basura en Java? Compara G1GC y ZGC.Lo que cubre una buena respuesta
- Garbage Collection (GC) en Java es el proceso automático de recuperar memoria de objetos que ya no son alcanzables desde las raíces (GC roots).
- G1GC (Garbage First) es un colector generacional, divide el heap en regiones (1-32 MB), prioriza la recolección de regiones con más basura, y ofrece pausas predecibles.
- ZGC es de baja latencia (pausas < 1 ms) no generacional, utiliza punteros coloreados y barreras de carga para realizar la mayoría del trabajo concurrentemente.
- G1GC es adecuado para aplicaciones con requisitos de pausa moderados (por defecto 200 ms); ZGC es ideal para aplicaciones que necesitan latencias extremadamente bajas y montones grandes (hasta 16 TB).
- ZGC puede consumir más CPU que G1GC debido a las barreras de carga, pero ofrece pausas mucho más cortas.
Ver respuesta de ejemplo
La recolección de basura en Java libera automáticamente la memoria ocupada por objetos que ya no son accesibles desde las raíces de la aplicación (stacks de hilos, variables estáticas, etc.). G1GC es un colector generacional que divide el heap en regiones y recolecta primero las regiones con más objetos muertos, lo que permite controlar el tiempo de pausa mediante la configuración del objetivo de pausa (por defecto 200 ms). Realiza fases de marcado concurrente y limpieza compactante, pero aún puede tener pausas de detención del mundo. ZGC, por otro lado, está diseñado para pausas submilisegundos independientemente del tamaño del heap. Utiliza punteros coloreados (64 bits donde algunos bits indican el estado del objeto) y barreras de carga para manejar la reubicación de objetos concurrentemente, evitando pausas largas. ZGC no es generacional (desde JDK 21 sí es generacional experimentalmente), lo que significa que trata todo el heap por igual. En términos de rendimiento, G1GC tiene menos overhead de CPU, pero ZGC ofrece mejor latencia. La elección depende de los requisitos: si la aplicación es sensible a pausas largas (ej. trading, sistemas en tiempo real), ZGC es mejor; si se prioriza el throughput con pausas moderadas, G1GC es suficiente.
- Implementa una cola bloqueante personalizada usando wait-notify (o condiciones de Lock).Lo que cubre una buena respuesta
- Una cola bloqueante personalizada debe soportar operaciones put (inserta) y take (extrae) que bloquean hasta que haya espacio o elementos disponibles.
- Se puede implementar con wait-notify sobre un objeto monitor o con condiciones de ReentrantLock.
- Usando ReentrantLock y dos Condition (notFull, notEmpty) se tiene más control y flexibilidad.
- La implementación debe asegurar que los métodos sean thread-safe usando sincronización o locks.
- Se puede usar un array circular como estructura de datos interna para eficiencia.
Ver respuesta de ejemplo
Una cola bloqueante personalizada encapsula una estructura de datos (por ejemplo, un array con índices de cabeza y cola) y proporciona métodos put y take que bloquean cuando la cola está llena o vacía respectivamente. La implementación con ReentrantLock y dos Condition (notFull, notEmpty) es más clara y potente que wait-notify simple, porque permite señalar condiciones específicas. Al llamar a put, si la cola está llena, se espera en notFull. Cuando se toma un elemento, se señala notFull para despertar a los hilos que esperan para insertar. Análogamente, take espera en notEmpty cuando la cola está vacía y señala notEmpty después de insertar. Es importante usar el patrón de bucle while para las condiciones de espera, ya que pueden ocurrir despertares espurios. A continuación se muestra una implementación completa con array circular.
Solución de referenciajava import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ColaBloqueante<T> { private final T[] buffer; // Array para almacenar elementos private int cabeza = 0; // Índice del primer elemento private int cola = 0; // Índice donde insertar el próximo elemento private int count = 0; // Número de elementos actuales private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); // Señala cuando hay espacio private final Condition notEmpty = lock.newCondition(); // Señala cuando hay elementos @SuppressWarnings("unchecked") public ColaBloqueante(int capacidad) { if (capacidad <= 0) throw new IllegalArgumentException(); buffer = (T[]) new Object[capacidad]; } // Inserta un elemento, bloqueando si la cola está llena public void put(T elemento) throws InterruptedException { lock.lockInterruptibly(); try { // Mientras la cola esté llena, esperamos while (count == buffer.length) { notFull.await(); } buffer[cola] = elemento; cola = (cola + 1) % buffer.length; count++; // Señalamos que ya no está vacía notEmpty.signal(); } finally { lock.unlock(); } } // Extrae y retorna un elemento, bloqueando si la cola está vacía public T take() throws InterruptedException { lock.lockInterruptibly(); try { // Mientras la cola esté vacía, esperamos while (count == 0) { notEmpty.await(); } T elemento = buffer[cabeza]; cabeza = (cabeza + 1) % buffer.length; count--; // Señalamos que ya no está llena notFull.signal(); return elemento; } finally { lock.unlock(); } } // Métodos opcionales: tamaño, etc. public int size() { lock.lock(); try { return count; } finally { lock.unlock(); } } } // Complejidad temporal: O(1) para put y take. // Complejidad espacial: O(capacidad) para el buffer.
Cómo prepararse
- Practica escribir código limpio y thread-safe en una pizarra o editor en línea.
- Comprende los internals de la JVM y sé capaz de razonar sobre la memoria y el comportamiento del GC.
- Domina el framework de colecciones: conoce los trade-offs de cada implementación.
- Revisa las características de Java 8+: streams, lambdas, optional, CompletableFuture y la nueva API de Fecha/Hora.
- Ten preparada una respuesta profunda para '¿En qué se diferencia un HashMap de un ConcurrentHashMap?' con detalles internos.
Preguntas frecuentes
¿Son importantes las características de Java 8 para las entrevistas?
Sí, la mayoría de las entrevistas asumen conocimiento de lambdas, streams, optionals y CompletableFuture. Se usan ampliamente en el desarrollo moderno de Java.
¿Necesito memorizar las banderas de tuning de la JVM?
Debes estar familiarizado con banderas comunes de GC como -XX:+UseG1GC, -Xms, -Xmx, y saber cómo ajustar tamaños de heap. Entender el razonamiento es más importante que memorizar.
¿Con qué frecuencia se preguntan patrones de diseño en entrevistas de Java?
Comúnmente, especialmente Singleton, Factory, Builder y Observer. Prepárate para implementar un Singleton thread-safe o explicar el patrón Strategy con lambdas de Java 8.
¿Cuál es la diferencia entre iteradores fail-fast y fail-safe?
Los iteradores fail-fast lanzan ConcurrentModificationException si la colección es modificada estructuralmente mientras se itera (por ejemplo, ArrayList). Los iteradores fail-safe operan sobre un clon (por ejemplo, ConcurrentHashMap, CopyOnWriteArrayList).
¿Debo conocer las características de Java 9-17 para las entrevistas?
Sí, espera preguntas sobre módulos (Jigsaw), clases selladas, records, pattern matching para instanceof y bloques de texto. Al menos debes estar al tanto de las características clave.
Practica preguntas sobre Java con retroalimentación instantánea de IA
Sube tu currículum, obtén una entrevista simulada personalizada y ve exactamente qué mejorar — gratis para empezar.