Java backend interview prep

Java Interview Questions and Answers 2026

A 30-question Java interview set for backend developers, from language fundamentals and JVM behavior to collections, Stream API and concurrency.

Start Java mock interview Technical interview practice in EnglishA mode where non-native speakers can practice passing technical interviews.

Java and JVM

1What is Java and why is it considered platform-independent?

Java is a statically typed general-purpose language. Source code is compiled into bytecode, and bytecode runs on the JVM. The same .class or .jar can usually run on different operating systems without recompilation if a compatible JVM is available. This is not absolute independence: native libraries, platform-specific paths, system commands and OS behavior can still make an application platform-dependent.

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

2What is the difference between JVM, JRE and JDK?

The JVM loads and executes bytecode and manages memory, threads and garbage collection. The JRE historically meant the JVM plus libraries required to run applications. The JDK includes the runtime and development tools such as javac, java, debugger, javadoc, jar and diagnostic tools. In modern distributions a separate JRE is less common because applications are often shipped with a custom runtime image.

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

3How does the JVM execute Java code and why is JIT needed?

javac compiles source code into bytecode. The JVM loads classes and starts interpreting or executing bytecode instructions. Frequently executed hot paths are detected at runtime and compiled by the JIT compiler into optimized machine code. JIT can inline methods, optimize loops, apply escape analysis and remove some allocations. This is why Java applications can get faster after warm-up, and why microbenchmarks without JMH are often misleading.

4How does ClassLoader work?

A ClassLoader loads class definitions into the JVM. The common loaders are Bootstrap, Platform, Application and custom loaders. A class is identified not only by its fully qualified name but also by the ClassLoader that loaded it, so two classes with the same name from different loaders can be different types. Java usually uses parent delegation: a loader asks its parent first to reduce the risk of replacing platform classes.

Loading
Linking: verification, preparation, resolution
Initialization

5How is JVM memory organized?

Key JVM memory areas include the heap for objects and arrays, Java stacks with call frames for each thread, Metaspace for class metadata, code cache for JIT-compiled code, the PC register and native stacks. Avoid the oversimplification that primitives always live on the stack and objects always live on the heap: a primitive field is stored inside its object, and JIT escape analysis can remove allocations or change object representation.

6How does Garbage Collector work?

The Garbage Collector frees memory occupied by objects that are no longer reachable from GC Roots. Roots include local variables of active methods, static fields, active threads and JNI references. Many collectors rely on the observation that most new objects die young, so the heap is often split into young and old generations. GC does not deterministically close files or connections; external resources should be closed with try-with-resources.

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

OOP and types

7Which four OOP principles does Java support?

Java supports encapsulation, abstraction, inheritance and polymorphism. Encapsulation hides internal state behind a public API. Abstraction focuses on essential behavior. Inheritance creates a new type based on an existing type. Polymorphism lets code work with different implementations through a shared contract. In practice, composition is often preferable to inheritance because it creates lower coupling.

interface PaymentService {
    void pay(BigDecimal amount);
}

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

8What is the difference between method overloading and overriding?

Overloading means several methods have the same name but different parameters; the compiler chooses the method based on static argument types. Overriding means a subclass replaces the implementation of an instance method from a parent class; runtime dispatch chooses the method based on the actual object type. Static methods are hidden, not overridden, and private methods are not overridden because they are not inherited as accessible members.

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"); }
}

9What is the difference between an interface and an abstract class?

An interface defines a contract and can contain abstract methods, default methods, static methods, constants and private helpers. An abstract class can additionally have instance state, constructors, protected fields, shared implementation and abstract or concrete methods. A class can extend only one class but implement multiple interfaces. Use an interface for an independent contract and an abstract class for closely related types with shared implementation and state.

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

10What does the final keyword mean?

A final variable can be assigned only once, but a final reference does not make the referenced object immutable. A final method cannot be overridden. A final class cannot be extended. final communicates intent and limits reference changes or inheritance, but by itself it does not provide a complete immutable model or thread safety.

