Prev Next

Java / Java 21 Interview Questions

1. What is Java 21 and why is it a significant release? 2. What are Virtual Threads in Java 21 and how do they differ from Platform Threads? 3. How does Pattern Matching for switch work in Java 21? 4. What are Record Patterns in Java 21 and how do they enable deconstruction? 5. What are Sequenced Collections in Java 21? 6. What are sealed classes and interfaces in Java and why are they important for pattern matching? 7. What are Java Records and what do they automatically generate? 8. What are Text Blocks in Java and how do you use them? 9. What is Structured Concurrency in Java 21 and what problem does it solve? 10. What are Scoped Values in Java 21 and how do they differ from ThreadLocal? 11. What is Generational ZGC in Java 21 and why does it improve upon the original ZGC? 12. What important String methods were added from Java 11 through Java 21? 13. What is 'var' in Java and what are its limitations? 14. What are switch expressions in Java and how do they differ from switch statements? 15. How does pattern matching for instanceof work in Java 16+? 16. What Stream API improvements were introduced in Java 9 through Java 21? 17. How has Optional been improved and how should it be used correctly? 18. How does CompletableFuture work in Java and how does it relate to virtual threads? 19. What does the 'volatile' keyword guarantee in Java's memory model? 20. What are the immutable collection factory methods introduced in Java 9? 21. What are functional interfaces in Java and how are lambdas related to them? 22. What are the most important Collectors and how do you write custom ones? 23. What are Unnamed Classes and Instance Main Methods in Java 21 (Preview)? 24. What improvements were made to NullPointerException messages in Java 14? 25. How does type erasure affect instanceof checks with generics in Java? 26. What are the differences between 'synchronized' and ReentrantLock in Java? 27. What are the key classes in the java.time package and when do you use each? 28. What garbage collectors are available in Java 21 and how do you choose between them? 29. What is the Java Platform Module System (JPMS) and when should you use it? 30. What are String Templates in Java 21 (Preview) and how do they improve string interpolation? 31. What are the contracts for equals(), hashCode(), and Comparable in Java? 32. What are the best practices for exception handling in Java? 33. Why is immutability important in Java and how do you implement it correctly? 34. What are default and static methods in Java interfaces? 35. How does HashMap work internally in Java? 36. How do virtual threads compare to reactive programming (Project Reactor / RxJava)? 37. What is the Java 11 HttpClient and how do you use it for HTTP requests? 38. What capabilities do Java enums have beyond simple named constants? 39. What are the java.util.concurrent.atomic classes and how do they work? 40. How does Java Reflection work and what are its performance implications? 41. What are Unnamed Patterns and Variables in Java 21 (Preview) and how do they reduce boilerplate? 42. When should you use StructuredTaskScope instead of CompletableFuture in Java 21? 43. What major APIs were removed between Java 17 and Java 21? 44. What are the most important JVM flags for tuning Java 21 application performance? 45. What are the key steps and pitfalls when migrating an application to Java 21? 46. How do Spring 7 and Spring Boot 4 optimize thread usage compared to older blocking models?
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. What is Java 21 and why is it a significant release?

Java 21, released in September 2023, is a Long-Term Support (LTS) release of the Java platform, making it the next major production-grade baseline after Java 17. It is the first LTS release to deliver Virtual Threads as a stable feature (Project Loom), Record Patterns, Pattern Matching for switch, and Sequenced Collections — all finalised and ready for production use.

The LTS designation means Oracle and other JDK vendors commit to extended support windows (typically 5–8 years depending on the vendor), making Java 21 the version most enterprises will target for migration from Java 11 or 17.

Key Java 21 JEPs (Finalised)
JEPFeatureCategory
444Virtual ThreadsConcurrency (Loom)
441Pattern Matching for switchLanguage
440Record PatternsLanguage
431Sequenced CollectionsCore Libraries
439Generational ZGCGC
445Unnamed Classes and Instance Main Methods (Preview)Language
453Structured Concurrency (Preview)Concurrency
446Scoped Values (Preview)Concurrency

Java 21 also continues the deprecation of thread primitives like Thread.stop(), and permanently removes the Security Manager, Applet API, and older RMI features that were deprecated in prior releases.

What concurrency feature was finalised and became production-ready in Java 21?
What type of release is Java 21?
2. What are Virtual Threads in Java 21 and how do they differ from Platform Threads?

Virtual Threads (JEP 444) are lightweight threads managed by the JVM rather than the operating system. A platform (OS) thread maps 1:1 to a kernel thread and consumes roughly 1–2 MB of stack memory each, limiting practical concurrency to a few thousand threads per JVM. Virtual threads are multiplexed over a small pool of carrier (platform) threads by the JVM scheduler, consuming only a few hundred bytes of heap per thread, enabling millions of concurrent threads in the same process.

Platform Threads vs Virtual Threads
AspectPlatform ThreadVirtual Thread
Managed byOS kernelJVM (Project Loom)
Memory (stack)~1–2 MB each~few hundred bytes (heap)
Max practical countThousandsMillions
Blocking I/OBlocks OS threadParks virtual thread; carrier thread freed
CreationThread / ExecutorServiceThread.ofVirtual() / Executors.newVirtualThreadPerTaskExecutor()
// Creating a virtual thread directly
Thread vt = Thread.ofVirtual().name("my-vt").start(() -> {
    System.out.println("Running on: " + Thread.currentThread());
});

// Per-task executor — one virtual thread per submitted task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofMillis(100)); // parks, not blocks carrier
            return i * i;
        }));
} // executor.close() waits for all tasks

When a virtual thread blocks on I/O or Thread.sleep(), the JVM parks it (saves its continuation to the heap) and reassigns the carrier thread to another runnable virtual thread — no OS thread is held idle. This is fundamentally different from reactive/async programming: you write simple blocking code and the JVM handles the event-loop-like scheduling transparently.

Key constraints: Synchronized blocks pin the virtual thread to its carrier (no parking during a synchronized block). Use java.util.concurrent.locks.ReentrantLock instead of synchronized in hot paths to avoid pinning. Thread-locals work but can cause high memory usage at scale — prefer Scoped Values (JEP 446).

What happens when a virtual thread calls Thread.sleep() or performs blocking I/O in Java 21?
Which Java 21 API creates an executor that gives each submitted task its own virtual thread?
3. How does Pattern Matching for switch work in Java 21?

JEP 441 finalises pattern matching in switch expressions and statements, extending the type-test patterns introduced in Java 16 (instanceof) to the full switch construct. It eliminates long chains of if-else instanceof casts and brings exhaustiveness checking to the compiler.

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 expression
static double area(Shape s) {
    return switch (s) {
        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 — sealed hierarchy is exhaustive
    };
}

// Guarded patterns — 'when' clause adds a boolean condition
static String classify(Object obj) {
    return switch (obj) {
        case Integer i when i < 0  -> "Negative int";
        case Integer i when i == 0 -> "Zero";
        case Integer i             -> "Positive int: " + i;
        case String s  when s.isEmpty() -> "Empty string";
        case String s              -> "String: " + s;
        case null                  -> "null value";
        default                    -> "Other: " + obj;
    };
}

// Dominance rule: more specific patterns must come first
// case Integer i when i < 0 must precede case Integer i

Three important rules in Java 21 pattern switch:

  1. Exhaustiveness: the compiler checks that all possible input values are covered; missing a subtype of a sealed class is a compile error, not a runtime problem.
  2. Guarded patterns (when clause): replaces the old && idiom inside case blocks.
  3. Null handling: a case null arm can be written explicitly; without it a null input still throws NullPointerException as before.
What keyword is used in Java 21 pattern switch to add a boolean guard to a pattern?
When a switch is used over a sealed interface with all subtypes covered, what can be omitted?
4. What are Record Patterns in Java 21 and how do they enable deconstruction?

JEP 440 finalises record patterns, which extend the type-test pattern (instanceof Point p) to simultaneously match a record's type and destructure its components into named variables. This eliminates the boilerplate of calling accessor methods after a type check.

record Point(int x, int y) {}
record Line(Point start, Point end) {}

// Before Java 21: type check + manual accessor calls
if (obj instanceof Point p) {
    System.out.println(p.x() + ", " + p.y()); // still need accessors
}

// Java 21 record pattern: match + deconstruct in one step
if (obj instanceof Point(int x, int y)) {
    System.out.println(x + ", " + y); // x and y bound directly
}

// Nested record patterns — deconstruct a Line into its Points' components
if (shape instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
    double length = Math.hypot(x2 - x1, y2 - y1);
    System.out.println("Length: " + length);
}

// Record patterns in switch
static String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
        case Point(int x, int y) when x == 0           -> "On Y-axis";
        case Point(int x, int y)                        -> "Point(" + x + "," + y + ")";
        case Line(Point s, Point e)                     -> "Line from " + s + " to " + e;
        default                                         -> "Unknown";
    };
}

// Unnamed patterns: _ to ignore components you don't need
if (obj instanceof Point(int x, _)) {
    System.out.println("x = " + x); // y component ignored
}

Record patterns compose with sealed types, switch expressions, and the when guard clause. The unnamed pattern _ (JEP 443, preview in Java 21) lets you discard components you do not need, avoiding the need to name every field.

What does 'if (obj instanceof Point(int x, int y))' do in Java 21?
What does the underscore '_' represent in a record pattern like 'Point(int x, _)'?
5. What are Sequenced Collections in Java 21?

JEP 431 introduces three new interfaces — SequencedCollection, SequencedSet, and SequencedMap — to the collections hierarchy. Before Java 21 there was no unified API to access the first or last element of a collection, or to iterate in reverse order; each concrete class had its own ad-hoc approach.

Pre-Java 21 inconsistencies
CollectionGet firstGet lastReverse iteration
Listlist.get(0)list.get(list.size()-1)Collections.reverse() / listIterator
Dequedeque.peekFirst()deque.peekLast()descendingIterator()
SortedSetsortedSet.first()sortedSet.last()No standard way
LinkedHashSetiter.next() hackNo direct wayNo standard way
// SequencedCollection adds:
interface SequencedCollection extends Collection {
    void addFirst(E e);
    void addLast(E e);
    E getFirst();      // replaces get(0) / peekFirst()
    E getLast();       // replaces get(size-1) / peekLast()
    E removeFirst();
    E removeLast();
    SequencedCollection reversed(); // reversed VIEW
}

