Preparación entrevista Java backend

Preguntas y respuestas de entrevista Java 2026

Una selección de 30 preguntas Java para entrevistas backend: lenguaje, JVM, colecciones, Stream API y concurrencia.

Empezar mock interview Java en español Práctica de entrevista técnica en inglésDiseñado para hablantes no nativos que desean practicar entrevistas técnicas en inglés.

Java y JVM

1¿Qué es Java y por qué se considera independiente de la plataforma?

Java es un lenguaje de propósito general con tipado estático. El código fuente se compila a bytecode, y el bytecode se ejecuta en la JVM. El mismo .class o .jar normalmente puede ejecutarse en distintos sistemas operativos sin recompilar si existe una JVM compatible. Esa independencia no es absoluta: native libraries, rutas específicas de plataforma, comandos del sistema y comportamiento del sistema operativo pueden volver una aplicación dependiente de la plataforma.

Java source -> javac -> bytecode -> JVM -> machine code

2¿En qué se diferencian JVM, JRE y JDK?

La JVM carga y ejecuta bytecode y gestiona memoria, threads y garbage collection. Históricamente, JRE significaba la JVM más las librerías necesarias para ejecutar aplicaciones. El JDK incluye runtime y herramientas de desarrollo como javac, java, debugger, javadoc, jar y diagnostic tools. En distribuciones modernas una JRE separada es menos común porque las aplicaciones suelen enviarse con una runtime image propia.

JDK = development tools + runtime
Runtime = JVM + standard libraries

3¿Cómo ejecuta la JVM el código Java y por qué hace falta JIT?

javac compila el código fuente a bytecode. La JVM carga clases y empieza a interpretar o ejecutar instrucciones bytecode. Las rutas calientes ejecutadas con frecuencia se detectan en runtime y el compilador JIT las compila a machine code optimizado. JIT puede inlinear métodos, optimizar loops, aplicar escape analysis y eliminar algunas allocations. Por eso una aplicación Java puede acelerarse tras el warm-up, y los microbenchmarks sin JMH suelen engañar.

4¿Cómo funciona ClassLoader?

Un ClassLoader carga definiciones de clases en la JVM. Los loaders comunes son Bootstrap, Platform, Application y custom loaders. Una clase se identifica no solo por su nombre fully qualified, sino también por el ClassLoader que la cargó, así que dos clases con el mismo nombre desde loaders distintos pueden ser tipos diferentes. Java normalmente usa parent delegation: un loader pregunta primero a su parent para reducir el riesgo de reemplazar platform classes.

Loading
Linking: verification, preparation, resolution
Initialization

5¿Cómo está organizada la memoria de la JVM?

Las áreas principales incluyen heap para objetos y arrays, Java stacks con frames de llamadas por thread, Metaspace para class metadata, code cache para código compilado por JIT, PC register y native stacks. Evita simplificar a 'primitives siempre en stack y objects siempre en heap': un primitive field se almacena dentro de su objeto, y JIT escape analysis puede eliminar allocations o cambiar la representación del objeto.

6¿Cómo funciona Garbage Collector?

Garbage Collector libera memoria ocupada por objetos que ya no son alcanzables desde GC Roots. Las roots incluyen variables locales de métodos activos, static fields, active threads y JNI references. Muchos collectors se apoyan en la observación de que la mayoría de objetos nuevos vive poco, así que el heap suele dividirse en young y old generations. GC no cierra archivos ni conexiones de forma determinista; los recursos externos deben cerrarse con try-with-resources.

try (var stream = Files.newInputStream(path)) {
    // use stream
}

POO y tipos

7¿Qué cuatro principios de POO soporta Java?

Java soporta encapsulation, abstraction, inheritance y polymorphism. Encapsulation oculta el estado interno detrás de una API pública. Abstraction se centra en el comportamiento esencial. Inheritance crea un nuevo tipo a partir de uno existente. Polymorphism permite trabajar con distintas implementaciones mediante un contrato común. En la práctica, composition suele ser preferible a inheritance porque reduce el acoplamiento.

interface PaymentService {
    void pay(BigDecimal amount);
}

class CardPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // payment implementation
    }
}

