Java Senior Developer Interview Questions 2026

Top 60 Questions & Answers — JVM, Concurrency, Java 21, Design Patterns

These are the deep-cut questions that separate senior Java developers from mid-level engineers. Interviewers at senior/lead level don't ask "what is a HashMap" — they probe JVM internals, threading models, GC tuning, Java 21 virtual threads, and architectural decisions. Every answer here is crafted to be interview-ready.

Easy = 3–5 year roles  |  Medium = 5–8 years  |  Hard = Senior / Lead / Architect
JVM Internals & Memory
1
Explain the JVM memory model — heap, stack, metaspace, and code cache.Medium
  • Heap — where all objects live. Divided into Young Generation (Eden + Survivor spaces) and Old Generation. GC manages this area.
  • Stack — per-thread. Stores stack frames (local variables, operand stack, return address). Each method call pushes a frame; each return pops one. Fixed size per thread (-Xss).
  • Metaspace (replaced PermGen in Java 8) — stores class metadata, method bytecode, constant pool. Grows dynamically from native memory. Capped with -XX:MaxMetaspaceSize.
  • Code Cache — compiled native code from the JIT compiler. If it fills up, the JIT stops compiling. Size set with -XX:ReservedCodeCacheSize.
  • Direct / Off-heap memory — used by NIO buffers (ByteBuffer.allocateDirect) and frameworks like Netty. Not GC-managed.
A common senior question: "my app isn't leaking objects but memory keeps growing." Answer: metaspace leak from class reloading, or direct memory from NIO — neither shows in heap dumps.
2
What is the difference between minor GC, major GC, and full GC?Medium
  • Minor GC — collects the Young Generation (Eden + Survivors). Fast (milliseconds). Objects that survive enough minor GCs are promoted to Old Gen. Triggered when Eden fills up.
  • Major GC — collects the Old Generation. Slower because objects are larger and more numerous. Often triggered when Old Gen fills up.
  • Full GC — collects both Young and Old Generation (and metaspace). The most expensive; causes a Stop-The-World (STW) pause. Triggered by concurrent GC failure, explicit System.gc(), or metaspace overflow.

Modern collectors like G1 and ZGC minimize STW pauses by doing most work concurrently. With ZGC on Java 21, pauses are sub-millisecond regardless of heap size.

3
How does the JIT compiler work? What is tiered compilation?Hard

The JIT (Just-In-Time) compiler converts bytecode to native machine code at runtime. It profiles code while running in interpreted mode and compiles hot spots.

Tiered compilation (default since Java 8) has 5 levels:

  • Level 0 — interpreted (no profiling)
  • Level 1 — C1 compiled, no profiling (trivial methods)
  • Level 2 — C1 compiled, limited profiling
  • Level 3 — C1 compiled, full profiling
  • Level 4 — C2 compiled (optimised native code, using profile data)

Methods start at level 0, quickly move to level 3 for profiling, then to level 4 once profiling data is collected. C2 applies aggressive optimisations: inlining, loop unrolling, dead code elimination, escape analysis.

Escape analysis is key: if the JIT proves an object never "escapes" the method, it can allocate it on the stack instead of the heap — zero GC pressure.
4
What is a memory leak in Java? How do you find and fix one?Hard

A Java memory leak is when objects are no longer needed but are still reachable by GC roots — so GC cannot collect them. Common causes:

  • Static collections that grow forever
  • Listeners/callbacks that are registered but never removed
  • Caches without eviction (e.g. HashMap as a cache)
  • Inner class holding reference to outer class longer than expected
  • Thread-local variables not cleaned up after use

How to find:

  1. Monitor heap usage over time — does it grow without stabilising?
  2. Take a heap dump: jmap -dump:live,format=b,file=heap.hprof <pid> or via /actuator/heapdump
  3. Analyse with Eclipse MAT or VisualVM — look for retained heap dominators
  4. Find the GC root chain that keeps objects alive

Fix: Remove the unintended reference — clear the collection, deregister the listener, use WeakHashMap or Caffeine cache with expiry.

5
What is the difference between WeakReference, SoftReference, and PhantomReference?Hard
  • StrongReference — normal reference. Object never collected while reachable.
  • SoftReference — GC collects it only when memory is low. Good for memory-sensitive caches — entries stay in memory when there's space.
  • WeakReference — GC collects it on the next GC cycle regardless of memory. Used in WeakHashMap — entries disappear when the key is no longer strongly reachable. Good for canonical maps (interning).
  • PhantomReference — GC enqueues it after finalisation but before memory is reclaimed. Used to perform cleanup actions as an alternative to finalize(). Queried from a ReferenceQueue.
Java 9+ added Cleaner (built on PhantomReference) as the recommended replacement for finalize().
6
What is the Java Memory Model (JMM) and why does it matter?Hard

The JMM defines how threads interact through memory — what values a thread is guaranteed to see when it reads a variable written by another thread.

Key rules:

  • Visibility — without synchronisation, a thread may see a stale cached copy of a variable. volatile guarantees all threads see the latest write.
  • Reordering — the JVM and CPU may reorder instructions for performance. The JMM defines happens-before relationships that prevent unsafe reordering.
  • Happens-before — if action A happens-before B, A's effects are visible to B. Established by: monitor lock/unlock, volatile write/read, thread start/join, static initialisation.
// Without volatile, thread B may never see flag = true
volatile boolean flag = false;

// Thread A
flag = true;

// Thread B
while (!flag) { } // guaranteed to eventually see true
7
What is class loading and how does the delegation model work?Medium

Class loading is the process of reading a .class file and creating a Class object in the JVM. The parent-delegation model:

  1. When a classloader is asked to load a class, it first delegates to its parent.
  2. If the parent can't find it, only then does the child try to load it.

Classloader hierarchy: Bootstrap (JDK core) → Platform (ext libs) → Application (your classpath) → custom classloaders.