// Now works uniformly on List, Deque, LinkedHashSet...
List list = new ArrayList<>(List.of("a", "b", "c"));
System.out.println(list.getFirst());  // "a"
System.out.println(list.getLast());   // "c"
list.addFirst("z");                   // ["z", "a", "b", "c"]

for (String s : list.reversed()) {
    System.out.print(s + " "); // c b a z
}

// SequencedMap adds firstEntry(), lastEntry(), reversed()
SequencedMap map = new LinkedHashMap<>();
map.put("one", 1); map.put("two", 2); map.put("three", 3);
System.out.println(map.firstEntry()); // one=1
System.out.println(map.lastEntry());  // three=3

The reversed() method returns a view — it does not copy the collection. Modifications through the reversed view affect the backing collection. All List, Deque, SortedSet, LinkedHashSet, SortedMap, and LinkedHashMap implementations now implement the appropriate sequenced interface.

What does SequencedCollection.reversed() return?
Which new method does SequencedCollection add to uniformly retrieve the first element across List, Deque, and LinkedHashSet?
6. What are sealed classes and interfaces in Java and why are they important for pattern matching?

Sealed classes (JEP 409, finalised in Java 17, heavily used with Java 21 pattern matching) restrict which classes can extend or implement them. You declare the permitted subtypes explicitly in a permits clause, giving the compiler a closed world of possibilities.

// Sealed interface — only these three can implement it
public sealed interface Expr
        permits Num, Add, Mul {
}

public record Num(int value)   implements Expr {}
public record Add(Expr l, Expr r) implements Expr {}
public record Mul(Expr l, Expr r) implements Expr {}

// Evaluator — exhaustive switch, no default needed
static int eval(Expr e) {
    return switch (e) {
        case Num(int v)      -> v;
        case Add(Expr l, Expr r) -> eval(l) + eval(r);
        case Mul(Expr l, Expr r) -> eval(l) * eval(r);
    };
}

// Usage: (2 + 3) * 4 = 20
Expr expr = new Mul(new Add(new Num(2), new Num(3)), new Num(4));
System.out.println(eval(expr)); // 20

Permitted subclasses must be in the same package (or module). Each permitted class must be one of: final (no further extension), sealed (extends the hierarchy with its own permits), or non-sealed (reopens the hierarchy for any extension).

Permitted subclass modifiers
ModifierMeaning
finalNo further subclassing allowed
sealedCan be extended, but only by its own permits clause
non-sealedOpens the type back up — any class can extend it

The key synergy with Java 21: because the compiler knows the complete set of sealed subtypes, a switch over a sealed type can be checked for exhaustiveness at compile time. If you add a new subtype to the sealed hierarchy, every exhaustive switch in the codebase becomes a compile error until it is updated.

What modifier must a class use if it is permitted by a sealed class but wants to allow any further subclassing?
What compile-time guarantee do sealed classes provide when used with pattern matching switch?
7. What are Java Records and what do they automatically generate?

Records (JEP 395, finalised in Java 16) are a concise syntax for declaring immutable data-carrier classes. A single record declaration replaces a full class with private final fields, a canonical constructor, accessors, equals(), hashCode(), and toString().

// Declaration — the header defines the record's components
public record Person(String name, int age) {}

// Equivalent traditional class (simplified):
// private final String name;
// private final int age;
// public Person(String name, int age) { this.name = name; this.age = age; }
// public String name() { return name; }   // accessors, NOT getName()
// public int age()     { return age; }
// public boolean equals(Object o) { ... }
// public int hashCode() { ... }
// public String toString() { return "Person[name=" + name + ", age=" + age + "]"; }

Person p = new Person("Alice", 30);
System.out.println(p.name()); // Alice    (accessor, not getName)
System.out.println(p);        // Person[name=Alice, age=30]

// Compact canonical constructor — validate without repeating assignments
public record Range(int lo, int hi) {
    Range { // compact form — no parameter list
        if (lo > hi) throw new IllegalArgumentException(lo + " > " + hi);
        // assignments lo = lo; hi = hi; happen automatically at end
    }
}

// Records CAN have:
// - static fields and methods
// - instance methods (non-component)
// - custom constructors that delegate to canonical
// - implement interfaces

// Records CANNOT:
// - extend any class (implicitly extend java.lang.Record)
// - declare instance fields beyond components
// - be abstract

Records work seamlessly with Java 21 features: they are the natural carrier type for record patterns, they compose well with sealed interfaces for algebraic data types, and their auto-generated equals()/hashCode() make them safe as map keys and set elements.

What naming convention does a record use for its generated accessor methods?
What does the compact canonical constructor in a record automatically do at its end?

8. What are Text Blocks in Java and how do you use them?

Text Blocks (JEP 378, finalised in Java 15) provide a multi-line string literal that preserves formatting without escape sequences. They are delimited by """ and the content starts on the line after the opening delimiter.

// Traditional string — escape-heavy
String json = "{\n" +
    "  \"name\": \"Alice\",\n" +
    "  \"age\": 30\n" +
"}";

// Text block — same result, readable
String json = """
        {
          "name": "Alice",
          "age": 30
        }
        """;

// Indentation is stripped automatically:
// Java strips the minimum leading whitespace (re-indentation)
// based on the position of the closing triple quote.

// SQL example
String sql = """
        SELECT id, name
        FROM   customers
        WHERE  active = true
        ORDER  BY name
        """;

// Escape sequences still work inside text blocks
String noTrailingNewline = """
        Hello\
        World""";
// Backslash at end of line joins the next line — no newline inserted

// \s — a space that prevents trailing whitespace stripping
String aligned = """
        one  \s
        two  \s
        """;

Re-indentation rules: Java finds the minimum indentation of all non-blank content lines plus the closing delimiter line, and strips that many leading spaces from every line. Placing the closing """ at the start of the line preserves all indentation of the content; indenting it further strips more.

Where does the content of a text block start?
What does the \s escape sequence do inside a text block?
9. What is Structured Concurrency in Java 21 and what problem does it solve?

Structured Concurrency (JEP 453, second preview in Java 21) is an API — built on StructuredTaskScope — that treats a group of concurrent subtasks as a single unit of work whose lifetime is scoped to the code block that created them. It solves the problem of unreliable cancellation, lost exceptions, and dangling threads that plague ad-hoc ExecutorService usage.

// Classic problem: if userTask or orderTask throws, the other leaks
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

    // Fork subtasks — each runs on its own virtual thread
    Subtask  userTask  = scope.fork(() -> fetchUser(userId));
    Subtask orderTask = scope.fork(() -> fetchOrder(orderId));

    scope.join()           // wait for both
         .throwIfFailed(); // propagate first failure as exception

    // Both succeeded — safe to access results
    return new Response(userTask.get(), orderTask.get());
} // scope.close() cancels any still-running subtasks automatically

// ShutdownOnSuccess — return the first result that succeeds
try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) {
    scope.fork(() -> callPrimaryService());
    scope.fork(() -> callFallbackService());
    scope.join();
    return scope.result(); // whichever finished first
}

The key guarantee is the structure invariant: a subtask never outlives the scope that forked it. When control leaves the try block (normally or via exception), StructuredTaskScope.close() cancels all still-running subtasks and waits for them to finish — preventing thread leaks entirely.

Built-in scope policies
PolicyShuts down scope when...Use case
ShutdownOnFailureAny subtask failsAll subtasks must succeed (fan-out + join)
ShutdownOnSuccessAny subtask succeedsFirst result wins (hedged request)
What invariant does StructuredTaskScope guarantee about subtask lifetimes?
What does scope.throwIfFailed() do in ShutdownOnFailure?
10. What are Scoped Values in Java 21 and how do they differ from ThreadLocal?

Scoped Values (JEP 446, first preview in Java 21) provide an immutable, bounded alternative to ThreadLocal designed specifically for virtual threads. A ScopedValue holds a value for the duration of a bounded scope (a ScopedValue.where(...).run(...) block) and is automatically removed when the scope exits — no manual remove() required.

ThreadLocal vs ScopedValue
AspectThreadLocalScopedValue
MutabilityMutable — set() anytimeImmutable — set once per scope, read-only inside
LifetimeLives until remove() or thread deathLives only within the where().run() scope
InheritanceMust opt-in (InheritableThreadLocal)Inherited automatically by child scopes
Memory with VTsMemory leak risk at millions of VTsBounded — cleaned up at scope exit
Thread safetyPer-thread copy, OKImmutable, inherently safe
// Declare a scoped value (typically static final)
private static final ScopedValue CURRENT_USER =
        ScopedValue.newInstance();

// Bind and run — value is available inside the lambda
ScopedValue.where(CURRENT_USER, currentUser)
           .run(() -> {
               processRequest(); // can read CURRENT_USER anywhere in call tree
           });

// Inside processRequest or any method it calls:
void processRequest() {
    User user = CURRENT_USER.get();    // always present in this scope
    // ... use user ...
}

// Nested rebinding — inner scope shadows outer
ScopedValue.where(CURRENT_USER, adminUser)
           .run(() -> {
               // CURRENT_USER is adminUser here
               ScopedValue.where(CURRENT_USER, guestUser)
                          .run(() -> {
                              // CURRENT_USER is guestUser here
                          });
               // CURRENT_USER is adminUser again
           });

Scoped Values are particularly useful for passing context (request IDs, current user, database transaction) down through a call stack without threading it through every method parameter — the same use case as ThreadLocal, but safe and efficient at millions of virtual threads.

What happens to a ScopedValue's binding when the where().run() scope exits?
How does a ScopedValue differ from ThreadLocal in terms of mutability?
11. What is Generational ZGC in Java 21 and why does it improve upon the original ZGC?

JEP 439 finalises Generational ZGC, which extends the existing low-latency Z Garbage Collector with separate young and old generations. Classic ZGC (introduced in Java 11) collects all live objects on every GC cycle, which is thorough but wastes CPU on long-lived objects that will not be collected.

Non-Generational ZGC vs Generational ZGC
AspectClassic ZGCGenerational ZGC
GenerationsSingle generation — all objectsYoung + Old (weak generational hypothesis)
GC frequencyFull heap every cycleYoung GC often, Old GC rarely
CPU overheadHigher (scans all live data)Lower (mostly scans short-lived young gen)
Allocation rateCan struggle at very high ratesHandles higher allocation rates
Max pause<1 ms (both)<1 ms (both)
Enable flagDefault in Java 21ZGC is generational by default in Java 21
// Generational ZGC is the default ZGC mode in Java 21
// Enable ZGC (generational by default):
// java -XX:+UseZGC MyApp