8¿En qué se diferencian method overloading y overriding?

Overloading significa que varios métodos tienen el mismo nombre pero parámetros distintos; el compilador elige el método según los tipos estáticos de los argumentos. Overriding significa que una subclass sustituye la implementación de un instance method de la parent class; runtime dispatch elige el método según el tipo real del objeto. Los static methods se ocultan, no se sobrescriben, y los private methods no se sobrescriben porque no se heredan como miembros accesibles.

void send(String message) {}
void send(String message, int priority) {}

class Animal {
    void speak() { System.out.println("Animal"); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Dog"); }
}

9¿En qué se diferencia un interface de una abstract class?

Un interface define un contrato y puede contener abstract methods, default methods, static methods, constants y private helpers. Una abstract class además puede tener estado de instancia, constructores, protected fields, implementación compartida y métodos abstractos o concretos. Una clase solo puede extend una class, pero puede implementar múltiples interfaces. Usa interface para un contrato independiente y abstract class para tipos cercanos con implementación y estado compartidos.

interface Repository<T> {
    Optional<T> findById(long id);
}

10¿Qué significa la palabra clave final?

Una variable final solo puede asignarse una vez, pero una referencia final no vuelve inmutable al objeto referenciado. Un método final no puede sobrescribirse. Una clase final no puede extenderse. final comunica intención y limita cambios de referencia o herencia, pero por sí solo no proporciona un modelo immutable completo ni thread safety.

final int maxAttempts = 3;

final List<String> names = new ArrayList<>();
names.add("Alice"); // allowed

public final void validate() {}

public final class Token {}

11¿Qué es un record en Java y cuándo usarlo?

Un record es una forma compacta de declarar un data carrier. El compilador crea private final fields, accessors, constructor, equals, hashCode y toString. Los records son final y encajan bien con DTOs, commands, events, results de cálculo, keys y values. Un record ofrece shallow immutability: si un componente contiene una mutable list, esa list aún puede cambiar, por lo que puede hacer falta defensive copy.

public record User(long id, String name) {}

public record Order(List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}

12¿Qué es un immutable object y cómo se crea?

Un immutable object no cambia su estado observable después de crearse. Normalmente la class se hace final, los fields private final, no se exponen setters, el estado se inicializa completamente en el constructor y no se filtran mutable internals. Para inputs y outputs mutables se usan defensive copies. Los immutable objects son más fáciles de razonar, más seguros como map keys y más sencillos de compartir entre threads.

public final class User {
    private final String name;
    private final List<String> roles;

    public User(String name, List<String> roles) {
        this.name = name;
        this.roles = List.copyOf(roles);
    }

    public List<String> getRoles() {
        return roles;
    }
}

Referencias, strings y comparación

13¿Java pasa argumentos por valor o por referencia?

Java siempre pasa argumentos por valor. Para primitives se copia el valor. Para objects se copia el valor de la referencia. Un método puede modificar el objeto mediante la referencia copiada, pero no puede reemplazar la variable del caller. Decir que 'los objetos se pasan por referencia' es técnicamente incorrecto: lo que se pasa por valor es la referencia al objeto.

void change(int value) {
    value = 100;
}

void rename(User user) {
    user.setName("Kate");
}

void replace(User user) {
    user = new User("New");
}

14¿En qué se diferencian == y equals()?

Para primitives, == compara valores. Para referencias a objects, == comprueba si ambas referencias apuntan al mismo objeto. equals() por defecto en Object también compara identidad, pero muchas classes lo sobrescriben para content equality. Si una class sobrescribe equals, casi siempre debe sobrescribir hashCode de forma coherente.

int a = 10;
int b = 10;
System.out.println(a == b); // true

String first = new String("java");
String second = new String("java");

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true

15¿Cuál es el contrato de equals() y hashCode()?

equals debe ser reflexive, symmetric, transitive, consistent y devolver false para null. Si a.equals(b) es true, hashCode debe coincidir. Lo contrario no es obligatorio: distintos objetos pueden tener el mismo hash. Los campos que participan en equality/hash no deben cambiar mientras el objeto esté dentro de HashMap o HashSet.

@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (!(object instanceof User other)) return false;
    return id == other.id;
}