final int maxAttempts = 3;

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

public final void validate() {}

public final class Token {}

11What are records in Java?

A record is a compact syntax for immutable data carriers. The compiler generates private final fields, constructor, accessors, equals, hashCode and toString. Records are useful for DTOs, value objects and method results. They are not a replacement for every domain entity: validation can be added in a compact constructor, but complex mutable behavior, lazy loading and inheritance-based models usually fit records poorly.

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

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

12How do you create an immutable object in Java?

Make the class final or carefully restrict inheritance, make fields private final, avoid setters and perform validation in the constructor. For mutable inputs, create defensive copies and return copies or unmodifiable views. Deep immutability requires all nested state to be immutable as well. final fields help safe publication, but an object can still be logically mutable if it exposes a mutable collection or mutable nested object.

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;
    }
}

References, strings and comparison

13Does Java pass by value or by reference?

Java always passes arguments by value. For primitives, the value itself is copied. For objects, the value of the reference is copied. A method can mutate the object through the copied reference, but it cannot replace the caller's reference with another object. This distinction explains why changing a field can be visible to the caller, while assigning the parameter to a new object is not.

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

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

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

14What is the difference between == and equals()?

For primitives, == compares values. For object references, == checks whether both references point to the same object. equals() is a method that should compare logical equality if the class implements it correctly. String, Integer, LocalDate and many value classes override equals. For custom domain objects, equals must reflect the intended identity or value semantics.

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

15What is the equals/hashCode contract?

If two objects are equal according to equals, they must return the same hashCode. The reverse is not required: different objects may have the same hashCode. equals should be reflexive, symmetric, transitive, consistent and return false for null. Breaking the contract corrupts behavior in HashMap, HashSet and other hash-based collections: an object may become impossible to find even though it is present.

@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);
}

16Why is String immutable?

String is immutable for safety, predictability and performance. Immutability allows string interning, safe sharing between threads, reliable hashCode caching and secure use in paths, URLs, class names and security-sensitive values. Operations like concat, replace and substring create new strings. Since Java 9, String internally may use byte[] with a compact representation, but the public immutability contract remains the same.

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

17What is the difference between String, StringBuilder and StringBuffer?

String is immutable, so repeated concatenation in loops can create many intermediate objects, although the compiler can optimize simple cases. StringBuilder is mutable and preferred for building strings in a single thread. StringBuffer is similar but synchronized, so it is usually slower and mainly relevant for legacy thread-safe APIs. For formatting, String.format or template-style APIs may be clearer, but they are not always faster.

StringBuilder builder = new StringBuilder();

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

String result = builder.toString();

String message = "Hello, " + name;

Generics and collections

18What are generics and what is type erasure?

Generics let code express type-safe containers and APIs, such as List<String>. At compile time, the compiler checks types and inserts casts where needed. At runtime, most generic type information is erased, so List<String> and List<Integer> share the same runtime class. This is why you cannot directly create new T(), use primitive type parameters, or reliably check instanceof List<String>.

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

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

19What is the Java Collections Framework?

The Collections Framework is a set of interfaces and implementations for storing and processing groups of objects. Core interfaces include List, Set, Queue, Deque and Map. Implementations include ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap and PriorityQueue. Choose a collection by access pattern, ordering requirements, uniqueness, lookup complexity, memory overhead and concurrency needs.

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

20How does HashMap work?

HashMap stores entries in buckets selected by a hash of the key. For lookup, it calculates the hash, finds the bucket and compares keys with equals. Collisions are handled inside the bucket; in modern Java, long chains can be treeified into a balanced tree when conditions are met. HashMap allows one null key and null values, is not thread-safe, and can resize when the load factor threshold is exceeded.

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

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

21What is the difference between HashMap, LinkedHashMap, TreeMap and ConcurrentHashMap?