// To use non-generational ZGC (legacy mode):
// java -XX:+UseZGC -XX:-ZGenerational MyApp

// Key tuning flags:
// -Xms / -Xmx  — min and max heap
// -XX:SoftMaxHeapSize=N  — soft limit ZGC tries to stay under
// -XX:ZUncommitDelay=N   — delay before returning memory to OS

// Monitoring GC:
// -Xlog:gc*:file=gc.log
// jcmd  GC.run — trigger a GC cycle
// jstat -gcutil  1s — watch GC stats every second

The weak generational hypothesis states that most objects die young. By collecting the young generation far more frequently than the old generation, Generational ZGC spends less CPU per GC cycle and handles higher allocation rates — particularly beneficial for microservices and high-throughput applications running on Java 21 virtual threads.

What is the key benefit Generational ZGC adds over classic (non-generational) ZGC?
In Java 21, what flag enables ZGC (which is generational by default)?
12. What important String methods were added from Java 11 through Java 21?

The String class received significant API improvements across several Java releases. Knowing the version they arrived in is common interview territory.

Key String Methods (Java 11–21)
MethodSincePurpose
isBlank()11Returns true if string is empty or contains only whitespace
strip() / stripLeading() / stripTrailing()11Unicode-aware whitespace stripping (vs trim() which uses ASCII <= 32)
lines()11Returns a Stream<String> of lines split by line terminators
repeat(int n)11Returns the string repeated n times
indent(int n)12Adds/removes leading whitespace per line
formatted(Object... args)15Instance version of String.format()
stripIndent()15Removes incidental whitespace (used by text blocks)
translateEscapes()15Interprets \n \t etc. as escape sequences
chars() / codePoints()9Returns IntStream of char/codepoint values
// isBlank and strip — Java 11
"  \t  ".isBlank();      // true
"  hello  ".strip();     // "hello" (Unicode-aware)
"  hello  ".trim();      // "hello" (ASCII only — legacy)

// lines() — Java 11
"a\nb\nc".lines().count();  // 3 (Stream)

// repeat() — Java 11
"ab".repeat(3);          // "ababab"

// formatted() — Java 15
"Hello %s, you are %d".formatted("Alice", 30);
// "Hello Alice, you are 30"

// indent() — Java 12
"hello\nworld".indent(4);
// "    hello\n    world\n"

// chars() — Java 9
"Java".chars().forEach(c -> System.out.print((char) c + " "));
// J a v a
Which String method added in Java 11 is Unicode-aware and should be preferred over trim()?
What does 'hello\nworld'.lines() return?
13. What is 'var' in Java and what are its limitations?

var (JEP 286, Java 10) introduces local variable type inference. The compiler infers the type from the initialiser on the right-hand side; you do not need to write the type explicitly. It is not a dynamic type — the variable is still strongly typed at compile time; var is just syntactic sugar.

// Without var
Map> scores = new HashMap>();
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));

// With var — type inferred from the right-hand side
var scores = new HashMap>();  // Map>
var reader = new BufferedReader(new FileReader("file.txt")); // BufferedReader

// Works in for-loops
for (var entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// Works with try-with-resources
try (var in = new FileInputStream("data.bin")) { ... }

// CANNOT be used:
// var x;                   // no initialiser — type unknown
// var x = null;            // null has no type
// private var name;        // instance/static fields
// public var foo() { }     // return types
// void bar(var x) { }      // method parameters
// var x = { 1, 2, 3 };     // array initialiser without explicit type

In Java 11, var was extended to lambda parameters so you can add annotations: (var x, var y) -> x + y is equivalent to (x, y) -> x + y but allows (@NotNull var x, @NotNull var y) -> x + y.

What does 'var' in Java guarantee about the type of the variable?
Which of the following is a valid use of 'var'?
14. What are switch expressions in Java and how do they differ from switch statements?

Switch expressions (JEP 361, finalised in Java 14) make switch a value-producing expression rather than only a control-flow statement. They use the arrow (->) syntax and require exhaustiveness.

Switch Statement vs Switch Expression
AspectSwitch StatementSwitch Expression
Produces a valueNoYes — must be exhaustive
Syntaxcase X: ... break;case X -> result; or case X -> { ... yield result; }
Fall-throughPossible (bug-prone)Not possible with arrows
ExhaustivenessNot checkedCompiler-enforced (default required unless exhaustive)
Multiple labelsNocase A, B, C -> ... (comma-separated)
// Switch STATEMENT (old style) — fall-through risk
int numLetters;
switch (day) {
    case MONDAY: case FRIDAY: case SUNDAY:
        numLetters = 6; break;
    case TUESDAY:
        numLetters = 7; break;
    default:
        numLetters = 8; break;
}

// Switch EXPRESSION (Java 14+) — exhaustive, no fall-through
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY               -> 7;
    case THURSDAY, SATURDAY    -> 8;
    case WEDNESDAY             -> 9;
}; // no default — DayOfWeek is exhaustive

// Multi-line arm with yield
int result = switch (x) {
    case 1 -> 10;
    case 2 -> {
        int y = x * x;
        System.out.println("Computing...");
        yield y + 5; // yield produces the value from a block arm
    }
    default -> 0;
};
What keyword produces a value from a block (multi-statement) arm in a switch expression?
What happens if a switch expression does not cover all possible values?
15. How does pattern matching for instanceof work in Java 16+?

JEP 394 (finalised in Java 16) extends the instanceof operator with a type pattern that both tests the type and binds a typed local variable in a single expression, eliminating the explicit cast that always followed a traditional instanceof check.

// Traditional — test + cast (redundant, error-prone)
if (obj instanceof String) {
    String s = (String) obj;         // redundant cast
    System.out.println(s.length());
}

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

// The pattern variable is in scope only where the test is true
if (!(obj instanceof String s)) {
    return; // s is NOT in scope here (obj didn't match)
}
// s IS in scope here — guard pattern
System.out.println(s.toUpperCase());

// Combine with &&
if (obj instanceof String s && s.length() > 3) {
    System.out.println(s.trim()); // s in scope because && is short-circuit
}

// In equals() — cleaner implementation
@Override
public boolean equals(Object o) {
    return o instanceof Point p
           && this.x == p.x
           && this.y == p.y;
}

The scoping rule is precise: the pattern variable is in scope in the part of the expression where the type test is guaranteed to have been true. It is not in scope in the else branch or on the false side of ||. This is called flow scoping and the compiler enforces it statically.

In 'if (obj instanceof String s && s.length() > 3)', why is 's' accessible in the right operand of &&?
What is the main improvement of pattern matching instanceof over the traditional instanceof + cast?
16. What Stream API improvements were introduced in Java 9 through Java 21?

The Stream API has been incrementally enhanced since Java 9. Knowing which methods are available and when they arrived is frequently tested.

Stream API Additions (Java 9–21)
MethodSinceDescription
takeWhile(Predicate)9Takes elements while predicate is true, then stops
dropWhile(Predicate)9Drops elements while predicate is true, then takes rest
iterate(seed, hasNext, next)9Finite iterate — 3-arg form with a terminating predicate
ofNullable(T)9Stream of 0 or 1 elements (empty if null)
Stream.of(T...)9Already existed; ofNullable is the new addition
Collectors.teeing(d1, d2, merger)12Collect into two downstreams, merge results
toList()16Unmodifiable List directly from stream (vs Collectors.toList())
mapMulti(BiConsumer)16Flexible flat-map alternative
Stream.gather(Gatherer)22 previewCustom intermediate operations (post-21)
// takeWhile / dropWhile — Java 9
List.of(1, 2, 3, 4, 5, 1, 2)
    .stream()
    .takeWhile(n -> n < 4)  // [1, 2, 3] — stops at first failure
    .toList();

// 3-argument iterate — finite sequence
Stream.iterate(1, n -> n <= 100, n -> n * 2)  // 1 2 4 8 ... 64
      .toList();

// ofNullable — avoids NPE in flatMap chains
Stream.ofNullable(maybeNull)  // empty stream if null, else stream of one
      .map(String::toUpperCase)
      .findFirst();

// Collectors.teeing — Java 12
record Stats(long count, double sum) {}
Stats s = numbers.stream().collect(
    Collectors.teeing(
        Collectors.counting(),
        Collectors.summingDouble(Double::doubleValue),
        Stats::new
    ));

// toList() — Java 16
List names = people.stream().map(Person::name).toList();
// returns an UNMODIFIABLE list — names.add("x") throws UnsupportedOperationException
What does Stream.toList() (added in Java 16) return, compared to Collectors.toList()?
What does Stream.ofNullable(value) produce when value is null?
17. How has Optional been improved and how should it be used correctly?

Optional<T> (Java 8) is a container for a value that may or may not be present. It is designed to be a return type that makes the absence of a value explicit in the API contract, not a replacement for null in all contexts.

Key Optional methods
MethodSincePurpose
of(T) / ofNullable(T) / empty()8Factory methods
isPresent() / isEmpty()8 / 11isEmpty() added Java 11
get() / orElse(T) / orElseGet(Supplier)8Extract value (orElseGet lazier)
orElseThrow(Supplier)8Throw custom exception if empty
map() / flatMap() / filter()8Transform the value
ifPresent(Consumer) / ifPresentOrElse()8 / 9ifPresentOrElse added Java 9
or(Supplier<Optional>)9Return this if present, else the supplied Optional
stream()9Returns Stream of 0 or 1 elements
// Correct use — as a return type
Optional findById(long id) {
    return Optional.ofNullable(repository.get(id));
}

// Chaining without get()
findById(42)
    .filter(u -> u.isActive())
    .map(User::email)
    .ifPresent(email -> sendNotification(email));

// or() — Java 9: fallback to another Optional
Optional user = findById(42)
    .or(() -> findByEmail("alice@example.com"));

// ifPresentOrElse — Java 9
findById(42).ifPresentOrElse(
    user -> System.out.println("Found: " + user),
    ()   -> System.out.println("Not found")
);

// stream() — Java 9: use Optional in flatMap on a stream of Optionals
List> opts = List.of(Optional.of("a"), Optional.empty(), Optional.of("b"));
List present = opts.stream()
    .flatMap(Optional::stream)  // ["a", "b"]
    .toList();