@Override
public int hashCode() {
    return Long.hashCode(id);
}

16¿Por qué String es immutable?

Después de crearse, el contenido de String no puede cambiar; los métodos devuelven una nueva string. Immutability hace seguro el String Pool, permite cachear hashCode, usar String como HashMap key, simplifica thread safety y protege valores como URLs, paths, class names y parameters. El contenido se compara con equals, aunque literals puedan apuntar al mismo objeto del pool.

String value = "Java";
value.toUpperCase();

System.out.println(value); // Java

String a = "java";
String b = "java";

System.out.println(a == b); // often true due to interning

17¿En qué se diferencian String, StringBuilder y StringBuffer?

String es immutable, así que las operaciones pueden crear nuevas strings. StringBuilder es mutable y no synchronized; es la opción estándar para construir una string dentro de un solo thread. StringBuffer se parece a StringBuilder pero es synchronized, por lo que normalmente es más lento y se usa poco. La concatenación simple no es un problema por sí misma: compiler/runtime optimizan muchas expresiones.

StringBuilder builder = new StringBuilder();

for (String item : items) {
    builder.append(item).append(',');
}

String result = builder.toString();

String message = "Hello, " + name;

Generics y colecciones

18¿Qué son generics y type erasure?

Generics dan type safety y eliminan casts manuales. Java implementa generics principalmente mediante type erasure: tras compilar, gran parte de la información generic type desaparece. Por eso no se puede hacer new T(), new T[10] ni instanceof List<String>. List<String> no es subtype de List<Object>. Para APIs flexibles se usan wildcards y la regla PECS: Producer Extends, Consumer Super.

List<String> names = new ArrayList<>();
names.add("Alice");

List<? extends Number> producer;
List<? super Integer> consumer;

19¿Cómo está diseñado Java Collections Framework?

Las interfaces principales son List para ordered sequence con duplicates, Set para unique elements, Queue/Deque para queues y Map para key-value pairs. Las implementaciones se eligen según operaciones: ArrayList para indexed access, HashSet para uniqueness, HashMap para key lookup, TreeMap para sorted keys, ArrayDeque para double-ended queue y ConcurrentHashMap para concurrent access.

List<String> list = new ArrayList<>();
Set<Long> ids = new HashSet<>();
Map<Long, User> usersById = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();

20¿Cómo funciona HashMap?

HashMap usa el hash de la clave para elegir un bucket. Luego busca la clave dentro del bucket mediante equals. La complejidad media de get/put es O(1), pero collisions pueden empeorar la situación; implementaciones modernas pueden convertir cadenas largas en tree bajo ciertas condiciones. HashMap permite una null key, no garantiza orden y no es thread-safe.

Map<Long, User> users = new HashMap<>();
users.put(user.id(), user);

User found = users.get(user.id());

21¿En qué se diferencian HashMap, LinkedHashMap, TreeMap y ConcurrentHashMap?

HashMap ofrece lookup rápido sin garantía de orden. LinkedHashMap conserva insertion order o access order y puede servir como base para un LRU cache. TreeMap mantiene keys ordenadas y sus operaciones suelen ser O(log n). ConcurrentHashMap está diseñado para concurrent access. Sus métodos individuales son thread-safe, pero una secuencia de varias llamadas no se vuelve atomic automáticamente.

map.computeIfAbsent(key, ignored -> loadValue());

22¿En qué se diferencian ArrayList y LinkedList?

ArrayList se basa en un dynamic array: indexed access O(1), append amortized O(1), insert middle O(n) y buena cache locality. LinkedList está formada por nodes: indexed access O(n), operaciones en extremos o mediante known iterator O(1), pero cada elemento requiere node y references. Decir que 'LinkedList siempre es más rápida para borrar' es falso, porque a menudo primero hay que encontrar el elemento en O(n). En la mayoría de aplicaciones ArrayList es más rápida, y para queue suele ser mejor ArrayDeque.

23¿Qué son Comparable y Comparator?

Comparable define el natural order dentro de la class. Comparator define un orden externo o alternativo y es útil cuando hay varios criterios de ordenación o no se puede cambiar la class. El resultado de comparison debe ser consistent y transitive. Es recomendable que el ordering sea coherente con equals, especialmente para TreeSet y TreeMap.