HashMap provides fast unordered access and is not thread-safe. LinkedHashMap preserves insertion order or access order and is useful for LRU-like caches. TreeMap keeps keys sorted by natural order or Comparator and has O(log n) operations. ConcurrentHashMap is designed for concurrent access with segmented/internal synchronization, does not allow null keys or values, and is preferable to synchronizedMap under load.

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

22What is the difference between ArrayList and LinkedList?

ArrayList is backed by an array. It provides fast indexed access, compact memory layout and efficient iteration, but insertion in the middle requires shifting elements. LinkedList is a doubly linked list. It has cheaper insertion if the node is already known, but indexed access is O(n), memory overhead is higher and cache locality is worse. In many real applications ArrayList is faster even when some insertions are involved.

23What is the difference between Comparable and Comparator?

Comparable defines the natural order inside the class via compareTo. Comparator is a separate object or lambda that defines external ordering. Comparable is useful when a type has one obvious default order. Comparator is better when there are multiple orders, such as by name, date or priority. compare and compareTo must be consistent and preferably compatible with equals when used in sorted sets or maps.

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 and resources

24What is the difference between checked and unchecked exceptions?

Checked exceptions inherit from Exception but not RuntimeException, and the compiler requires callers to handle or declare them. Unchecked exceptions inherit from RuntimeException and are not required in method signatures. Checked exceptions fit recoverable situations at API boundaries. Unchecked exceptions fit programming errors, invalid state and contract violations. In modern service code, teams often avoid excessive checked exceptions because they spread through layers noisily.

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

throw new IllegalArgumentException("Invalid id");

25What is the difference between throw, throws and try-with-resources?

throw immediately throws an exception. throws declares a possible exception in a method signature. try-with-resources automatically closes AutoCloseable resources in reverse order. If both the main block and close throw exceptions, the secondary one is kept as a suppressed exception. This is safer and clearer than manual finally blocks when several resources are involved.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

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

Stream API and functional style

26What is Stream API?

Stream API lets code process sequences declaratively. Intermediate operations such as filter, map and sorted are lazy; terminal operations such as toList, collect, reduce and forEach trigger execution. A stream is not a collection, is usually used only once and works best with pure functions without side effects. parallelStream does not guarantee faster execution and can hurt performance or correctness.

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

27What is Optional and when should you use it?

Optional<T> explicitly represents the absence of a result and works well as a return type. Useful methods include map, flatMap, filter, orElse, orElseGet and orElseThrow. orElse evaluates its argument eagerly, while the supplier in orElseGet runs only when the value is absent. Optional is usually not recommended for entity fields, method parameters or serialized DTOs unless there is a strong reason.

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

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

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

Concurrency

28What do synchronized, volatile and atomic classes guarantee?

synchronized provides mutual exclusion and a happens-before relationship. volatile guarantees visibility of the latest writes and restricts reordering, but it does not make compound operations atomic. Atomic classes provide individual atomic operations such as incrementAndGet. Use locks or synchronized for several related changes, volatile for simple state flags and AtomicInteger or LongAdder for counters under contention.

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

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

29What are race condition, deadlock and thread safety?

A race condition happens when the result depends on unpredictable thread execution order. A deadlock happens when threads wait forever for each other's resources. To achieve thread safety, reduce shared mutable state, keep a consistent lock order, shorten critical sections, avoid calling external code under locks, and use immutable objects, concurrent collections, timeouts or message passing. Thread safety is a property of the whole state access protocol.

30What is the difference between ExecutorService, CompletableFuture and virtual threads?

ExecutorService manages a thread pool and task execution. CompletableFuture helps compose asynchronous operations. Virtual threads are lightweight JVM threads that are convenient for large numbers of blocking I/O tasks. They simplify a thread-per-request model, but they do not speed up CPU-bound work, remove database or external resource limits, or make mutable state thread-safe automatically.

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());
}