// ANTI-PATTERNS:
// opt.isPresent() ? opt.get() : default -- use orElse(default)
// Optional> -- use flatMap
// Optional as a field -- use null or an explicit state enum
// Optional as a parameter -- use method overloading
Which Optional method was added in Java 9 to provide a fallback Optional when the original is empty?
What is the recommended way to handle Optional in a flatMap over a stream of Optional values?
18. How does CompletableFuture work in Java and how does it relate to virtual threads?

CompletableFuture<T> (Java 8) provides composable asynchronous computation pipelines. It lets you chain transformations (thenApply), combinations (thenCombine), and error handlers (exceptionally) without blocking threads.

// Basic async computation
CompletableFuture future = CompletableFuture
    .supplyAsync(() -> fetchUserName(userId))     // runs in ForkJoinPool
    .thenApply(String::toUpperCase)               // transform result
    .thenApply(name -> "Hello, " + name);

System.out.println(future.join()); // blocks calling thread until done

// Combining two independent futures
CompletableFuture   userFuture  = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture  orderFuture = CompletableFuture.supplyAsync(() -> fetchOrder(id));

CompletableFuture result = userFuture
    .thenCombine(orderFuture,
        (user, order) -> user.name() + " ordered " + order.item());

// Error handling
future.exceptionally(ex -> "Default value on error: " + ex.getMessage())
      .thenAccept(System.out::println);

// Run all, collect results
List> futures = ids.stream()
    .map(id -> CompletableFuture.supplyAsync(() -> fetch(id)))
    .toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
    .thenApply(v -> futures.stream().map(CompletableFuture::join).toList())
    .join();

// With virtual threads (Java 21): use simple blocking code instead
// StructuredTaskScope is the recommended replacement for CompletableFuture
// when running on virtual threads — simpler and handles cancellation

With Java 21 virtual threads, the async/reactive style of CompletableFuture is less necessary for I/O-bound work — you can write simple blocking code on virtual threads and achieve the same throughput. However, CompletableFuture remains useful for CPU-bound parallel pipelines and for code that must integrate with existing async APIs.

Which CompletableFuture method combines the results of two independent futures when both complete?
What does CompletableFuture.join() do?
19. What does the 'volatile' keyword guarantee in Java's memory model?

The volatile keyword provides two guarantees in the Java Memory Model (JMM): visibility and ordering.

  1. Visibility: A write to a volatile variable is immediately flushed to main memory, and a read of a volatile variable always reads from main memory — not from a CPU cache. This prevents a thread from seeing a stale cached value written by another thread.
  2. Ordering: volatile establishes a happens-before relationship. All writes performed before a volatile write are visible to any thread that subsequently reads that volatile variable.
// Without volatile — Thread B may never see the update from Thread A
class BadFlag {
    boolean running = true; // may be cached in Thread A's register
    void stop() { running = false; }
    void work() { while (running) { /* spin */ } }
}

// With volatile — Thread B sees the write immediately
class GoodFlag {
    volatile boolean running = true;
    void stop() { running = false; }  // flushed to main memory
    void work() { while (running) { /* spin */ } } // reads main memory
}

// volatile is NOT atomic for compound operations
volatile int counter = 0;
counter++;  // NOT atomic: read + increment + write — use AtomicInteger

// Double-checked locking with volatile (Java 5+ safe)
class Singleton {
    private static volatile Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {                  // first check — no lock
            synchronized (Singleton.class) {
                if (instance == null) {          // second check — locked
                    instance = new Singleton();  // volatile ensures ordering
                }
            }
        }
        return instance;
    }
}

volatile is weaker than synchronized (no mutual exclusion) but cheaper. Use it for single-variable state flags and simple publish/read patterns. For compound operations (increment, compare-and-swap), use java.util.concurrent.atomic.AtomicInteger and friends.

What does 'volatile' guarantee that plain field access does NOT?
Why is 'volatile int counter; counter++;' NOT thread-safe?
20. What are the immutable collection factory methods introduced in Java 9?

Java 9 (JEP 269) introduced convenient static factory methods on List, Set, and Map for creating small, immutable collections without the verbosity of Arrays.asList() or Collections.unmodifiableList().

// Java 9+ — concise and immutable
List       list = List.of("a", "b", "c");           // immutable
Set       set  = Set.of(1, 2, 3);                    // immutable, no dups
Map map = Map.of("one", 1, "two", 2);        // up to 10 pairs
Map bigMap = Map.ofEntries(                    // more than 10 pairs
    Map.entry("a", 1),
    Map.entry("b", 2)
);

// Copying (Java 10+)
List copy = List.copyOf(mutableList);  // immutable copy
Set  sCopy = Set.copyOf(existingSet);

// Java 9 vs Arrays.asList
// Arrays.asList: fixed-size but MUTABLE (set() works, add() throws)
// List.of: IMMUTABLE (set(), add(), remove() all throw)

list.add("d");      // UnsupportedOperationException
list.set(0, "z");  // UnsupportedOperationException

// Null not permitted in List.of / Set.of / Map.of
List.of("a", null, "c"); // NullPointerException at creation

// Set.of does not allow duplicates
Set.of(1, 2, 2); // IllegalArgumentException

These factory-created collections are optimised for memory: small sets and maps use compact array-based implementations rather than hash tables when they have few elements. The iteration order of Set.of and Map.of is intentionally unspecified and may vary between JVM runs.

What is the key difference between Arrays.asList() and List.of() in Java 9+?
What happens when you pass a duplicate element to Set.of()?
21. What are functional interfaces in Java and how are lambdas related to them?

A functional interface is an interface with exactly one abstract method (SAM — Single Abstract Method). The @FunctionalInterface annotation is optional but recommended — it causes the compiler to enforce the single-abstract-method rule. Lambdas and method references provide implementations for functional interfaces without requiring an anonymous class.

Key built-in functional interfaces (java.util.function)
InterfaceSignaturePurpose
FunctionR apply(T t)Transform T to R
Consumervoid accept(T t)Consume T, no return
SupplierT get()Produce T, no input
Predicateboolean test(T t)Test T — returns boolean
BiFunctionR apply(T t, U u)Two inputs, one output
UnaryOperatorT apply(T t)Function where T-in = T-out
BinaryOperatorT apply(T t1, T t2)Two T inputs, one T output
Runnablevoid run()No input, no output
CallableV call()No input, returns V, throws Exception
// Lambda syntax
Function length = s -> s.length();
Function length2 = String::length; // method reference

// Composing functions
Function toUpper = String::toUpperCase;
Function len    = String::length;
Function upperLen = toUpper.andThen(len);
System.out.println(upperLen.apply("hello")); // 5

// Predicate composition
Predicate notBlank  = Predicate.not(String::isBlank);
Predicate shortWord = s -> s.length() < 5;
Predicate both      = notBlank.and(shortWord);

// Custom functional interface
@FunctionalInterface
interface ThrowingSupplier {
    T get() throws Exception; // can declare checked exceptions
}

// Method reference types
String::toUpperCase    // instance method — unbound
System.out::println    // instance method — bound (specific object)
String::new            // constructor reference
Integer::parseInt      // static method reference
What does @FunctionalInterface enforce?
What is 'String::toUpperCase' when used as a lambda target for Function?
22. What are the most important Collectors and how do you write custom ones?

Collectors define how a terminal collect() operation assembles the stream elements. java.util.stream.Collectors provides ~40 factory methods; the most frequently used in interviews are:

import static java.util.stream.Collectors.*;

// Grouping
Map> byCity =
    people.stream().collect(groupingBy(Person::city));

// Grouping with downstream collector
Map countByCity =
    people.stream().collect(groupingBy(Person::city, counting()));

Map avgAgeByCity =
    people.stream().collect(groupingBy(Person::city, averagingInt(Person::age)));

// Partitioning — splits into true/false map
Map> adultMap =
    people.stream().collect(partitioningBy(p -> p.age() >= 18));

// Joining
String csv = names.stream().collect(joining(", ", "[", "]"));
// e.g. "[Alice, Bob, Carol]"

// toMap
Map byId = people.stream()
    .collect(toMap(Person::id, p -> p,
                   (existing, duplicate) -> existing)); // merge fn for duplicates

// teeing (Java 12) — two simultaneous collectors
record MinMax(int min, int max) {}
MinMax mm = IntStream.of(3, 1, 4, 1, 5, 9)
    .boxed()
    .collect(teeing(
        minBy(Comparator.naturalOrder()),
        maxBy(Comparator.naturalOrder()),
        (min, max) -> new MinMax(min.get(), max.get())
    ));

// toUnmodifiableList/Set/Map — Java 10
List immutable = stream.collect(toUnmodifiableList());
What does Collectors.partitioningBy(predicate) return?
What is the purpose of the third argument (merge function) in Collectors.toMap()?
23. What are Unnamed Classes and Instance Main Methods in Java 21 (Preview)?

JEP 445 (preview in Java 21) removes much of the ceremony required to write a simple Java program. It targets learning, scripting, and small utilities — not production application structure.

// Traditional Hello World — requires class declaration, static, String[] args
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

// Java 21 Preview — unnamed class, instance main, no args
void main() {
    System.out.println("Hello, World!");
}

// The file is still named Main.java (or any name)
// Compile:  javac --enable-preview --release 21 Main.java
// Run:      java  --enable-preview Main

// Top-level methods and fields are allowed
String greeting = "Hello";

void main() {
    System.out.println(greeting + ", World!");
    greet("Alice");
}

void greet(String name) {
    System.out.println(greeting + ", " + name + "!");
}

// Main method signature flexibility:
// void main()              -- new (no args, instance)
// static void main()       -- no args, static
// static void main(String[]) -- traditional

The launch protocol in JEP 445 checks for a valid main method in this order: first static void main(String[]) (traditional), then static void main(), then void main(String[]), then void main(). This ensures backward compatibility — existing programs are unaffected.

What Java 21 preview JEP allows writing 'void main() { }' without a class declaration?
Why does JEP 445 not break existing programs that use 'public static void main(String[] args)'?
24. What improvements were made to NullPointerException messages in Java 14?

JEP 358 (Java 14) enabled helpful NullPointerException messages that precisely identify which variable or expression was null, rather than just the line number and a generic message. This was one of the most immediately useful quality-of-life improvements in recent Java history.