class User implements Comparable<User> {
    private int age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(age, other.age);
    }
}

Comparator<User> byName =
    Comparator.comparing(User::getName);

Comparator<User> byAgeThenName =
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName);

Exceptions, Stream API y concurrencia

24¿En qué se diferencian checked exceptions y unchecked exceptions?

Checked exceptions heredan de Exception pero no de RuntimeException; el compiler exige manejarlas o declararlas. Unchecked exceptions heredan de RuntimeException y suelen indicar contract violation, invalid state o programming error. Error describe problemas graves de la JVM o del entorno. Checked exception encaja cuando el caller realmente puede recuperarse; exceptions no deberían ser un mecanismo normal de branching.

void load() throws IOException {
    // ...
}

throw new IllegalArgumentException("Invalid id");

25¿En qué se diferencian throw, throws y try-with-resources?

throw lanza una exception inmediatamente. throws declara una posible exception en la method signature. try-with-resources cierra automáticamente recursos AutoCloseable en reverse order. Si hay exception tanto en el bloque principal como al cerrar, la secundaria se conserva como suppressed exception. Esto es preferible a un finally manual cuando hay varios recursos.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}

26¿Qué es Stream API?

Stream API permite procesar sequences de forma declarativa. Intermediate operations como filter/map/sorted son lazy; terminal operations como toList/collect/reduce/forEach disparan la ejecución. Stream no es una collection, normalmente se usa una sola vez y funciona mejor con pure functions sin side effects. parallelStream no garantiza acelerar y puede empeorar performance o correctness.

List<String> names = users.stream()
    .filter(User::isActive)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .toList();

27¿Qué es Optional y cuándo usarlo?

Optional<T> representa explícitamente la ausencia de resultado y encaja bien como return value. Métodos útiles incluyen map, flatMap, filter, orElse, orElseGet y orElseThrow. orElse evalúa su argumento siempre, mientras que el supplier de orElseGet solo se ejecuta si el value está absent. Optional normalmente no se recomienda sin necesidad en entity fields, method parameters o serialized DTOs.

Optional<User> user = repository.findById(id);

String name = user
    .map(User::getName)
    .orElse("Unknown");

optional.orElse(expensiveOperation());
optional.orElseGet(() -> expensiveOperation());

28¿Qué garantizan synchronized, volatile y atomic classes?

synchronized proporciona mutual exclusion y una relación happens-before. volatile garantiza visibility de las últimas escrituras y restringe reordering, pero no vuelve atomic las operaciones compuestas. Atomic classes ofrecen operaciones atomic individuales como incrementAndGet. Para varios cambios relacionados se usan lock/synchronized, para un simple state flag volatile, y para counters AtomicInteger o LongAdder bajo alta contención.

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();

29¿Qué son race condition, deadlock y thread safety?

Race condition ocurre cuando el resultado depende de un orden impredecible de ejecución de threads. Deadlock ocurre cuando threads esperan indefinidamente recursos de otros. Para thread safety se reduce shared mutable state, se mantiene un lock order consistente, se acortan critical sections, se evita llamar external code bajo lock y se usan immutable objects, concurrent collections, timeouts o message passing. Thread safety es una propiedad de todo el protocolo de acceso al state.

30¿En qué se diferencian ExecutorService, CompletableFuture y virtual threads?

ExecutorService gestiona un thread pool y la ejecución de tasks. CompletableFuture ayuda a componer operaciones asíncronas. Virtual threads son lightweight JVM threads, cómodos para gran cantidad de blocking I/O tasks. Simplifican un modelo thread-per-request, pero no aceleran CPU-bound work, no eliminan límites de base de datos o recursos externos y no vuelven thread-safe el mutable state automáticamente.

try (var executor = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = executor.submit(() -> calculate());
    int result = future.get();
}

CompletableFuture<User> userFuture =
    CompletableFuture.supplyAsync(this::loadUser)
        .thenApply(this::enrichUser)
        .exceptionally(error -> fallbackUser());

try (var executor =
         Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}