This prevents user code from replacing core JDK classes (e.g. java.lang.String). Application servers use custom classloaders to isolate web apps — each app gets its own classloader instance, so two apps can use different versions of the same library without conflict.

8
What is String interning and how does the String pool work?Medium

The String pool (string intern pool) is a cache of String literals stored in the heap (moved from PermGen to Heap in Java 7). When you write "hello" as a literal, the JVM checks if that value already exists in the pool and reuses it.

String a = "hello";
String b = "hello";
System.out.println(a == b);       // true — same pool object

String c = new String("hello");
System.out.println(a == c);       // false — c is a new heap object
System.out.println(a == c.intern()); // true — intern() returns pool ref

String interning saves memory when many identical strings exist (e.g. database column names, enum-like values). But overusing intern() can fill the pool and cause GC pressure in older JVMs.

9
What is StackOverflowError vs OutOfMemoryError and how do you diagnose each?Medium
  • StackOverflowError — thread's call stack exceeded its size (-Xss, default ~512KB–1MB). Almost always infinite recursion. Read the stack trace — the repeating pattern is the culprit. Fix: add a base case, or convert to iteration.
  • OutOfMemoryError: Java heap space — heap exhausted. Take a heap dump and analyse with MAT. Look for the largest retained heap dominators.
  • OutOfMemoryError: Metaspace — class metadata space exhausted. Usually caused by classloader leaks (frameworks regenerating classes: proxies, bytecode generation). Increase -XX:MaxMetaspaceSize temporarily, then find the leak.
  • OutOfMemoryError: Direct buffer memory — NIO direct memory exhausted. Increase -XX:MaxDirectMemorySize or find the NIO buffer leak.
  • OutOfMemoryError: Unable to create new native thread — OS thread limit hit. Reduce thread count or stack size.
10
How does escape analysis work and what optimisations does it enable?Hard

Escape analysis is a JIT optimisation that determines whether an object's reference can "escape" the method that created it (be stored in a field, returned, or passed to another thread).

If an object does not escape, the JIT can:

  • Stack allocate it — no heap allocation, zero GC pressure, automatic cleanup when the method returns.
  • Scalar replace it — decompose the object's fields into individual stack variables, eliminating the object entirely.
  • Eliminate synchronisation — if a lock object doesn't escape, locking on it is unnecessary and removed.
// The JIT may stack-allocate 'point' here if it doesn't escape
public double distance() {
    Point point = new Point(3, 4);  // may never touch the heap
    return Math.sqrt(point.x * point.x + point.y * point.y);
}
Enable with -XX:+DoEscapeAnalysis (default on). Check with -XX:+PrintEscapeAnalysis.
Concurrency & Threading
11
What is the difference between synchronized, volatile, and ReentrantLock?Medium
  • synchronized — mutual exclusion + visibility. One thread at a time inside the block. Simple but not interruptible, no tryLock, always reentrant.
  • volatile — visibility only, no atomicity. Guarantees reads/writes go straight to main memory, not a CPU cache. Good for flags (running = false) but not for compound operations like count++.
  • ReentrantLock — same mutual exclusion as synchronized but with more control: tryLock(timeout), lockInterruptibly(), fair ordering, multiple condition variables. Use when you need any of these features.
ReentrantLock lock = new ReentrantLock();
try {
    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try { /* critical section */ }
        finally { lock.unlock(); }
    }
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
12
What is a deadlock and how do you prevent it?Medium

A deadlock occurs when two or more threads each hold a lock that the other needs:

// Thread 1: lock A then B
// Thread 2: lock B then A → deadlock

Four conditions must all be true (Coffman conditions): mutual exclusion, hold-and-wait, no preemption, circular wait.

Prevention strategies:

  • Lock ordering — always acquire locks in the same global order. Breaks circular wait.
  • Lock timeout — use tryLock(timeout); release held locks and retry if timeout expires.
  • Lock-free data structures — use java.util.concurrent collections (ConcurrentHashMap, ConcurrentLinkedQueue) which use CAS instead of locks.
  • Avoid nested locks — hold only one lock at a time when possible.

Diagnose: take a thread dump (jstack <pid>). A deadlock shows a cycle in the "waiting on" chain.

13
What is the difference between Callable and Runnable? What does Future give you?Easy
  • Runnablevoid run(). Cannot return a value or throw a checked exception.
  • Callable<V>V call() throws Exception. Returns a value and can throw checked exceptions.
ExecutorService exec = Executors.newFixedThreadPool(4);

Future<Integer> future = exec.submit(() -> {
    Thread.sleep(1000);
    return 42;
});

// Do other work while the task runs...
Integer result = future.get(); // blocks until done, returns 42

Future lets you: check if done (isDone()), wait with timeout (get(timeout, unit)), or cancel (cancel(true)). CompletableFuture extends this with non-blocking callbacks and composable pipelines.

14
Explain CompletableFuture and its non-blocking composition capabilities.Hard
// Chain async tasks without blocking
CompletableFuture
    .supplyAsync(() -> fetchUser(userId))           // async task
    .thenApply(user -> enrichWithProfile(user))      // transform result
    .thenCompose(user -> fetchOrders(user.getId()))  // flat-map async
    .thenCombine(fetchPreferences(userId),           // merge two futures
                 (orders, prefs) -> build(orders, prefs))
    .exceptionally(ex -> Response.error(ex.getMessage())) // error handling
    .thenAccept(response -> sendToClient(response)); // terminal action

Key methods:

  • thenApply — synchronous transform (like Stream.map)
  • thenCompose — flat-map: when the function returns another CompletableFuture
  • thenCombine — combine two independent futures when both complete
  • allOf — wait for all futures to complete
  • anyOf — complete when any future completes

By default runs on ForkJoinPool.commonPool(). Pass a custom Executor as the second arg to any *Async variant for better control.