// Before Java 14: vague NPE
// Exception in thread "main" java.lang.NullPointerException
//     at com.example.Main.main(Main.java:12)

// Java 14+: helpful NPE message
// NullPointerException: Cannot invoke "String.length()" because
// "" is null

// Detailed example
a.b.c.d = 42;
// Cannot assign field "d" because "a.b.c" is null

a[5].b = 42;
// Cannot assign field "b" because "a[5]" is null

a.b = foo();
// Cannot invoke "Main.foo()" because "a" is null

// Enabled by default since Java 15
// (In Java 14 it was enabled with -XX:+ShowCodeDetailsInExceptionMessages)

// Complements with Objects.requireNonNull for fail-fast validation
String name = Objects.requireNonNull(input, "input must not be null");

// Objects.requireNonNullElse — Java 9
String display = Objects.requireNonNullElse(name, "Unknown");

// Objects.requireNonNullElseGet — lazy supplier
String display2 = Objects.requireNonNullElseGet(name, () -> computeDefault());

The JVM analyses the bytecode at the point of the NPE and constructs a sentence describing which specific reference in a complex expression was null. This works for field access, method invocations, array accesses, and unboxing operations. It dramatically reduces debugging time for nested null-checks.

Since which Java version are helpful NullPointerException messages enabled by default?
What does Objects.requireNonNullElseGet(value, supplier) return when 'value' is null?
25. How does type erasure affect instanceof checks with generics in Java?

Type erasure means generic type parameters are removed at compile time, leaving only the raw type at runtime. This has direct implications for instanceof checks.

// Cannot check parameterised generic types at runtime
List<String> strings = List.of("a", "b");
if (strings instanceof List<String>) { }  // compile error - cannot check List <String>
if (strings instanceof List<?>)     { }  // OK - wildcard is fine
if (strings instanceof List)        { }  // OK - raw type is fine (with warning)

// Generic type parameter in pattern — unchecked warning
// The JVM can only check 'instanceof List', not 'instanceof List'

// Java 21 allows wildcard in pattern
static void process(Object obj) {
    if (obj instanceof List<?> list) {
      
        list.forEach(System.out::println); // OK — each element is Object
    }
}

// Workaround: check elements individually
static boolean isListOfStrings(Object obj) {
    if (obj instanceof List<?> list) {
        return list.stream().allMatch(e -> e instanceof String);
    }
    return false;
}

// getClass() also suffers from erasure
List<String> ls = new ArrayList<>();
List<Integer> li = new ArrayList<>();
System.out.println(ls.getClass() == li.getClass()); // true — both ArrayList

Type erasure is the root cause. Java generics are a compile-time mechanism; at runtime both List<String> and List<Integer> are simply List. Pattern matching cannot change this — instanceof List<String> would require the JVM to inspect every element, which is why it is disallowed. Project Valhalla (future Java) aims to address this with reified generics via value types.

Why can't you write 'obj instanceof List<String>' as an instanceof check in Java?
Which form of generic instanceof is allowed in Java 21?
26. What are the differences between 'synchronized' and ReentrantLock in Java?

synchronized is Java's built-in intrinsic lock mechanism. ReentrantLock (in java.util.concurrent.locks) provides the same mutual-exclusion guarantee but with more capabilities at the cost of more verbose code.

synchronized vs ReentrantLock
FeaturesynchronizedReentrantLock
Lock acquisitionImplicit — enters on block entryExplicit — must call lock()
UnlockAutomatic on block exitExplicit — must call unlock() in finally
Try to acquireNot possibletryLock() / tryLock(timeout, unit)
Interruptible lockNot possiblelockInterruptibly()
FairnessNon-fair (JVM decides)Can be fair (new ReentrantLock(true))
Condition variablesOne per monitor (wait/notify)Multiple Condition objects per lock
Virtual thread pinningPins virtual thread to carrierDoes NOT pin — preferred for VTs
// synchronized — simple cases
synchronized (this) {
    // critical section
}

// ReentrantLock — more control, required for virtual-thread-safe code
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull  = lock.newCondition();

void produce(T item) throws InterruptedException {
    lock.lock();
    try {
        while (queue.size() == capacity) notFull.await();
        queue.add(item);
        notEmpty.signal();
    } finally {
        lock.unlock(); // always in finally to prevent lock leaks
    }
}

// tryLock — non-blocking attempt
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
    try { /* critical section */ }
    finally { lock.unlock(); }
} else {
    System.out.println("Could not acquire lock in time");
}

The critical Java 21 consideration: synchronized pins a virtual thread to its carrier OS thread for the duration of the synchronized block, preventing the scheduler from parking the virtual thread and assigning the carrier to another task. ReentrantLock does not pin — the virtual thread can still be parked inside a lock. For high-concurrency virtual-thread applications, replacing hot synchronized blocks with ReentrantLock is a meaningful performance improvement.

Why should ReentrantLock be preferred over synchronized in code that runs on virtual threads?
What does ReentrantLock.tryLock(500, TimeUnit.MILLISECONDS) return if the lock cannot be acquired?
27. What are the key classes in the java.time package and when do you use each?

The java.time package (Java 8, JSR 310) replaces the problematic java.util.Date and java.util.Calendar classes. All classes are immutable and thread-safe by design.

Core java.time classes
ClassRepresentsExample
LocalDateDate without time or timezone2024-03-15
LocalTimeTime without date or timezone14:30:00.000
LocalDateTimeDate + time, no timezone2024-03-15T14:30:00
ZonedDateTimeDate + time + timezone2024-03-15T14:30:00+05:30[Asia/Kolkata]
OffsetDateTimeDate + time + fixed UTC offset2024-03-15T14:30:00+05:30
InstantMoment on UTC timeline (nanosecond precision)2024-03-15T09:00:00Z
DurationAmount of time in hours/minutes/secondsPT2H30M
PeriodAmount of time in years/months/daysP1Y2M3D
ZoneIdA time zone ID"Europe/London"
// LocalDate operations
LocalDate today     = LocalDate.now();
LocalDate birthday  = LocalDate.of(1990, Month.JUNE, 15);
LocalDate nextMonth = today.plusMonths(1);
long days = ChronoUnit.DAYS.between(birthday, today);

// ZonedDateTime — essential for cross-timezone work
ZonedDateTime nyNow = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime tokyoNow = nyNow.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));

// Instant — machine time, for logging and persistence
Instant start = Instant.now();
// ... do work ...
Duration elapsed = Duration.between(start, Instant.now());
System.out.println("Elapsed: " + elapsed.toMillis() + "ms");

// Formatting / parsing
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
String   formatted = today.atTime(14, 30).format(fmt);
LocalDateTime parsed  = LocalDateTime.parse("25/12/2024 09:00", fmt);

// Converting legacy Date to Instant
Instant i  = new Date().toInstant();
LocalDate ld = i.atZone(ZoneId.systemDefault()).toLocalDate();
Which java.time class represents a precise moment in time on the UTC timeline without timezone offset?
What is the difference between Duration and Period in java.time?
28. What garbage collectors are available in Java 21 and how do you choose between them?

Java 21 ships four major garbage collectors, each optimised for different trade-off points on the throughput-vs-latency spectrum.

GC Comparison in Java 21
GCFlagPause targetThroughputBest for
Serial GC-XX:+UseSerialGCNot optimised — STWLowSingle-core, small heap (<100 MB)
Parallel GC-XX:+UseParallelGCSeconds (STW)HighestBatch, throughput-first, CPU-heavy
G1 GC (default)-XX:+UseG1GC<200 ms targetHighGeneral-purpose, heaps 1–32 GB
ZGC (Generational)-XX:+UseZGC<1 msGoodLow-latency, very large heaps (>32 GB)
Shenandoah-XX:+UseShenandoahGC<10 msGoodLow-latency alternative to ZGC
// G1 (default since Java 9) — good general purpose starting point
// java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MyApp

// ZGC — for large heaps needing sub-millisecond pauses
// java -Xms16g -Xmx32g -XX:+UseZGC MyApp
// In Java 21, ZGC is generational by default

// Parallel GC — batch jobs, maximum throughput
// java -Xms8g -Xmx8g -XX:+UseParallelGC -XX:ParallelGCThreads=8 MyApp

// GC logging (useful in all environments)
// -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20m

// Key GC metrics to monitor:
// - GC pause duration and frequency
// - Allocation rate (Eden fill rate)
// - Promotion rate (objects escaping young gen)
// - Heap occupancy after GC
// - Metaspace usage

For Java 21 virtual-thread applications: ZGC (Generational) or G1 are the best choices. Virtual threads create many short-lived objects (parked continuations), and G1/ZGC handle high young-generation allocation rates well. Avoid Parallel GC for latency-sensitive applications — its stop-the-world pauses can be seconds long on large heaps.

What is the default GC in Java 21 for general-purpose server applications?
Which JVM flag enables ZGC in Java 21?
29. What is the Java Platform Module System (JPMS) and when should you use it?

JPMS (Project Jigsaw, JEP 261, Java 9) introduces the concept of modules — named, versioned groups of packages with explicit dependency declarations and access control. It solves the classpath hell problem (no visibility into what JARs exist or what they expose) and improves security by allowing packages to be completely non-accessible to other modules.

// module-info.java — at the root of the source tree
module com.example.myapp {
    // Explicit dependencies
    requires java.sql;
    requires com.example.utils;
    requires transitive com.example.api; // transitive: callers get it too
    requires static  com.example.opt;    // optional at runtime

    // Explicit exports — only these packages are readable by other modules
    exports com.example.myapp.api;
    exports com.example.myapp.model to com.example.web; // qualified export

    // Opens for reflection (e.g., for Spring, Hibernate, Jackson)
    opens com.example.myapp.model;              // to all modules
    opens com.example.myapp.impl to spring.core; // to Spring only

    // Service provider / consumer
    uses   com.example.api.Plugin;                // consumes
    provides com.example.api.Plugin               // provides
        with com.example.myapp.impl.MyPlugin;
}

// Compile
// javac --module-source-path src -d out -m com.example.myapp

// Run
// java --module-path out -m com.example.myapp/com.example.myapp.Main

// Reflect without opens in module — use --add-opens at JVM startup
// java --add-opens java.base/java.lang=com.example.myapp ...

