Skip to main content
PProfileMatch

Java Interview Questions and Answers

26 questions8 beginner · 12 intermediate · 6 advancedLast reviewed

This set is for developers preparing for Java roles, from fresher screening rounds to experienced backend interviews. It starts with language fundamentals, moves through collections, exceptions and generics, and ends with concurrency, memory management and the newer language features that interviewers like to probe.

The best way to use it is to read only the question, say your answer out loud in under a minute, and then compare it with the model answer and the "what they are checking" line. Run the code samples yourself and change them. If a topic feels shaky, the Java course and the Advanced Java course cover it step by step.

  1. 1What is the difference between the JVM, the JRE and the JDK?Beginner

    The JVM is the virtual machine that loads class files, verifies them and runs the bytecode, either interpreting it or compiling hot paths to native code with the JIT. It is what makes compiled Java portable: the same bytecode runs on any platform that has a JVM.

    The JRE is the JVM plus the standard class libraries needed to run programs. The JDK is the JRE plus the developer tools: the javac compiler, jar, javadoc, debuggers and profilers. You need a JDK to build, only a runtime to run. In recent versions you normally just install a JDK, because a separate JRE download is largely gone.

    What the interviewer is checking: Whether you understand the compile-once, run-anywhere model and where the compiler and runtime sit.

    Likely follow-up: What does the JIT compiler do, and why is a Java program often slower in the first few seconds?

  2. 2Why is String immutable in Java, and what does that buy you?Beginner

    A String object cannot change after it is created: every method that looks like a modification, such as replace or toUpperCase, returns a new object. The class is final and its internal data is private, so nobody can subclass it to sneak in mutability.

    This gives several benefits. Literals can be shared through the string pool because nobody can alter a shared instance. The hash code can be cached, which makes strings cheap map keys. Strings are safe to share between threads without locking. And they are safe to use for things like file paths, class names and connection URLs, because a validated value cannot change after the check.

    The cost is garbage when you build text in a loop, so use StringBuilder there.

    What the interviewer is checking: Whether you can give concrete reasons (pool, hash caching, thread safety, security) instead of just saying it cannot be changed.

    Likely follow-up: Does a final String variable mean the same thing as an immutable String?

  3. 3Is Java pass-by-value or pass-by-reference? Show what happens with an object argument.Beginner

    Java is always pass-by-value. For an object, the value being copied is the reference, not the object. So the method gets its own copy of the pointer, and both pointers lead to the same object.

    That means a method can change the object's fields and the caller sees it, but if the method reassigns its parameter to a new object, only the local copy changes and the caller's variable still points at the original. The snippet shows both effects at once.

    java
    public class A {
        static class Parcel {
            String label;
            Parcel(String label) { this.label = label; }
        }
    
        static void relabel(Parcel p) {
            p.label = "LOCKER-7";      // mutates the shared object
            p = new Parcel("LOST");    // rebinds only the local copy of the reference
        }
    
        public static void main(String[] args) {
            Parcel parcel = new Parcel("HOLD");
            relabel(parcel);
            System.out.println(parcel.label);
        }
    }

    Output

    LOCKER-7

    What the interviewer is checking: Whether you can separate mutating an object from rebinding a reference, which is where most candidates get confused.

    Likely follow-up: How would you write a method that really swaps two Parcel variables for the caller?

  4. 4What do the keywords static and final mean on a field, a method and a class?Beginner

    static ties a member to the class rather than to an instance. A static field exists once and is shared by all instances, and a static method is called on the class and has no this. Static methods are not overridden, only hidden.

    final means "cannot be changed after this point". A final variable is assigned once, a final method cannot be overridden, and a final class cannot be extended. String and the wrapper classes are final.

    The common trap is thinking a final reference makes the object immutable. It only fixes which object the variable points to. A final List can still have elements added.

    What the interviewer is checking: Precise definitions and awareness of the final-reference versus immutable-object distinction.

    Likely follow-up: When does a static initializer block run, and how many times?

  5. 5When would you choose an interface over an abstract class, and the other way round?Beginner

    Use an interface to define a capability that unrelated classes can offer, like Comparable or a PaymentGateway contract. A class can implement many interfaces, and an interface describes behaviour without dictating how objects are built. Since Java 8, interfaces can also carry default and static methods, so you can add a method without breaking existing implementors.

    Use an abstract class when the subtypes are genuinely related and share state or a partial implementation: instance fields, constructors, protected helpers. Because a class can extend only one parent, that choice is more costly.

    Default to an interface, and reach for an abstract class when you need shared state.

    What the interviewer is checking: Design judgement rather than a memorised feature table, plus awareness of default methods.

    Likely follow-up: If two interfaces provide the same default method and a class implements both, what happens?

  6. 6What is the difference between == and equals(), and why does Integer behave oddly with ==?Beginner

    For objects, == compares references: are these the very same object? equals() compares logical content, as defined by the class. For primitives, == compares values directly.

    Strings show the difference. Two literals with the same text share one pooled instance, so == is true, but new String("tea") creates a separate object, so == is false while equals is true. Boxed integers are the same kind of trap: Integer.valueOf caches values from -128 to 127, so == on small values happens to work and on large ones does not. Always compare objects with equals.

    java
    public class H {
        public static void main(String[] args) {
            String a = "tea";
            String b = "tea";
            String c = new String("tea");
            System.out.println(a == b);
            System.out.println(a == c);
            System.out.println(a.equals(c));
            System.out.println(a == c.intern());
        }
    }

    Output

    true
    false
    true
    true

    What the interviewer is checking: Reference versus value equality, plus knowing about the string pool and the boxed-integer cache.

    Likely follow-up: What does intern() do, and would you use it in production code?

  7. 7Explain checked versus unchecked exceptions. Which should your own code throw?Beginner

    Checked exceptions extend Exception but not RuntimeException, and the compiler forces callers to catch them or declare them. Unchecked exceptions are RuntimeException and its subclasses, plus Error; the compiler does not force handling.

    The intent: checked for recoverable conditions the caller can reasonably act on, like a missing file or a network timeout; unchecked for programming mistakes such as a null argument or an illegal state. In practice many modern codebases and frameworks lean on unchecked exceptions, because checked ones leak through layers and clutter lambdas.

    My rule: throw unchecked for bugs and contract violations, checked only when the caller has a meaningful recovery path, and never swallow an exception silently.

    What the interviewer is checking: Whether you know the hierarchy and can argue a design stance instead of reciting the definition.

    Likely follow-up: Why are checked exceptions awkward inside a stream lambda?

  8. 8ArrayList or LinkedList: which would you pick, and why is the textbook answer misleading?Beginner

    ArrayList is backed by a resizable array: indexed access is constant time, appending is amortised constant, and inserting or removing in the middle shifts elements. LinkedList is a doubly linked list: it has constant-time insertion at the ends or at an iterator's position, but reaching element i means walking the nodes, which is linear.

    The textbook says use linked lists for frequent insertions. In practice ArrayList wins most workloads, because contiguous memory is cache-friendly and every LinkedList node is an extra object with two pointers. Even mid-list removal is often faster on an array for realistic sizes, since finding the position costs more than the shift.

    I pick ArrayList by default, and ArrayDeque if I need a queue or stack.

    What the interviewer is checking: Big-O knowledge combined with practical awareness of memory layout.

    Likely follow-up: Why is ArrayDeque usually a better queue than LinkedList?

  9. 9State the equals() and hashCode() contract. What breaks if you override only equals()?Intermediate

    If two objects are equal according to equals, they must return the same hashCode. equals must also be reflexive, symmetric, transitive and consistent. Unequal objects may share a hash code, but fewer collisions mean faster lookups.

    Hash-based collections use the hash to choose a bucket first and only then call equals. If you override equals alone, two logically equal objects usually land in different buckets, so HashSet.contains or HashMap.get fails to find them. The snippet below shows a coupon that is equal to its twin but still not found. Use the same fields in both methods, and prefer Objects.hash or a record, which generates both.

    java
    import java.util.*;
    
    public class B {
        static class Coupon {
            final String code;
            Coupon(String code) { this.code = code; }
            @Override public boolean equals(Object o) {
                return o instanceof Coupon c && c.code.equals(code);
            }
            // hashCode deliberately NOT overridden
        }
    
        public static void main(String[] args) {
            Set<Coupon> used = new HashSet<>();
            used.add(new Coupon("SAVE10"));
            System.out.println(used.contains(new Coupon("SAVE10")));
            System.out.println(new Coupon("SAVE10").equals(new Coupon("SAVE10")));
        }
    }

    Output

    false
    true

    What the interviewer is checking: Whether you connect the contract to how hash collections actually behave.

    Likely follow-up: Can two unequal objects have the same hashCode, and what does that cost?

  10. 10Walk me through how a HashMap stores and finds a key.Intermediate

    A HashMap keeps an array of buckets. On put, it calls hashCode on the key, mixes the high bits into the low bits, and uses the result to pick an index in the array (the capacity is a power of two, so this is a cheap bit mask). If the bucket is empty the entry is stored there. If not, it walks the bucket comparing hashes and then equals, replacing the value on a match or appending a new entry.

    Collisions are chained. Since Java 8, a bucket that grows long can be converted to a balanced tree, which protects lookups against many colliding keys. When the number of entries passes capacity times the load factor (default 0.75), the table doubles and entries are redistributed. get follows the same path: hash, index, then equals.

    What the interviewer is checking: Depth on hashing, collisions, resizing and the Java 8 treeification, not just "it uses a hash table".

    Likely follow-up: Why does the HashMap capacity have to be a power of two, and when would you presize it?

  11. 11What goes wrong if you mutate an object that is already a HashMap key?Intermediate

    The entry is stored in a bucket chosen from the hash at insertion time. If you change a field that feeds hashCode, the key now hashes somewhere else, so the map looks in the wrong bucket and finds nothing, while the old entry sits where nobody will look for it.

    In the example, the bay key is changed from slot 1 to slot 2 after insertion. A lookup with the mutated key fails, a lookup with a fresh slot-1 key also fails because equals now disagrees, and yet the map still reports size 1. That is effectively a memory leak.

    So keys should be immutable, or at least the fields used in hashCode and equals must never change while the object is in a map. Strings, enums and records are good keys.

    java
    import java.util.*;
    
    public class I {
        static class Key {
            int slot;
            Key(int slot) { this.slot = slot; }
            @Override public boolean equals(Object o) { return o instanceof Key k && k.slot == slot; }
            @Override public int hashCode() { return Integer.hashCode(slot); }
        }
    
        public static void main(String[] args) {
            Map<Key, String> bays = new HashMap<>();
            Key k = new Key(1);
            bays.put(k, "van");
            k.slot = 2;                       // key mutated after insertion
            System.out.println(bays.get(k));
            System.out.println(bays.get(new Key(1)));
            System.out.println(bays.size());
        }
    }

    Output

    null
    null
    1

    What the interviewer is checking: Practical understanding of why key immutability matters.

    Likely follow-up: How would you make a composite key safe to use in a map?

  12. 12Why does removing an element inside a for-each loop throw ConcurrentModificationException?Intermediate

    Most collection iterators are fail-fast. The collection keeps a modification counter; the iterator records it when created and compares it on every next. If the list was structurally changed by anything other than the iterator itself, the counter differs and the iterator throws.

    It is a best-effort bug detector, not a thread-safety guarantee, and it can also fire in single-threaded code, as below. Safe ways to remove during traversal are Iterator.remove(), removeIf, or collecting into a new list. For concurrent use, copy-on-write or concurrent collections have weakly consistent iterators that never throw this exception.

    One quirk: removing the second-to-last element happens not to throw, because hasNext returns false first. Do not rely on that.

    java
    import java.util.*;
    
    public class D {
        public static void main(String[] args) {
            List<String> queue = new ArrayList<>(List.of("ana", "bo", "cy", "di"));
            try {
                for (String name : queue) {
                    if (name.equals("bo")) queue.remove(name);
                }
            } catch (ConcurrentModificationException e) {
                System.out.println("fail-fast: " + e.getClass().getSimpleName());
            }
            queue.removeIf(n -> n.length() == 2);
            System.out.println(queue);
        }
    }

    Output

    fail-fast: ConcurrentModificationException
    [ana]

    What the interviewer is checking: Whether you know the mechanism and the correct alternatives, not just the exception name.

    Likely follow-up: What does weakly consistent mean for a ConcurrentHashMap iterator?

  13. 13HashMap, Hashtable, Collections.synchronizedMap and ConcurrentHashMap: how do they differ?Intermediate

    HashMap is not thread-safe and allows one null key and null values. Hashtable is a legacy class where every method is synchronized, so it is safe but slow under contention, and it rejects nulls. Collections.synchronizedMap wraps any map in one lock, with the same bottleneck, and you must still lock manually while iterating.

    ConcurrentHashMap is built for concurrency: reads mostly do not block, writes lock only a small part of the table (in modern versions, a single bucket), and it also rejects null keys and values. Its compute, merge and putIfAbsent are atomic, which is how you do safe check-then-act. Its size is an estimate under concurrent updates.

    Reach for ConcurrentHashMap for shared caches and counters; a plain get followed by put is still a race.

    What the interviewer is checking: Whether you know why a synchronized wrapper is not the same as a concurrent design, and the atomic compound operations.

    Likely follow-up: How would you implement a thread-safe word counter using ConcurrentHashMap?

  14. 14What does this method return, and why? try { throw ... } finally { return 42; }Intermediate

    It returns 42 and the exception disappears. A finally block always runs, whether the try block completes, returns or throws. If finally itself executes a return, that return replaces whatever was pending, including an in-flight exception. The compiler may warn about it.

    So never return, break or throw from finally, because it can hide the real failure. Use finally only for cleanup. The few cases where it does not run are the JVM exiting through System.exit, a crash, or the thread being killed.

    For resource cleanup, prefer try-with-resources over a manual finally.

    java
    public class C {
        @SuppressWarnings("finally")
        static int attempt() {
            try {
                throw new IllegalStateException("gate jammed");
            } finally {
                return 42;
            }
        }
        public static void main(String[] args) {
            System.out.println(attempt());
        }
    }

    Output

    42

    What the interviewer is checking: Understanding of finally semantics and the danger of swallowed exceptions.

    Likely follow-up: If the try block returns a variable and finally modifies it, which value does the caller get?

  15. 15How does try-with-resources work, and what are suppressed exceptions?Intermediate

    Any resource that implements AutoCloseable and is declared in the parentheses is closed automatically when the block ends, normally or by exception. Resources close in reverse order of declaration, so the last one opened closes first.

    If the body throws and close() also throws, the body's exception is the one propagated, and the close failure is attached to it as a suppressed exception, retrievable with getSuppressed(). A hand-written finally would let the close exception replace the original, losing the root cause.

    The output shows B closing before A, the jam from the body being thrown, and both close failures attached beneath it.

    java
    public class K {
        static class Gate implements AutoCloseable {
            final String name;
            Gate(String name) { this.name = name; System.out.println("open " + name); }
            void pass(boolean fail) { if (fail) throw new IllegalStateException("jam at " + name); }
            @Override public void close() {
                System.out.println("close " + name);
                throw new RuntimeException("close failed " + name);
            }
        }
    
        public static void main(String[] args) {
            try (Gate a = new Gate("A"); Gate b = new Gate("B")) {
                b.pass(true);
            } catch (Exception e) {
                System.out.println(e.getMessage());
                for (Throwable s : e.getSuppressed()) System.out.println("suppressed: " + s.getMessage());
            }
        }
    }

    Output

    open A
    open B
    close B
    close A
    jam at B
    suppressed: close failed B
    suppressed: close failed A

    What the interviewer is checking: Resource-handling discipline and knowledge of close ordering and exception priority.

    Likely follow-up: Can you use an existing variable in try-with-resources, and what must be true of it?

  16. 16Explain lazy evaluation in streams. What does this pipeline actually execute?Intermediate

    Intermediate operations such as filter, map and flatMap only describe the pipeline; nothing runs until a terminal operation like toList or collect pulls elements through. Elements flow one at a time through the whole chain, and short-circuiting operations like limit or findFirst can stop the source early.

    In the example, the stream asks for two distinct matching items. After the second order produces "lamp", the limit is satisfied, so the third order is never visited, which the peek output proves. A stream is also single-use: consuming it twice throws IllegalStateException.

    Keep lambdas free of side effects, because order and number of executions are not something to depend on.

    java
    import java.util.*;
    import java.util.stream.*;
    
    public class E {
        record Order(String id, List<String> items) {}
    
        public static void main(String[] args) {
            List<Order> orders = List.of(
                new Order("o1", List.of("tea", "mug")),
                new Order("o2", List.of("lamp")),
                new Order("o3", List.of("tea")));
    
            List<String> distinct = orders.stream()
                .peek(o -> System.out.println("visit " + o.id()))
                .flatMap(o -> o.items().stream())
                .filter(i -> i.startsWith("t") || i.equals("lamp"))
                .distinct()
                .limit(2)
                .toList();
            System.out.println(distinct);
        }
    }

    Output

    visit o1
    visit o2
    [tea, lamp]

    What the interviewer is checking: Whether you understand pull-based, short-circuiting pipelines and why side effects are risky.

    Likely follow-up: What is the difference between map and flatMap, and when do you need the latter?

  17. 17How should Optional be used, and where is it considered a misuse?Intermediate

    Optional is meant as a return type that says "there may be no result", forcing the caller to deal with absence. Use it with map, filter, orElseGet or orElseThrow rather than isPresent() followed by get(), which is just a null check with extra steps.

    Misuse includes using it for fields, method parameters or collection elements, wrapping it in other Optionals, or returning an Optional where an empty list would do. It is also not serialisable by design and adds an allocation.

    One detail interviewers like: orElse(x) evaluates x eagerly even when a value is present, while orElseGet(supplier) runs only when needed. If the default is expensive or has side effects, use orElseGet.

    What the interviewer is checking: Whether you treat Optional as an API-design tool and know the orElse versus orElseGet difference.

    Likely follow-up: Why is Optional.get() with no check considered a code smell?

  18. 18What is type erasure, and what limits does it put on generics?Intermediate

    Generics are checked by the compiler, and then the type arguments are erased: at runtime a List<String> and a List<Integer> are the same class (getClass() returns equal results for both). The compiler inserts casts where needed.

    Consequences: you cannot write new T() or new T[10], cannot do instanceof List<String>, and cannot overload methods that differ only by type argument. Primitives cannot be type arguments, so you box. Generic arrays are a classic source of unchecked warnings.

    For flexibility, remember PECS: use ? extends T when you only read from a structure (producer), and ? super T when you only write into it (consumer). A method that sums prices can take List<? extends Number>, and a method that fills a sink can take List<? super Integer>.

    What the interviewer is checking: Knowledge of what exists at runtime and of wildcard rules, not only the syntax.

    Likely follow-up: Why is List<Integer> not a subtype of List<Number>?

  19. 19What is the difference between synchronized and volatile? When is volatile not enough?Intermediate

    synchronized gives mutual exclusion (one thread in the block at a time) and also visibility: changes made inside are visible to the next thread that takes the same lock. volatile gives only visibility and ordering for a single variable: a write is seen by subsequent reads in other threads, and it prevents certain reorderings.

    That is why volatile suits a stop flag or a safely published reference, but not a counter. count++ is read, add, write, three steps, and two threads can interleave them and lose an update, volatile or not. For that, use synchronized, a lock, or AtomicInteger/LongAdder.

    Rule of thumb: volatile for one independent flag, atomics for single counters, locks when several fields must change together.

    What the interviewer is checking: Clear separation of atomicity from visibility.

    Likely follow-up: What does the happens-before relationship guarantee in the Java Memory Model?

  20. 20What do records and sealed types give you, and how do they combine with switch?Intermediate

    A record is a compact, final data carrier: you declare the components and the compiler generates the constructor, accessors, equals, hashCode and toString. Records are standard since Java 16. Sealed types, standard since Java 17, restrict which classes may implement or extend a type, using permits.

    Together they model closed alternatives, like a fare that is flat, per-kilometre or surged. With pattern matching for switch (standard since Java 21), the compiler knows every permitted subtype, so the switch needs no default branch, and adding a new subtype turns missing cases into compile errors rather than runtime surprises.

    Records are shallowly immutable: a record holding a mutable list still exposes that list.

    java
    public class G {
        sealed interface Fare permits Flat, PerKm, Surge {}
        record Flat(double amount) implements Fare {}
        record PerKm(double km, double rate) implements Fare {}
        record Surge(Fare base, double multiplier) implements Fare {}
    
        static double price(Fare f) {
            return switch (f) {
                case Flat fl -> fl.amount();
                case PerKm p -> p.km() * p.rate();
                case Surge s -> price(s.base()) * s.multiplier();
            };
        }
    
        public static void main(String[] args) {
            Fare trip = new Surge(new PerKm(12, 1.5), 2);
            System.out.println(price(trip));
            System.out.println(trip);
        }
    }

    Output

    36.0
    Surge[base=PerKm[km=12.0, rate=1.5], multiplier=2.0]

    What the interviewer is checking: Awareness of modern Java and the design benefit of exhaustive checking.

    Likely follow-up: When would you still choose a normal class over a record?

  21. 21How would you size and configure a thread pool, and why is Executors.newFixedThreadPool risky?Advanced

    Size depends on the workload. CPU-bound work wants roughly one thread per core; I/O-bound work spends time waiting, so more threads help, bounded by the downstream system and memory. Measure rather than guess.

    newFixedThreadPool uses an unbounded queue, so a slow consumer lets tasks pile up until memory runs out, with no back-pressure. I prefer constructing a ThreadPoolExecutor directly with a bounded queue, named threads, and a deliberate rejection policy. Remember that a pool grows past its core size only when the queue is full.

    Always shut down: shutdown() stops accepting tasks and lets queued ones finish, shutdownNow() interrupts workers. Call get on futures, or exceptions inside tasks vanish silently.

    What the interviewer is checking: Operational judgement: bounded queues, back-pressure, shutdown and error visibility.

    Likely follow-up: What happens when a bounded pool and its queue are both full and another task arrives?

  22. 22How can a deadlock happen, and how do you prevent and diagnose one?Advanced

    A deadlock needs threads that each hold a lock and wait for another that is held by the other side. Thread 1 holds the ledger lock and wants the audit lock while thread 2 holds audit and wants ledger, so neither moves. It needs mutual exclusion, hold-and-wait, no pre-emption and a circular wait; break any one.

    The practical fix is a global lock ordering, so every thread acquires locks in the same sequence, as in the example. Others: hold fewer locks and for shorter times, use tryLock with a timeout and back off, or avoid shared locks with immutable data and message passing.

    To diagnose a hung service, take thread dumps with jstack or jcmd Thread.print; the JVM reports the detected lock cycle and the stack of each blocked thread.

    java
    public class L {
        static final Object ledger = new Object();
        static final Object audit = new Object();
    
        static void transfer(Object first, Object second, String who) {
            synchronized (first) {
                try { Thread.sleep(50); } catch (InterruptedException e) { }
                synchronized (second) {
                    System.out.println(who + " done");
                }
            }
        }
    
        public static void main(String[] args) throws Exception {
            // Both threads take locks in the SAME global order: ledger, then audit.
            Thread t1 = new Thread(() -> transfer(ledger, audit, "t1"));
            Thread t2 = new Thread(() -> transfer(ledger, audit, "t2"));
            t1.start(); t2.start();
            t1.join(); t2.join();
            System.out.println("no deadlock");
        }
    }

    Output

    t1 done
    t2 done
    no deadlock

    What the interviewer is checking: Whether you can explain the cause and give concrete prevention and diagnosis techniques.

    Likely follow-up: What is the difference between a deadlock, a livelock and starvation?

  23. 23Why does double-checked locking need volatile? Show a correct lazy singleton.Advanced

    The pattern checks the field without a lock for speed, then locks and checks again before creating the object. Without volatile, the reference assignment can become visible to another thread before the constructor's writes do, because the compiler and CPU may reorder them. That thread could then see a non-null but partly constructed object.

    Declaring the field volatile forbids that reordering, establishing a happens-before edge between publication and reading. The second check inside the lock stops two threads that both passed the first check from creating two instances.

    In practice I avoid the pattern: the initialization-on-demand holder class, or a single-element enum, gives lazy, thread-safe initialisation with no explicit synchronisation. And in a Spring application, the container's singleton scope removes the need entirely.

    What the interviewer is checking: Depth on the Java Memory Model: safe publication, reordering and happens-before.

    Likely follow-up: How does the holder-class idiom get thread safety without any locks in your code?

  24. 24How does garbage collection decide what to free, and how do memory leaks still happen in Java?Advanced

    The collector frees objects that are no longer reachable from GC roots such as thread stacks, static fields and JNI references. Most JVMs use generational collection: new objects start in a young generation that is collected often and cheaply, survivors are promoted to the old generation, which is collected less often. G1 has been the default collector since Java 9; ZGC and others target very short pauses.

    A leak in Java means objects that are still reachable but no longer needed: an unbounded static cache, listeners never deregistered, a ThreadLocal in a pooled thread that is never cleared, or unclosed resources.

    To investigate, enable -XX:+HeapDumpOnOutOfMemoryError, open the dump in a heap analyser, and follow the path from a GC root to the biggest retained set. Tune only after measuring.

    What the interviewer is checking: Reachability-based thinking and a realistic debugging workflow instead of memorised collector names.

    Likely follow-up: What is the difference between the heap, the stack and metaspace?

  25. 25What are virtual threads, and when would they not help?Advanced

    Virtual threads, final in Java 21, are lightweight threads scheduled by the JVM onto a small pool of platform "carrier" threads. When a virtual thread blocks on I/O, it is unmounted and the carrier runs another one. This lets you write simple blocking code, one thread per request, and still handle huge numbers of concurrent waits, since creating one is cheap.

    They help when tasks spend most of their time waiting: HTTP calls, database queries, queues. They do not make CPU-bound work faster, because you still have only as many cores. Do not pool them; create one per task. In Java 21, blocking inside a synchronized block can pin the carrier, so prefer ReentrantLock on hot blocking paths. Also watch downstream limits: ten thousand threads can overwhelm a database, so cap with a semaphore.

    java
    import java.util.*;
    import java.util.concurrent.*;
    
    public class M {
        public static void main(String[] args) throws Exception {
            try (ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor()) {
                List<Future<String>> fs = new ArrayList<>();
                for (int i = 0; i < 3; i++) {
                    int id = i;
                    fs.add(vt.submit(() -> {
                        Thread.sleep(10);
                        return "call-" + id + " virtual=" + Thread.currentThread().isVirtual();
                    }));
                }
                for (var f : fs) System.out.println(f.get());
            }
        }
    }

    Output

    call-0 virtual=true
    call-1 virtual=true
    call-2 virtual=true

    What the interviewer is checking: Awareness of current Java concurrency and the judgement to know when it does and does not apply.

    Likely follow-up: How would you limit concurrent database calls when each request runs on its own virtual thread?

  26. 26When can a parallel stream make code slower or wrong?Advanced

    A parallel stream splits the source and runs chunks on the shared common ForkJoinPool. It helps for CPU-heavy, stateless work over large, easily split sources such as arrays and ArrayList. It hurts on small data, where splitting and merging cost more than the work, and on poorly splittable sources like LinkedList or Stream.iterate.

    It is wrong when lambdas share mutable state, such as adding to a plain ArrayList from forEach, which is a data race. Reductions must be associative and stateless. Blocking I/O inside a parallel stream ties up the common pool that the rest of the JVM also uses, so unrelated work stalls.

    I would measure with a realistic data size first, and for I/O use an explicit executor instead.

    What the interviewer is checking: Whether you treat parallelism as an optimisation to measure rather than a free speedup.

    Likely follow-up: How would you run a parallel stream on your own pool rather than the common one?

Fill the gaps with a free course

If one of these Java answers felt shaky, these 3 courses cover the same ground step by step: Java, Advanced Java, Object-Oriented Programming.

More interview questions

Practising Java for a real opening? Upload your resume to see which open jobs list Java skills you already have, or browse open roles.