15
What is the difference between ConcurrentHashMap and HashMap in concurrent use?Medium

HashMap is not thread-safe. Concurrent modification causes data corruption, infinite loops (Java 7), or ConcurrentModificationException.

ConcurrentHashMap (Java 8+) uses segment-level locking — it locks only the bucket being modified, not the entire map. This allows:

  • Concurrent reads with no locking at all
  • Concurrent writes to different buckets simultaneously
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Atomic compute — avoids get + put race condition:
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
// Or: map.merge("key", 1, Integer::sum);
Collections.synchronizedMap() is different — it locks the entire map on every operation, giving lower concurrency than ConcurrentHashMap.
16
What is the ForkJoinPool and how does work stealing work?Hard

ForkJoinPool is a specialised thread pool designed for divide-and-conquer tasks. Each thread has its own deque of tasks. Work stealing: when a thread's deque is empty, it "steals" tasks from the tail of another thread's deque. This keeps all threads busy with minimal contention.

ForkJoinPool pool = new ForkJoinPool(4);
long sum = pool.invoke(new SumTask(array, 0, array.length));

class SumTask extends RecursiveTask<Long> {
    protected Long compute() {
        if (length <= THRESHOLD) return sumSequentially();
        SumTask left = new SumTask(array, start, mid);
        SumTask right = new SumTask(array, mid, end);
        left.fork();               // run left in parallel
        return right.compute() + left.join(); // run right here, wait for left
    }
}

The common ForkJoinPool (ForkJoinPool.commonPool()) backs parallel streams and CompletableFuture async methods. Its size = CPU cores - 1 by default.

17
What is a race condition and how do atomic classes help?Medium

A race condition occurs when a program's result depends on the relative timing of thread execution. Classic example: count++ is three operations (read, increment, write) and is not atomic:

// Thread A reads 5, Thread B reads 5, both write 6 → result is 6 not 7
int count = 0;
count++; // NOT thread-safe

java.util.concurrent.atomic classes use hardware CAS (Compare-And-Swap) for lock-free atomicity:

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // atomic, no lock needed
count.compareAndSet(expected, newValue); // CAS operation
LongAdder adder = new LongAdder(); // better under high contention
adder.increment(); // uses multiple cells to reduce CAS contention

LongAdder outperforms AtomicLong under high write contention by spreading updates across cells and summing on read.

18
What is ThreadLocal and what are its risks?Medium

ThreadLocal gives each thread its own independent copy of a variable — no synchronisation needed because threads never share it.

ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd"));

String formatted = sdf.get().format(new Date()); // thread-safe

Risks:

  • Memory leak in thread pools — threads in a pool are reused, not destroyed. A ThreadLocal set on iteration 1 persists on iteration 2. Always call threadLocal.remove() in a finally block.
  • Hidden state — ThreadLocal creates invisible coupling. Makes code hard to test and reason about.
  • Virtual threads (Java 21) — ThreadLocal on virtual threads can hold many copies (one per vthread). Use ScopedValue instead.
19
What are BlockingQueue implementations and when do you use each?Medium
  • ArrayBlockingQueue — bounded, backed by array. Fair (FIFO) ordering. Good when you want backpressure — producers block when full.
  • LinkedBlockingQueue — optionally bounded, backed by linked nodes. Separate locks for head and tail — higher throughput than ArrayBlockingQueue under contention.
  • SynchronousQueue — no capacity. Each put must be paired with a take. The "hand-off" queue. Used in Executors.newCachedThreadPool().
  • PriorityBlockingQueue — unbounded, orders elements by priority. Good for task scheduling.
  • DelayQueue — elements become available only after a delay. Good for scheduled retries, cache expiry.
20
What is the difference between sleep(), wait(), yield(), and join()?Medium
  • Thread.sleep(ms) — pauses the current thread for at least ms. Does NOT release locks. Throws InterruptedException.
  • Object.wait() — must hold the object's monitor. Releases the lock and waits until notify() / notifyAll() / timeout. Throws InterruptedException.
  • Thread.yield() — hint to the scheduler to give other threads a chance. Rarely useful; scheduler may ignore it.
  • thread.join() — the calling thread waits until the target thread completes. Used to wait for background threads to finish before proceeding.
Always call wait() in a loop: while (!condition) { wait(); } to handle spurious wakeups.
Java 17–21 Modern Features
21
What are Virtual Threads (Java 21) and how do they differ from platform threads?Hard

Platform threads = OS threads. Each one consumes ~1MB stack and is expensive to create. Thread pools are needed to control cost.

Virtual threads (Project Loom, GA in Java 21) are JVM-managed, lightweight threads. They are multiplexed onto a small pool of OS carrier threads. When a virtual thread blocks on I/O, the carrier thread is freed to run other virtual threads.

// Create millions of virtual threads cheaply
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 1_000_000).forEach(i ->
        exec.submit(() -> handleRequest(i)));
}

Key differences:

  • Virtual threads are cheap to create — no need for thread pools
  • Blocking I/O on a virtual thread does not block the OS thread
  • Same Thread API — no new programming model
  • ThreadLocal works but is costly at scale — prefer ScopedValue
  • CPU-bound work gains nothing — virtual threads don't add parallelism
22
What are Records in Java and when should you use them?Easy

Records (Java 16) are immutable data carriers. They auto-generate constructor, getters, equals(), hashCode(), and toString():

// Instead of 40 lines of boilerplate:
record Point(double x, double y) {}

record UserDTO(Long id, String name, String email) {
    // Compact constructor for validation:
    UserDTO {
        Objects.requireNonNull(name, "name required");
        name = name.trim();
    }
}

Point p = new Point(3.0, 4.0);
System.out.println(p.x()); // getter
System.out.println(p);     // Point[x=3.0, y=4.0]

Use records for: DTOs, value objects, database query results, API response payloads. Don't use for entities that need mutable state.