Practical reality: most enterprise applications still use the unnamed module (plain classpath) because migrating large codebases to named modules is complex, and many libraries do not provide module-info.java yet. However, libraries and frameworks are increasingly module-aware, and applications targeting the JVM directly (command-line tools, serverless jars) benefit greatly from JPMS for smaller deployable images via jlink.

What does 'exports com.example.api to com.example.web' do in module-info.java?
What is the difference between 'requires' and 'requires transitive' in a module declaration?
30. What are String Templates in Java 21 (Preview) and how do they improve string interpolation?

JEP 430 introduces String Templates as a preview feature in Java 21. They provide type-safe string interpolation via template processors — a safer alternative to string concatenation and String.format() that prevents injection vulnerabilities by separating the template structure from the values.

// Traditional approaches — error-prone
String name = "Alice";
int age  = 30;
String s1 = "Hello " + name + ", you are " + age + " years old.";
String s2 = String.format("Hello %s, you are %d years old.", name, age);
String s3 = "Hello %s, you are %d years old.".formatted(name, age);

// String Template (Java 21 Preview) — requires --enable-preview
String s4 = STR."Hello \{name}, you are \{age} years old.";
// Embedded expressions are evaluated and inserted

// Expressions can be any Java expression
String s5 = STR."The sum of 3+4 is \{3+4}.";
String s6 = STR."First name: \{name.split(" ")[0]}";

// Multi-line template with STR
String json = STR."""
        {
          "name":  "\{name}",
          "age":   \{age}
        }
        """;

// FMT processor — formatted output like printf
String formatted = FMT."Total: %10.2f\{total} USD";

// Custom template processor — safe SQL (not yet in preview)
// PreparedStatement ps = SQL."SELECT * FROM users WHERE id = \{userId}";
// The SQL processor parameterises the value — injection-safe

The key safety property: unlike simple string interpolation in other languages, String Templates allow custom processors (STR, FMT, or user-defined) to control how embedded values are combined with the template. A SQL processor can automatically use prepared statement parameters; a JSON processor can escape values correctly — the template structure and values are never naively concatenated.

What is the name of the standard String Template processor for basic string interpolation in Java 21?
What safety advantage do String Templates provide over 'SELECT * FROM users WHERE id=' + userId?
31. What are the contracts for equals(), hashCode(), and Comparable in Java?

These three methods have formal contracts that must be maintained for code to work correctly with collections, sorting, and data structures.

// equals() contract:
// 1. Reflexive:  x.equals(x) == true
// 2. Symmetric:  x.equals(y) == y.equals(x)
// 3. Transitive: x.equals(y) && y.equals(z) => x.equals(z)
// 4. Consistent: multiple calls return same result (no random state)
// 5. Null:       x.equals(null) == false (never throws NPE)

// hashCode() contract:
// 1. Consistent: same object => same hashCode across calls
// 2. Equality:   x.equals(y) MUST imply x.hashCode() == y.hashCode()
// 3. Collision:  x.hashCode() == y.hashCode() does NOT imply x.equals(y)

// Records auto-generate correct equals() and hashCode()
record Point(int x, int y) {} // perfect equals and hashCode built-in

// Manual implementation (old style)
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;
    return x == p.x && y == p.y;
}
@Override
public int hashCode() {
    return Objects.hash(x, y);
}

// Comparable — natural ordering for TreeSet, TreeMap, Collections.sort
record Employee(String name, int salary) implements Comparable {
    @Override
    public int compareTo(Employee other) {
        return Integer.compare(this.salary, other.salary); // ascending salary
    }
}

// Comparator — external ordering (does not require modifying the class)
Comparator byNameThenSalary =
    Comparator.comparing(Employee::name)
              .thenComparingInt(Employee::salary);

The most common bug: implementing equals() without updating hashCode(). Two objects that are equal will then have different hash codes, causing them to land in different hash buckets — a HashMap will store both, and HashSet will contain both, breaking uniqueness silently.

What is the mandatory relationship between equals() and hashCode()?
What does Comparator.comparing(Employee::name).thenComparingInt(Employee::salary) produce?
32. What are the best practices for exception handling in Java?

Exception handling design is a common interview discussion topic. Java distinguishes checked exceptions (must be declared or caught — compiler enforces), unchecked (RuntimeException subclasses), and errors (JVM-level, not catchable in normal code).

// 1. Prefer specific exceptions over general ones
// Bad:  catch (Exception e) { ... }
// Good: catch (IOException | ParseException e) { ... }

// 2. Try-with-resources for AutoCloseable resources
try (var conn = ds.getConnection();
     var stmt = conn.prepareStatement(sql)) {
    // Both closed automatically in reverse order
} catch (SQLException e) {
    throw new DataAccessException("Query failed", e); // preserve cause
}

// 3. Wrap exceptions to maintain abstraction layers
// A service layer should not leak SQLException to the web layer
public User findUser(long id) {
    try {
        return repository.findById(id);
    } catch (SQLException e) {
        throw new ServiceException("User lookup failed for id=" + id, e);
    }
}

// 4. Never swallow exceptions silently
// Bad:
try { riskyOp(); } catch (Exception e) { /* ignore */ }
// Good: at minimum, log
try { riskyOp(); } catch (Exception e) { log.error("Failed", e); throw e; }

// 5. Checked vs unchecked — modern advice
// Use checked: caller CAN and SHOULD recover (FileNotFoundException)
// Use unchecked: programming error or unrecoverable (NullPointer, IllegalArgument)

// 6. Custom exceptions — extend RuntimeException for most domain errors
public class UserNotFoundException extends RuntimeException {
    private final long userId;
    public UserNotFoundException(long userId) {
        super("User not found: " + userId);
        this.userId = userId;
    }
    public long getUserId() { return userId; }
}

Multi-catch (catch (IOException | ParseException e)) was added in Java 7 and reduces boilerplate when handling multiple exception types with identical recovery logic. The caught variable is implicitly final in a multi-catch.

What does try-with-resources guarantee that a traditional try/finally cannot?
Why should a service layer wrap SQLException in a domain exception before throwing to callers?
33. Why is immutability important in Java and how do you implement it correctly?

An immutable object's state cannot change after construction. Immutable objects are inherently thread-safe, can be freely shared without copying, make better HashMap keys, and simplify reasoning about code because no method can have hidden side effects on them.

// Building an immutable class correctly
public final class Money { // 1. final class — no subclassing
    private final BigDecimal amount;   // 2. private final fields
    private final Currency currency;
    private final List notes;  // 3. defensive copy of mutable input

    public Money(BigDecimal amount, Currency currency, List notes) {
        this.amount   = Objects.requireNonNull(amount);
        this.currency = Objects.requireNonNull(currency);
        // Defensive COPY — don't store caller's mutable list
        this.notes    = List.copyOf(notes); // unmodifiable copy
    }

    public BigDecimal getAmount()   { return amount; }
    public Currency getCurrency()   { return currency; }
    public List getNotes()  { return notes; } // safe — unmodifiable

    // Return new instance for 'mutations'
    public Money add(Money other) {
        if (!this.currency.equals(other.currency))
            throw new IllegalArgumentException("Currency mismatch");
        return new Money(this.amount.add(other.amount), this.currency, notes);
    }
}

// Records are immutable by default (final, final fields, no setters)
record Point(int x, int y) {}

// Java 9+ — unmodifiable collection factories
List immList = List.of("a", "b", "c");    // unmodifiable
Map immMap = Map.of("k", 1);       // unmodifiable

// Collections.unmodifiable* wraps but the underlying can still be mutated
var inner = new ArrayList<>(List.of("a"));
var wrapped = Collections.unmodifiableList(inner);
inner.add("b"); // wrapped now also reflects the change!

The subtle pitfall: Collections.unmodifiableList(list) creates a view — not a copy. If the backing list is mutated through the original reference, the view reflects the change. List.copyOf() (Java 10) creates a true immutable snapshot.

What is the difference between Collections.unmodifiableList() and List.copyOf() regarding immutability?
Why must an immutable class make defensive copies of mutable constructor arguments?
34. What are default and static methods in Java interfaces?

Java 8 added default and static methods to interfaces, fundamentally changing the relationship between interfaces and abstract classes.

interface Validator {
    // Abstract method — implementors must provide
    boolean isValid(T value);

    // Default method — provided implementation, can be overridden
    default Validator and(Validator other) {
        return value -> this.isValid(value) && other.isValid(value);
    }

    default Validator or(Validator other) {
        return value -> this.isValid(value) || other.isValid(value);
    }

    default Validator negate() {
        return value -> !this.isValid(value);
    }

    // Static factory method — belongs to the interface, not instances
    static  Validator of(Validator validator) {
        return Objects.requireNonNull(validator);
    }
}

// Usage
Validator notBlank  = s -> !s.isBlank();
Validator notTooLong = s -> s.length() <= 100;
Validator combined  = notBlank.and(notTooLong);

System.out.println(combined.isValid("hello")); // true
System.out.println(combined.isValid(""));      // false

// Diamond problem resolution
interface A { default String hello() { return "A"; } }
interface B extends A { default String hello() { return "B"; } }
class C implements A, B {
    // Must override — most-specific interface wins (B),
    // but compiler forces explicit resolution
    @Override public String hello() { return B.super.hello(); }
}

Default methods enable interface evolution: adding a new method to an interface in a library no longer breaks all existing implementors, because the default provides backward compatibility. This is how Java 8 added methods like Collection.forEach(), List.sort(), and Map.getOrDefault() without breaking existing code.

What problem do interface default methods primarily solve?
When two interfaces provide conflicting default methods and a class implements both, what must the class do?
35. How does HashMap work internally in Java?

Understanding HashMap internals — hashing, buckets, collisions, resizing, and treeification — is one of the most commonly asked Java interview topics at mid-to-senior level.

// Internal structure:
// - Node[] table  — array of buckets (initially null, lazily allocated)
// - Default capacity: 16, load factor: 0.75
// - When size > capacity * loadFactor → resize (double capacity)

// put(key, value) steps:
// 1. Compute hash: (h = key.hashCode()) ^ (h >>> 16)  — spread high bits
// 2. Bucket index: hash & (capacity - 1)
// 3. If bucket empty: insert new Node
// 4. If bucket occupied (collision):
//    - Walk linked list: if key equals an existing node, update value
//    - Else append to linked list
//    - If list length >= TREEIFY_THRESHOLD (8) AND capacity >= 64:
//      convert to balanced Red-Black tree (O(log n) instead of O(n))

// Performance characteristics
// Best case (no collisions): O(1) get, put
// Worst case (all in one bucket, pre-Java 8): O(n)
// Worst case (all in one bucket, Java 8+): O(log n) after treeification

// Why the spread: hashCode() of strings/integers often has poor high-bit distribution
// The XOR spread: (h) ^ (h >>> 16) mixes high and low 16 bits

// Thread safety
// HashMap is NOT thread-safe
// ConcurrentHashMap: segments replaced by per-bin locking in Java 8
// Hashtable: fully synchronized (legacy, slow)
// Collections.synchronizedMap: wraps HashMap, one lock for all ops

Map map = new HashMap<>(32, 0.75f); // pre-size to avoid rehash

The load factor trade-off: a lower load factor (e.g., 0.5) means fewer collisions but more memory wasted on empty buckets. A higher load factor (e.g., 0.9) uses memory efficiently but increases collision probability, degrading performance toward O(log n) or O(n). The default 0.75 is the empirically best trade-off for general use.

What does HashMap do when a single bucket's linked list reaches TREEIFY_THRESHOLD (8) entries?
What is the purpose of the bit-spreading step '(h = key.hashCode()) ^ (h >>> 16)' in HashMap?
36. How do virtual threads compare to reactive programming (Project Reactor / RxJava)?

Both virtual threads (Java 21) and reactive frameworks solve the same underlying problem: how to serve many concurrent I/O-bound requests without blocking OS threads, which are expensive. They solve it very differently.

Reactive vs Virtual Threads
AspectReactive (Reactor/RxJava)Virtual Threads (Java 21)
Programming styleFunctional/async chains (flatMap, subscribe)Imperative blocking code
DebuggingComplex — callbacks and stack traces are fragmentedSimple — full stack traces, works with standard debugger
Library compatibilityRequires reactive-aware libs (R2DBC, WebFlux)Works with any blocking library (JDBC, HttpClient)
Error handlingonError / catch in chainsNormal try/catch
Learning curveHigh — new mental modelLow — reads like sequential code
Context propagationManual (reactor Context)ThreadLocal (or ScopedValue)
CPU-bound workNo advantageNo advantage — still needs ForkJoinPool
// Reactive style (Spring WebFlux)
Mono result = userRepository.findById(id)        // Mono
    .flatMap(user -> orderRepository.findByUser(user.id())) // Mono
    .map(order -> order.summary())
    .onErrorReturn("default response");

// Virtual thread style (Spring Boot 3.2+ with Tomcat virtual threads)
String result;
try {
    User  user  = userRepository.findById(id);    // blocking — parks VT
    Order order = orderRepository.findByUser(user.id()); // same
    result = order.summary();
} catch (Exception e) {
    result = "default response";
}

// Spring Boot 3.2 — enable virtual threads for Tomcat:
// spring.threads.virtual.enabled=true
// All @RequestMapping handlers run on virtual threads automatically

The consensus in the Java community for Java 21+: prefer virtual threads for new applications — the code is simpler, debuggable, and compatible with the existing JDBC/HTTP ecosystem. Reactive programming remains justified when you need fine-grained back-pressure, stream operators for event pipelines, or when integrating with reactive middleware that does not have a blocking API.

What is the main advantage of virtual threads over reactive programming from a developer experience perspective?
What Spring Boot property enables virtual threads for Tomcat in Spring Boot 3.2+?
37. What is the Java 11 HttpClient and how do you use it for HTTP requests?

The java.net.http.HttpClient (JEP 321, finalised in Java 11) is a modern, non-blocking HTTP client that supports HTTP/1.1 and HTTP/2, WebSockets, synchronous and asynchronous request modes, and streaming. It replaces the antiquated HttpURLConnection.

import java.net.http.*;
import java.net.URI;
import java.time.Duration;

// Build a reusable client (expensive to create — use as singleton)
HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .connectTimeout(Duration.ofSeconds(10))
    .followRedirects(HttpClient.Redirect.NORMAL)
    .build();

// Synchronous GET
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users/1"))
    .header("Accept", "application/json")
    .timeout(Duration.ofSeconds(30))
    .GET()
    .build();

HttpResponse response =
    client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode()); // 200
System.out.println(response.body());       // JSON string