23
What are Sealed Classes and how do they enable exhaustive pattern matching?Medium

Sealed classes (Java 17) restrict which classes can extend or implement them:

sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}

// Pattern matching switch — compiler knows all cases are covered:
double area = switch (shape) {
    case Circle c    -> Math.PI * c.radius() * c.radius();
    case Rectangle r -> r.w() * r.h();
    case Triangle t  -> 0.5 * t.base() * t.height();
    // No default needed — compiler knows it's exhaustive
};

This replaces the visitor pattern and instanceof chains. The compiler enforces that all permitted subtypes are handled — you can't forget a case at compile time.

24
What is Pattern Matching for instanceof (Java 16)?Easy
// Before Java 16:
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Java 16+:
if (obj instanceof String s) {
    System.out.println(s.length()); // s is already String, no cast
}

// With guards (Java 21 switch):
switch (obj) {
    case Integer i when i > 0 -> System.out.println("Positive: " + i);
    case Integer i             -> System.out.println("Non-positive: " + i);
    case String s              -> System.out.println("String: " + s);
    default                    -> System.out.println("Other");
}
25
What are Text Blocks and when should you use them?Easy
// Before (Java 14-):
String json = "{\n" +
    "  \"name\": \"Alice\",\n" +
    "  \"email\": \"alice@example.com\"\n" +
    "}";

// Text blocks (Java 15+):
String json = """
        {
          "name": "Alice",
          "email": "alice@example.com"
        }
        """;

String sql = """
        SELECT u.id, u.name, o.total
        FROM users u
        JOIN orders o ON u.id = o.user_id
        WHERE u.active = true
        """;