// Asynchronous POST with JSON body
HttpRequest postRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {"name":"Alice","age":30}
        """))
    .build();

client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println)
    .join(); // wait for demo purposes

// With virtual threads (Java 21): use send() (blocking) — no need for sendAsync()
// The virtual thread parks during the network wait — OS thread is free
What does HttpClient.send() return and what thread does it block?
Why should HttpClient be created once and reused rather than created per request?
38. What capabilities do Java enums have beyond simple named constants?

Java enums are full-fledged classes that happen to have a fixed number of instances. This makes them far more powerful than C-style enums and eliminates entire categories of bugs that arise from using int constants.

public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS  (4.869e+24, 6.0518e6),
    EARTH  (5.976e+24, 6.37814e6);

    private final double mass;   // kg
    private final double radius; // meters

    Planet(double mass, double radius) { // constructor
        this.mass   = mass;
        this.radius = radius;
    }

    static final double G = 6.67300E-11;

    double surfaceGravity() { return G * mass / (radius * radius); }
    double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); }
}

double earthWeight = 75.0;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
    System.out.printf("Weight on %s = %6.2f%n", p, p.surfaceWeight(mass));
}

// Abstract methods — each constant provides its own implementation
enum Operation {
    PLUS("+ ") { @Override double apply(double x, double y) { return x + y; } },
    MINUS("-") { @Override double apply(double x, double y) { return x - y; } };

    private final String symbol;
    Operation(String symbol) { this.symbol = symbol; }
    abstract double apply(double x, double y);
    @Override public String toString() { return symbol; }
}

// Enum with switch (Java 21 — pattern matching works on enums)
int priority = switch (severity) {
    case CRITICAL -> 1;
    case HIGH     -> 2;
    case MEDIUM   -> 3;
    case LOW      -> 4;
};

// EnumSet and EnumMap — fast, memory-efficient specialisations
EnumSet workdays = EnumSet.range(MONDAY, FRIDAY);
EnumMap weights = new EnumMap<>(Planet.class);
What does Planet.values() return?
What is the advantage of EnumSet and EnumMap over HashSet and HashMap for enum keys?
39. What are the java.util.concurrent.atomic classes and how do they work?

The java.util.concurrent.atomic package provides lock-free, thread-safe single-variable operations using CPU-level Compare-And-Swap (CAS) instructions. They are faster than synchronized blocks for single-variable updates because they avoid locking entirely.

// AtomicInteger — counter safe for use from multiple threads
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();   // returns new value: 1
counter.getAndIncrement();   // returns old value: 1, new value: 2
counter.addAndGet(5);        // atomically add 5
counter.compareAndSet(7, 0); // only updates if current == 7

// AtomicLong, AtomicBoolean, AtomicReference work the same way

// AtomicReference — CAS on an object reference
AtomicReference name = new AtomicReference<>("initial");
boolean updated = name.compareAndSet("initial", "updated"); // true

// LongAdder — better than AtomicLong under HIGH contention
LongAdder hitCount = new LongAdder();
hitCount.increment();    // thread-safe, very fast under contention
long total = hitCount.sum(); // reads the aggregate
// LongAdder splits the counter across multiple cells to reduce contention

// AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
AtomicIntegerArray arr = new AtomicIntegerArray(10);
arr.compareAndSet(5, 0, 99); // CAS on index 5

// VarHandle (Java 9) — generalized CAS on any field
// More powerful but more verbose than atomic classes

LongAdder vs AtomicLong: at low contention they perform similarly. Under high contention (many threads incrementing simultaneously), LongAdder is significantly faster because it distributes the counter across multiple cells and only aggregates in sum(). Use LongAdder for pure counters; use AtomicLong when you need atomic read-modify-write with a precise current value (e.g., compareAndSet patterns).

How does LongAdder achieve better performance than AtomicLong under high thread contention?
What does compareAndSet(expected, update) do in AtomicInteger?
40. How does Java Reflection work and what are its performance implications?

Reflection allows programs to inspect and manipulate classes, methods, fields, constructors, and annotations at runtime — without knowing them at compile time. It powers frameworks like Spring (dependency injection), Hibernate (ORM), JUnit (test discovery), and Jackson (JSON serialisation).

import java.lang.reflect.*;

// Inspect a class at runtime
Class clazz = Class.forName("com.example.User");
// or: User.class  or  user.getClass()

// Fields
for (Field f : clazz.getDeclaredFields()) {
    f.setAccessible(true); // bypass private access
    System.out.println(f.getName() + " : " + f.getType().getSimpleName());
}

// Methods
Method method = clazz.getDeclaredMethod("setName", String.class);
method.setAccessible(true);
method.invoke(userInstance, "Alice"); // calls userInstance.setName("Alice")

// Constructor — create an instance dynamically
Constructor ctor = clazz.getDeclaredConstructor(String.class, int.class);
ctor.setAccessible(true);
Object user = ctor.newInstance("Alice", 30);

// Annotations — reading custom metadata
@Retention(RetentionPolicy.RUNTIME) // must be RUNTIME to read via reflection
@Target(ElementType.METHOD)
@interface Timed { String value() default ""; }

@Timed("fetchUser")
void fetchUser() { /* ... */ }

Method m = SomeClass.class.getMethod("fetchUser");
if (m.isAnnotationPresent(Timed.class)) {
    Timed t = m.getAnnotation(Timed.class);
    System.out.println("Timer label: " + t.value());
}

Performance implications: Reflection bypasses JIT optimisations (inlining, escape analysis) and incurs overhead from security checks, type conversions, and dynamic dispatch. setAccessible(true) in Java 9+ requires the module to be opened. Java 21 provides MethodHandles as a faster alternative for repeated reflective access — they can be specialised and inlined by the JIT after a warm-up period.

What annotation retention policy is required for an annotation to be readable via reflection at runtime?
What is the performance disadvantage of using reflection compared to direct method calls?
41. What are Unnamed Patterns and Variables in Java 21 (Preview) and how do they reduce boilerplate?

JEP 443 (preview in Java 21) introduces the underscore _ as a special token meaning 'I don't care about this value'. It can be used as an unnamed variable in catch blocks, try-with-resources, enhanced for loops, and lambda parameters — and as an unnamed pattern component in record patterns.

// --- Unnamed variable in catch ---
// Before: must name the exception even if you don't use it
try { processFile(); } catch (IOException e) { throw new RuntimeException(e); }

// After (Java 21 Preview):
try { processFile(); } catch (IOException _) { throw new RuntimeException("failed"); }

// --- Unnamed variable in enhanced for ---
int count = 0;
for (var _ : list) count++;  // iterate just for the count, don't need the element

// --- Unnamed pattern component in record pattern ---
record Point(int x, int y) {}
record Line(Point start, Point end) {}

// Only care about the x of start, ignore everything else
if (shape instanceof Line(Point(int x, _), _)) {
    System.out.println("Line starts at x=" + x);
}

// --- Unnamed lambda parameter ---
list.forEach(_ -> count++);

// Multiple unnamed variables in same scope (all named _ — allowed)
try {
    int _ = Integer.parseInt(s1);
    int _ = Integer.parseInt(s2);  // two _ variables, both unnamed
} catch (NumberFormatException _) {
    System.out.println("Parse failed");
}

Before JEP 443, Java required naming every variable even if it was intentionally unused, leading to suppressed warnings (@SuppressWarnings("unused")) and distracting variable names like ignored or dummy. The underscore communicates intent clearly: this value exists but is deliberately not examined.

In Java 21 (preview), what does using '_' as a variable name communicate?
Can you declare multiple unnamed variables '_' in the same scope in Java 21 (preview)?
42. When should you use StructuredTaskScope instead of CompletableFuture in Java 21?

Both APIs manage concurrent asynchronous work, but they have different designs, guarantees, and ideal use cases. Java 21 introduces StructuredTaskScope as the preferred model when running on virtual threads.

CompletableFuture vs StructuredTaskScope
AspectCompletableFutureStructuredTaskScope
API styleCallback/continuation chainImperative — fork, then join
CancellationManual — no automatic cleanupAutomatic — scope closes all tasks on exit
Error propagationManual — must handle in chainthrowIfFailed() re-throws cleanly
Thread modelForkJoinPool (platform threads) or custom executorVirtual threads (one per fork)
StructureUnstructured — tasks can outlive scopeStructured — subtasks never outlive scope
Stack tracesFragmented across continuationsFull linear traces per virtual thread
AvailabilityJava 8+Java 21 (preview)
// CompletableFuture — unstructured, manual cancellation
CompletableFuture  userF  = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture orderF = CompletableFuture.supplyAsync(() -> fetchOrder(id));
// If fetchUser throws, fetchOrder may continue running — thread leak possible
String result = userF.thenCombine(orderF,
    (u, o) -> u.name() + " / " + o.total()).join();

// StructuredTaskScope — structured, automatic cleanup
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var userTask  = scope.fork(() -> fetchUser(id));
    var orderTask = scope.fork(() -> fetchOrder(id));
    scope.join().throwIfFailed();  // if either fails, closes both
    String result2 = userTask.get().name() + " / " + orderTask.get().total();
} // scope.close() cancels any still-running tasks

// Choose CompletableFuture when:
// - Integrating with existing CF-based APIs
// - You need complex transformation pipelines on the result
// - You cannot use preview features

// Choose StructuredTaskScope when:
// - You want automatic cancellation and no thread leaks
// - Code runs on virtual threads (Java 21)
// - You want structured, readable concurrent code
What critical problem does StructuredTaskScope solve that CompletableFuture does not guarantee?
Why do virtual thread stack traces in StructuredTaskScope applications read better than CompletableFuture traces?
43. What major APIs were removed between Java 17 and Java 21?

Java 21 continues the cleanup of APIs that were deprecated in earlier releases. Understanding what was removed helps interviewers assess whether a candidate's Java knowledge is current.

Removals and Deprecations (Java 17–21)
API/FeatureDeprecatedRemovedReplacement
Security ManagerJava 17Java 21 (JEP 411 warning)OS-level security / security frameworks
Applet APIJava 9Java 23 target; Java 17 deprecated for removalWeb technologies
Thread.stop/suspend/resumeJava 1.2Not yet; flagged deprecated for removalCooperative interruption
finalize()Java 9Java 18 (JEP 421 deprecated)Cleaner API / try-with-resources
RMI ActivationJava 15Java 17Direct RMI or gRPC
Experimental AOT/JIT (Graal)N/AJava 17GraalVM as separate product
CMS Garbage CollectorJava 9Java 14G1, ZGC, Shenandoah
// finalize() — use Cleaner instead (Java 9+)
// Old pattern:
class OldResource {
    @Override protected void finalize() { /* cleanup */ } // unreliable, deprecated
}

// Modern pattern:
class ManagedResource implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;

    ManagedResource() {
        cleanable = CLEANER.register(this, new CleanupAction());
    }

    @Override public void close() { cleanable.clean(); }

    static class CleanupAction implements Runnable {
        @Override public void run() { /* release native resource */ }
    }
}

// SecurityManager — Java 17 deprecated, Java 21 warns, will be removed
// If your code calls System.setSecurityManager() → update now
// Migration: use OS-level security, module system, or third-party frameworks

The finalize() deprecation is particularly important: it was unreliable (not guaranteed to run, can be called on a different thread, delays GC), and caused memory leaks. The Cleaner API provides a reliable, predictable cleanup mechanism without the pitfalls of finalisation.

What is the recommended replacement for finalize() in modern Java?
In which Java version was the Security Manager deprecated for removal?
44. What are the most important JVM flags for tuning Java 21 application performance?

Understanding key JVM flags distinguishes senior engineers from juniors. Here are the flags that matter most for Java 21 production deployments.

Essential JVM Flags
FlagCategoryPurpose
-Xms / -XmxMemoryInitial / max heap size. Set equal to avoid resizing pauses
-XX:+UseZGCGCEnable ZGC (Generational by default in Java 21)
-XX:MaxGCPauseMillis=NGCG1 pause target (best effort)
-XX:+UseStringDeduplicationGCG1: deduplicate identical String objects
-Xlog:gc*:file=gc.logLoggingGC logging to file
-XX:+HeapDumpOnOutOfMemoryErrorDiagnosticsAuto heap dump on OOM
-XX:HeapDumpPath=/pathDiagnosticsLocation for heap dump
-XX:+ExitOnOutOfMemoryErrorReliabilityExit JVM on OOM instead of limping on
-Djdk.virtualThreadScheduler.parallelism=NLoomNumber of carrier threads for VTs
--enable-previewLanguageEnable preview features (e.g., String Templates, unnamed classes)
-XX:+TieredCompilationJITMulti-tier JIT (default on — rarely need to disable)
--add-opens module/package=ALL-UNNAMEDModulesOpen module package to classpath (for reflection)
# Production example: Java 21 virtual-thread microservice with ZGC
java \
  -Xms512m -Xmx2g \
  -XX:+UseZGC \
  -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/app/heap.hprof \
  -XX:+ExitOnOutOfMemoryError \
  -Djava.util.concurrent.ForkJoinPool.common.parallelism=1 \
  -jar myapp.jar

# Carrier thread count for virtual threads (default = number of CPU cores)
# -Djdk.virtualThreadScheduler.parallelism=16

# Diagnose pinning (virtual thread pinned to carrier too long)
# -Djdk.tracePinnedThreads=full
What does setting -Xms equal to -Xmx improve in a production JVM?
Which JVM flag helps diagnose virtual thread pinning issues in Java 21?
45. What are the key steps and pitfalls when migrating an application to Java 21?

Migrating from Java 8/11/17 to Java 21 requires attention to removed APIs, module system access restrictions, and the opportunity to adopt new features incrementally.

// Step 1: Compile with Java 21 — fix deprecation warnings
// javac --release 21 -Xlint:deprecation src/**/*.java

// Step 2: Run with Java 21 — look for startup warnings
// WARNING: Illegal reflective access ... (module access)
// Fix: add --add-opens flags until migrated properly

// Step 3: Scan for removed APIs
// - SecurityManager: remove System.setSecurityManager() calls
// - Thread.stop() / suspend() / resume(): use interrupt/flags
// - sun.* / com.sun.* internal APIs: use public replacements
// - Applet: remove entirely

// Step 4: Update dependencies
// Many libraries had Java 9+ compatibility issues that are now fixed
// Spring Boot 3.x requires Java 17+ and supports Java 21 natively
// Hibernate 6.x is Jakarta EE and Java 17+ compatible
// Jackson 2.15+ has module-info.java

// Step 5: Enable virtual threads (Spring Boot 3.2)
// spring.threads.virtual.enabled=true
// Then: remove thread pool sizing, increase load test concurrency

// Step 6: Replace synchronized with ReentrantLock in hot paths
// Use: jcmd  Thread.print to see pinned virtual threads

// Step 7: Adopt new language features incrementally
// Records -> DTO classes first
// Pattern matching instanceof -> existing instanceof+cast
// Text blocks -> multi-line strings (SQL, JSON, HTML)
// Switch expressions -> switch statements returning a value

// Step 8: Update GC selection
// G1 is still default and excellent
// Consider ZGC if latency percentiles matter and heap > 4 GB
Common Migration Pitfalls
PitfallSymptomFix
sun.misc.Unsafe usageInaccessibleObjectExceptionUse VarHandle or Cleaner API
Reflective access to internalsInaccessibleObjectException--add-opens or update library
Split packages across modulesModule resolution failureMerge JARs or use classpath
Thread.stop() removalNoSuchMethodErrorCooperative interrupt flag
synchronized + virtual threadsCarrier thread pinningReplace with ReentrantLock
High ThreadLocal usageMemory pressure at high VT countSwitch to ScopedValue
What is the primary configuration to enable virtual threads for HTTP request handling in Spring Boot 3.2?
Why does replacing 'synchronized' with 'ReentrantLock' matter for virtual thread performance?
46. How do Spring 7 and Spring Boot 4 optimize thread usage compared to older blocking models?

Older versions of Spring MVC scaled using a rigid "one-request-per-thread" blocking architecture tied to expensive platform (OS) threads. Spring 7 builds on top of modern Java baselines to offer native, out-of-the-box optimization for Virtual Threads. This allows applications to process millions of concurrent web requests using lightweight, ephemeral threads without switching entirely to complex, reactive programming architectures.

«
»
Spring

Comments & Discussions