The indentation is automatically stripped based on the closing """ position. Use for SQL, JSON, HTML, XML embedded in Java code.

26
What is SequencedCollection in Java 21?Medium

Java 21 added three new interfaces in the collections hierarchy to unify first/last element access which previously required different APIs per collection type:

  • SequencedCollection<E> — adds getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast(), reversed()
  • SequencedSet<E> — extends SequencedCollection, no duplicates
  • SequencedMap<K,V> — adds firstEntry(), lastEntry(), sequencedKeySet(), reversed()
List<String> list = new ArrayList<>(List.of("a","b","c"));
list.getFirst(); // "a" — no more get(0)
list.getLast();  // "c" — no more get(list.size()-1)
list.reversed(); // reversed view
27
What are Structured Concurrency and ScopedValues in Java 21?Hard

Structured Concurrency treats concurrent tasks as a unit: if a parent task spawns child tasks, the parent doesn't complete until all children complete or the first fails:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User> user   = scope.fork(() -> fetchUser(id));
    Future<List<Order>> orders = scope.fork(() -> fetchOrders(id));
    scope.join().throwIfFailed(); // both must succeed
    return new Response(user.resultNow(), orders.resultNow());
}

ScopedValue is an immutable, inheritable thread-local replacement — values flow into child tasks automatically without copy overhead:

static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

ScopedValue.where(CURRENT_USER, user).run(() -> handleRequest());
28
What key changes came in Java 17 regarding security and encapsulation?Medium
  • Strong encapsulation of JDK internals — reflective access to sun.* and com.sun.* packages is denied by default. Libraries that relied on internal APIs must be updated.
  • Removal of RMI Activation, Applet API, SecurityManager — legacy APIs marked for removal in 17, removed in subsequent versions.
  • Sealed classes GA — restricted class hierarchies.
  • Pattern matching for instanceof GA.
  • Context-Specific Deserialization Filters — helps protect against deserialization vulnerabilities (JEP 415).

Java 17 is also the LTS release that most production teams migrated to from Java 11. Java 21 is the next LTS.

Collections & Generics
29
How does HashMap work internally in Java 8+?Medium

HashMap uses an array of buckets. On put(key, value):

  1. key.hashCode() is computed and spread with a secondary hash.
  2. Index = hash & (capacity - 1).
  3. If the bucket is empty, a new node is placed there.
  4. If there's a collision, nodes form a linked list in the bucket.
  5. Java 8 improvement: when a bucket's linked list exceeds 8 entries and capacity ≥ 64, the list is converted to a red-black tree, giving O(log n) lookup instead of O(n).

When the load factor (default 0.75) is exceeded, the map doubles capacity and rehashes all entries. This is expensive — pre-size maps when the final size is known: new HashMap<>(expectedSize / 0.75 + 1).

30
What is the difference between ArrayList, LinkedList, and ArrayDeque?Easy
  • ArrayList — dynamic array. O(1) random access by index. O(n) insert/delete in the middle (shifts elements). Best for: read-heavy, random access.
  • LinkedList — doubly-linked list. O(1) insert/delete at head/tail. O(n) index access (must traverse). Also implements Deque. In practice, cache locality is poor — ArrayList often beats LinkedList even for insertions due to CPU cache effects.
  • ArrayDeque — resizable circular array. O(1) amortised add/remove at both ends. No capacity waste, faster than LinkedList for queue/stack use. Preferred over Stack and LinkedList as a Deque.

Default choice: ArrayList for List, ArrayDeque for stack/queue.

31
What is type erasure in generics and what are its implications?Hard

Generic type parameters are erased at compile time — List<String> and List<Integer> are the same List at runtime. The compiler inserts casts and checks but the JVM sees only raw types.

Implications:

  • Cannot use instanceof with generic types: x instanceof List<String> is a compile error.
  • Cannot create generic arrays: new T[10] is illegal.
  • Cannot get the type parameter at runtime: List<String>.class doesn't exist, only List.class.
  • Heap pollution can occur when unchecked casts bypass the type system.

Workaround: pass Class<T> as a constructor argument (type token), or use Jackson's TypeReference<List<User>>(){} which captures the type at construction via reflection on the anonymous subclass.

32
What is the difference between ? extends T and ? super T?Hard

The PECS principle: Producer Extends, Consumer Super.

  • List<? extends Animal> — the list produces Animals (you can read from it as Animal). But you cannot add to it (compiler doesn't know the exact subtype).
  • List<? super Cat> — the list consumes Cats (you can add Cat or subtypes). But you can only read from it as Object.
// Copy from source to dest:
public <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) dest.add(item);
}

// Collections.sort signature:
public static <T> void sort(List<T> list,
    Comparator<? super T> c)
33
When would you use TreeMap vs LinkedHashMap vs HashMap?Medium
  • HashMap — O(1) average get/put, no ordering. Default choice for key-value lookup.
  • LinkedHashMap — maintains insertion order (or access order if configured). O(1) operations. Use for: LRU cache (access-order mode + override removeEldestEntry), ordered JSON output.
  • TreeMap — keeps keys sorted (natural order or custom Comparator). O(log n) operations. Use for: range queries (subMap, headMap, tailMap), sorted iteration, finding floor/ceiling keys.
TreeMap<Integer, String> map = new TreeMap<>();
map.put(3,"c"); map.put(1,"a"); map.put(2,"b");
map.floorKey(2);    // 2
map.subMap(1, 3);   // {1=a, 2=b}
34
What are the new Collection factory methods and their characteristics?Easy
List<String> list = List.of("a","b","c");        // Java 9+
Set<String> set   = Set.of("x","y");
Map<String,Integer> map = Map.of("a",1,"b",2);
Map<String,Integer> map2 = Map.ofEntries(        // for larger maps
    Map.entry("a",1), Map.entry("b",2));

// Java 10+:
List<String> copy = List.copyOf(existingList);   // immutable copy

Key characteristics of List.of / Set.of / Map.of:

  • Immutable — any structural modification throws UnsupportedOperationException
  • No null elements allowed — throws NullPointerException
  • Set/Map ordering is not guaranteed (random between JVM runs)
  • Compact memory layout — more efficient than ArrayList for small fixed collections
Design Patterns
35
Implement a thread-safe Singleton in Java.Medium

The best approach — enum singleton (guaranteed by JVM, handles serialisation and reflection attacks):

public enum DatabaseConnection {
    INSTANCE;
    public void query(String sql) { ... }
}

Alternative — initialization-on-demand holder (lazy, thread-safe, no synchronisation overhead):

public class Config {
    private Config() {}
    private static class Holder {
        static final Config INSTANCE = new Config();
    }
    public static Config getInstance() { return Holder.INSTANCE; }
}

The class is only loaded when first accessed. The JVM class loading guarantee ensures thread safety without synchronized.

36
What is the Builder pattern and why is it preferred over telescoping constructors?Easy
public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final int timeoutMs;

    private HttpRequest(Builder b) {
        this.url = b.url;
        this.method = b.method;
        this.headers = Collections.unmodifiableMap(b.headers);
        this.timeoutMs = b.timeoutMs;
    }

    public static class Builder {
        private final String url;            // required
        private String method = "GET";       // optional with default
        private Map<String, String> headers = new HashMap<>();
        private int timeoutMs = 5000;

        public Builder(String url) { this.url = url; }
        public Builder method(String m) { method = m; return this; }
        public Builder header(String k, String v) { headers.put(k,v); return this; }
        public Builder timeout(int ms) { timeoutMs = ms; return this; }
        public HttpRequest build() { return new HttpRequest(this); }
    }
}

// Usage:
HttpRequest req = new HttpRequest.Builder("https://api.example.com")
    .method("POST").header("Auth","Bearer xyz").timeout(3000).build();
37
What is the Strategy pattern? Show a Java example.Medium

Strategy defines a family of interchangeable algorithms behind a common interface. The client chooses the algorithm at runtime.

@FunctionalInterface
interface SortStrategy {
    void sort(int[] arr);
}

class Sorter {
    private final SortStrategy strategy;
    public Sorter(SortStrategy strategy) { this.strategy = strategy; }
    public void sort(int[] arr) { strategy.sort(arr); }
}

// Usage with lambdas (lambdas ARE strategy pattern):
Sorter s = new Sorter(Arrays::sort);
s.sort(new int[]{3,1,2});

// Or swap strategy at runtime:
Sorter s2 = new Sorter(arr -> bubbleSort(arr));

Java 8+ functional interfaces make Strategy pattern trivial — most cases where you'd write a Strategy interface are now just a Function<T,R>, Comparator, or custom @FunctionalInterface.

38
What is the Observer pattern and how does Java support it?Medium

Observer defines a one-to-many dependency: when a subject changes state, all registered observers are notified automatically.

// Modern approach with generics:
interface EventListener<T> { void onEvent(T event); }

class EventBus<T> {
    private final List<EventListener<T>> listeners = new CopyOnWriteArrayList<>();
    public void subscribe(EventListener<T> l) { listeners.add(l); }
    public void publish(T event) { listeners.forEach(l -> l.onEvent(event)); }
}

EventBus<OrderEvent> bus = new EventBus<>();
bus.subscribe(event -> notificationService.send(event));
bus.subscribe(event -> auditLog.record(event));
bus.publish(new OrderEvent(orderId, "PLACED"));

In Spring, ApplicationEventPublisher + @EventListener is the built-in observer implementation. Spring Cloud Stream and Kafka provide the same pattern across service boundaries.

39
What is the Decorator pattern? How does Java I/O use it?Medium

Decorator wraps an object to add behaviour without changing its interface or creating subclasses.

Java I/O is the classic example — each class wraps another:

// Layer by layer:
InputStream raw        = new FileInputStream("data.gz");
InputStream buffered   = new BufferedInputStream(raw);    // add buffering
InputStream compressed = new GZIPInputStream(buffered);   // add decompression
Reader reader          = new InputStreamReader(compressed, StandardCharsets.UTF_8);
BufferedReader br      = new BufferedReader(reader);       // add line reading

String line = br.readLine();

Each layer adds a single responsibility. You compose them as needed without a combinatorial explosion of subclasses. This is also the pattern behind Spring's HttpServletRequestWrapper and most filter chains.

40
What is the Proxy pattern and how does Spring use it?Hard

Proxy provides a surrogate for another object, controlling access to it. Spring uses JDK dynamic proxies and CGLIB proxies to implement AOP:

// JDK Dynamic Proxy (interface-based):
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    (proxyObj, method, args) -> {
        System.out.println("Before: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After: " + method.getName());
        return result;
    });

Spring uses this pattern for @Transactional, @Cacheable, @Async, @Secured — the actual bean you inject is a proxy that intercepts method calls and adds the cross-cutting behaviour. This is why self-invocation (this.method()) bypasses these annotations — the call skips the proxy.

41
What is the Command pattern and where is it used in Java/Spring?Medium

Command encapsulates a request as an object, allowing parameterisation, queuing, logging, and undoable operations.

@FunctionalInterface
interface Command { void execute(); }

class CommandQueue {
    private final Queue<Command> queue = new LinkedList<>();
    public void enqueue(Command cmd) { queue.offer(cmd); }
    public void run() { while (!queue.isEmpty()) queue.poll().execute(); }
}

// Usage:
queue.enqueue(() -> emailService.send(email));
queue.enqueue(() -> auditLog.record(action));
queue.run();

Used in: Spring Batch (Step, Tasklet), Undo/Redo systems, Task schedulers, CQRS command buses, Spring Integration MessageHandler.

42
What are the SOLID principles and a Java violation example for each?Medium
  • S — Single Responsibility: a class should have one reason to change. Violation: UserService that handles auth, sends emails, AND generates reports.
  • O — Open/Closed: open for extension, closed for modification. Violation: adding a new payment type by adding an if/else to PaymentProcessor instead of a new implementation.
  • L — Liskov Substitution: subtypes must be usable in place of their base type. Violation: Square extends Rectangle where setting width also sets height — breaks width/height independence.
  • I — Interface Segregation: no client should be forced to depend on methods it doesn't use. Violation: one fat Animal interface with fly(), swim(), run() — Dog is forced to implement fly().
  • D — Dependency Inversion: depend on abstractions, not concretions. Violation: OrderService instantiating new MySQLOrderRepository() instead of injecting an OrderRepository interface.
Performance & GC Tuning
43
Compare G1GC, ZGC, and Shenandoah. When do you choose each?Hard
  • G1GC (default Java 9+) — divides heap into equal regions, prioritises regions with most garbage ("Garbage First"). Sub-second pauses on heaps up to ~32GB. Good default for most applications.
  • ZGC (Java 15+ production) — concurrent, sub-millisecond pauses regardless of heap size (up to 16TB). Use for: latency-sensitive apps, large heaps, where any GC pause is unacceptable (trading systems, ad bidding).
  • Shenandoah — similar to ZGC but from Red Hat. Also sub-millisecond, concurrent. Available in OpenJDK. Good alternative when already using Red Hat JDK.
  • Serial/Parallel GC — old collectors. Serial for single-core/embedded. Parallel for batch/throughput where pauses don't matter.

Rule: G1 for most production workloads. ZGC when latency SLAs are strict (<1ms GC pause required).

44
How do you profile a Java application in production?Hard

Tools:

  • async-profiler — low-overhead, sampling CPU profiler. Uses perf_events / AsyncGetCallTrace to avoid safepoint bias. Can profile both Java and native frames.
  • JDK Flight Recorder (JFR) — built-in, very low overhead (<1%). Records GC, thread activity, I/O, JIT, allocations. Enabled at startup: -XX:+FlightRecorder. Analyse with JDK Mission Control.
  • jstack — thread dump. Good for deadlock/livelock diagnosis.
  • jmap — heap histogram and heap dump.
  • Arthas (Alibaba) — attach to running JVM, trace methods, watch arguments/return values, decompile classes without restart.
# async-profiler 30s CPU profile to flamegraph HTML
./profiler.sh -d 30 -f flamegraph.html <pid>

# JFR: record 60s, download and open in Mission Control
jcmd <pid> JFR.start duration=60s filename=recording.jfr
45
What is false sharing in multi-core systems and how do you avoid it?Hard

CPU cache lines are typically 64 bytes. When two threads write to different variables that happen to be on the same cache line, each write invalidates the other thread's cache — causing unnecessary cache misses (false sharing).

// Bad: both counters may share a cache line
class Counters {
    long counterA;
    long counterB;
}

// Fix: pad to separate cache lines
@jdk.internal.vm.annotation.Contended  // Java 8+ — adds padding automatically
class Counter {
    long value;
}

// Or manual padding:
class Counter {
    long p1,p2,p3,p4,p5,p6,p7; // 56 bytes padding
    long value;                  // on its own 64-byte cache line
    long q1,q2,q3,q4,q5,q6,q7;
}

Relevant in high-throughput ring buffers and concurrent counters. LMAX Disruptor's Sequence class uses this technique to achieve ~100M ops/sec.

46
What is object pooling and when does it help in Java?Medium

Object pooling reuses expensive-to-create objects rather than creating and discarding them. Examples where it helps:

  • Database connections — HikariCP pool: opening a TCP connection is ~50ms; reusing takes microseconds.
  • Thread pools — thread creation costs ~1ms; reusing an existing thread is instant.
  • Netty ByteBuffer pool — direct memory allocation is expensive; Netty pools buffers with PooledByteBufAllocator.

When not to pool normal objects: modern GC is very fast at allocating and collecting short-lived objects (Eden space is just a pointer bump). Pooling small plain objects often adds synchronisation overhead that outweighs the allocation savings. Measure first.

// Apache Commons Pool2 for custom pools:
GenericObjectPool<ExpensiveConnection> pool =
    new GenericObjectPool<>(factory, config);
ExpensiveConnection conn = pool.borrowObject();
try { conn.doWork(); } finally { pool.returnObject(conn); }
47
What tools and techniques do you use to diagnose high CPU usage in Java?Hard
  1. Find the hot thread: top -H -p <pid> (Linux) — shows per-thread CPU. Note the thread ID in hex.
  2. Thread dump: jstack <pid> — find the thread matching the hex ID (nid=0x...).
  3. Analyse the stack trace — what is it doing? Infinite loop? Regex backtracking? Sorting? Hashing with poor distribution?
  4. Flame graph — async-profiler CPU mode, 30s sample. The widest frames are the hottest code paths.

Common causes of unexpectedly high CPU:

  • Regex catastrophic backtracking (exponential worst case)
  • Excessive GC (check jstat -gcutil <pid> 1000 — if GC time > 5%, GC is a problem)
  • Tight spin loops in lock-free code
  • JSON serialisation of very large objects
48
What is safepoint bias in profilers and how does async-profiler avoid it?Hard

Traditional profilers (VisualVM CPU profiler) sample thread state only at JVM safepoints — points in code where it is safe to pause all threads (method returns, loop back-edges). CPU-intensive code that never reaches a safepoint is invisible.

This causes safepoint bias: you see methods that happen to safepoint, not the methods burning CPU.

async-profiler uses POSIX signals (SIGPROF) and the internal AsyncGetCallTrace API to interrupt threads at any arbitrary point — no safepoint required. This gives a true picture of where CPU time is spent, including tight loops and native code.

JFR (JDK Flight Recorder) also avoids safepoint bias for its profiling events from Java 16+ improvements.
49
How do parallel streams work and when should you avoid them?Medium

Parallel streams use the ForkJoinPool's common pool to split the stream's source, process sub-tasks in parallel, and combine results. They help when:

  • The data source is large (100k+ elements)
  • The per-element operation is CPU-bound and expensive
  • Elements are processed independently (no shared mutable state)

Avoid parallel streams when:

  • Elements are few — thread overhead exceeds the benefit
  • The operation is I/O-bound — you block threads in the common pool, hurting other parallel streams and CompletableFuture tasks
  • The source is poorly splittable (LinkedList splits slowly)
  • Order matters and you can't afford the reordering overhead
// Measure before using:
long result = LongStream.rangeClosed(1, 10_000_000)
    .parallel()
    .filter(n -> isPrime(n))  // CPU-bound: good candidate
    .count();
50
What is JMH and how do you write a proper microbenchmark?Hard

JMH (Java Microbenchmark Harness) is the standard tool for measuring Java performance. It handles JVM warm-up, JIT compilation, dead code elimination, and measurement noise.

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringBenchmark {

    private String value = "hello world";

    @Benchmark
    public boolean contains() {
        return value.contains("world"); // result returned → not dead-code eliminated
    }

    @Benchmark
    public int indexOf() {
        return value.indexOf("world");
    }
}

// Run: java -jar benchmarks.jar -wi 5 -i 10 -f 2

Key rules for valid benchmarks: always return or consume results (Blackhole.consume()), use @State to keep shared data, run warmup iterations, use multiple forks. Anything less gives numbers that the JIT can make meaningless.

Advanced Language & Architecture
51
What is the difference between checked and unchecked exceptions? Should you use checked exceptions?Medium
  • Checked exceptions (extend Exception) — the compiler forces callers to handle or declare them. E.g. IOException, SQLException.
  • Unchecked exceptions (extend RuntimeException) — no compiler enforcement. E.g. NullPointerException, IllegalArgumentException.

Modern Java opinion: prefer unchecked exceptions for most application code. Checked exceptions were intended for recoverable conditions but in practice:

  • They pollute lambdas (functional interfaces don't allow checked exceptions)
  • Callers often just catch and rethrow or swallow them
  • Most major frameworks (Spring, Hibernate) wrap checked exceptions in runtime exceptions

Use checked exceptions when: the caller can realistically recover (e.g. file not found → ask user for another path). Use unchecked when: it's a programming error or unrecoverable condition.

52
What is reflection and what are the performance and security implications?Medium

Reflection lets you inspect and manipulate classes, fields, and methods at runtime without compile-time knowledge:

Class<?> cls = Class.forName("com.example.UserService");
Method method = cls.getDeclaredMethod("findById", Long.class);
method.setAccessible(true); // bypass private
Object result = method.invoke(serviceInstance, 42L);

Performance: reflective invocation is 2–10x slower than direct call due to security checks, argument boxing, and no JIT inlining. In hot paths, use MethodHandle (Java 7+) or invoke dynamic — these can be inlined by the JIT.

Security: setAccessible(true) bypasses Java's access control. Java 17+ strong encapsulation blocks this for JDK internal classes. Modules (module-info.java) control which packages allow deep reflection.

53
What is the difference between composition and inheritance? When do you favour each?Medium

Inheritance ("is-a"): a class extends another, inheriting state and behaviour. Creates tight coupling — subclass depends on superclass internals. Any change to the superclass can break subclasses (fragile base class problem).

Composition ("has-a"): a class holds references to objects providing the behaviour it needs. Much more flexible — you can swap implementations, combine multiple behaviours, and test in isolation.

// Prefer this (composition):
class UserService {
    private final UserRepository repo;      // composed
    private final EmailService emailSvc;    // composed
    private final AuditLogger audit;        // composed
}

// Over this (inheritance chain):
class UserService extends BaseService extends AbstractRepository { ... }

Rule: inherit when the relationship truly is "is-a" and the subclass is a specialisation (not just reuse). Compose when you need flexibility, multiple behaviours, or to avoid coupling to internals. Effective Java: "Favour composition over inheritance."

54
What is the Optional class and what common mistakes do developers make with it?Medium
// Good use: represent optional return value from a method
Optional<User> findByEmail(String email);

// Chain without null checks:
findByEmail("alice@example.com")
    .map(User::getProfile)
    .filter(p -> p.isVerified())
    .ifPresentOrElse(this::sendWelcome, this::sendVerificationEmail);

Common mistakes:

  • optional.get() without checking — throws NoSuchElementException. Use orElse, orElseGet, or orElseThrow.
  • Using Optional as a field or method parameter — it's designed as a return type only.
  • optional.isPresent() + optional.get() — just use optional.map() or ifPresent().
  • Wrapping primitives in Optional<Integer> — use OptionalInt, OptionalLong.
  • Using orElse(expensiveCall())orElse always evaluates the argument. Use orElseGet(() -> expensiveCall()) for lazy evaluation.
55
What is lazy initialisation and the double-checked locking idiom?Hard
// Correct double-checked locking (requires volatile):
public class HeavyResource {
    private static volatile HeavyResource instance;

    public static HeavyResource getInstance() {
        if (instance == null) {              // first check (no lock)
            synchronized (HeavyResource.class) {
                if (instance == null) {      // second check (with lock)
                    instance = new HeavyResource();
                }
            }
        }
        return instance;
    }
}

volatile is essential. Without it, the JIT may reorder instance = new HeavyResource() — another thread may see a partially constructed object. Volatile establishes a happens-before relationship on the write.

In most cases the initialization-on-demand holder (Q35) is simpler and equally correct. Use DCL only when you need runtime-configurable lazy init that the holder pattern can't express.

56
What is Java serialisation and what are its problems?Medium

Java serialisation converts an object graph to a byte stream (ObjectOutputStream) and back (ObjectInputStream). Enabled by implementing Serializable.

Problems:

  • Security — deserialisation of untrusted data is one of the most dangerous Java vulnerabilities. Remote code execution is possible if attacker-controlled bytes are deserialised (Apache Commons Collections exploit).
  • MaintenanceserialVersionUID must be managed. Changing class fields breaks compatibility.
  • Performance — slow compared to JSON (Jackson), Protobuf, or Avro.
  • No schema — no versioning, no cross-language support.

Modern alternatives: Jackson JSON, Protocol Buffers, Avro, Kryo. Only use Java serialisation for JVM-internal caching (e.g. Ehcache) and only with deserialization filters (ObjectInputFilter).

57
What is method inlining and how does it affect performance?Hard

Method inlining is when the JIT replaces a method call with the body of the callee — eliminating the call overhead and enabling further optimisations on the combined code.

// Before inlining:
int result = add(a, b);
int add(int x, int y) { return x + y; }

// After inlining (JIT does this):
int result = a + b;  // call eliminated

Inlining triggers further optimisations: constant folding, dead code elimination, escape analysis on the inlined result.

Inlining limits: JIT won't inline methods over ~325 bytecodes (-XX:MaxInlineSize) or call chains deeper than ~9 levels (-XX:MaxInlineLevel). In hot code, keep methods small to stay inlineable. Diagnose with -XX:+PrintInlining.

58
What is the difference between deep copy and shallow copy in Java?Medium
  • Shallow copy — copies the top-level object. Nested objects are shared (both original and copy point to the same inner objects). Object.clone() and copy constructors produce shallow copies by default.
  • Deep copy — recursively copies all nested objects. Modifications to the copy don't affect the original.
// Deep copy approaches:
// 1. Manual copy constructor — most explicit and fastest
// 2. Serialization round-trip (slow, requires Serializable)
// 3. Jackson: objectMapper.readValue(objectMapper.writeValueAsString(obj), Class)
// 4. ModelMapper / MapStruct for DTOs

// Immutable records avoid the problem entirely:
record Point(int x, int y) {} // no copy needed, safe to share

Best design: make objects immutable (records, final fields). Then you never need to copy them — sharing is safe.

59
How does the Java module system (JPMS) work and why was it introduced?Medium

Introduced in Java 9, JPMS (Project Jigsaw) adds a module layer above packages. Each module declares what it exports and what it requires:

// module-info.java
module com.example.userservice {
    requires spring.core;
    requires spring.web;
    exports com.example.userservice.api;     // public API
    // com.example.userservice.internal not exported → inaccessible
}

Why introduced:

  • Strong encapsulation — internal packages truly inaccessible to other modules (no more sun.* abuse)
  • Reliable configuration — missing or circular dependencies caught at startup, not runtime
  • Smaller JREs — jlink creates custom JREs with only required modules (important for containers)

In practice most teams use the classpath still. JPMS is most valuable for library authors and applications that need small Docker images.

60
If given a slow REST endpoint, walk through how you would diagnose and fix it.Hard

This is the classic senior-level "real world" question. A structured approach:

  1. Measure first — capture response time percentiles (p50, p95, p99). Is it consistently slow or intermittently slow?
  2. Check the logs — enable Spring Boot slow query logging: spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=100
  3. Check the database — is the slow query hitting an index? Run EXPLAIN ANALYZE. Add an index if needed.
  4. N+1 problem? — count the queries per request in the Hibernate SQL log. If 1 request fires 50 queries, fix with JOIN FETCH or @EntityGraph.
  5. External calls — is the endpoint calling another service or API? Time each call. Add circuit breakers and timeouts if missing.
  6. Thread pool saturation — check Actuator: /actuator/metrics/executor.active. If threads are maxed out, the endpoint is queued.
  7. Memory/GC — high GC activity causes stop-the-world pauses. Check jstat -gcutil <pid> 1000.
  8. Flame graph — async-profiler on the live process. The widest frame is the bottleneck.
  9. Caching — is this data read-heavy and rarely changing? Add @Cacheable with Redis.
Fix the biggest issue first, measure again, repeat. Don't optimise blindly — 90% of the time it's the database.

Next Steps

Now that you have covered Java internals, continue with other interview topics: