var, records, sealed classes (sealed/non-sealed/permits), pattern matching (instanceof, switch, record patterns), switch expressions, text blocks, virtual threads and Project Loom architecture, virtual vs platform threads, blocking I/O on virtual threads, pinning, structured concurrency and StructuredTaskScope, the Vector API, the Foreign Function & Memory API, profiling the JVM in production, tuning a JVM for a million users, and Java vs Go for backends.
Published September 25, 2026
Senior candidates are expected to know what shipped in which LTS, and which features change how you design code:
State preview versus final status accurately. It shows you actually follow the platform.
var (Java 10)? Is it just syntactic sugar?Short answer: var is local variable type inference: the compiler infers the static type from the initialiser. The variable is still strongly and statically typed. It's syntactic sugar, with a few real capabilities:
The limits:
var x = null;, and no array initialisers without new.Style: use it when the type is obvious from the right-hand side (var orders = new ArrayList<Order>()). Avoid it when it hides important types (var result = service.process()), and beware of inferring concrete types: var list = new ArrayList<>() becomes ArrayList<Object>.
Short answer: A record (preview in 14 and 15, final in Java 16) is a transparent, shallowly immutable data carrier:
public record Money(BigDecimal amount, Currency currency) {
public Money { // compact constructor: validation and normalisation
Objects.requireNonNull(amount); Objects.requireNonNull(currency);
amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
}
public Money plus(Money other) { requireSameCurrency(other); return new Money(amount.add(other.amount), currency); }
private void requireSameCurrency(Money o) { if (!currency.equals(o.currency)) throw new IllegalArgumentException(); }
}
The compiler generates:
amount(), not getAmount());equals/hashCode/toString over all the components.Records are final, can't extend classes, and can't declare instance fields beyond their components. They can implement interfaces, and have static members, methods and nested types. They're ideal for DTOs, value objects, events, map keys, and multiple return values, and they support record patterns for deconstruction (Java 21).
Common trap: "records are deeply immutable". A List component can still be mutated, so copy it in the compact constructor (List.copyOf). Also, JPA entities can't be records.
Learn it in depth → Records
sealed, non-sealed and permits work together (Java 17)? When should you use sealed classes?Short answer: A sealed class or interface restricts which types may extend or implement it, through permits. The permitted subclasses must be in the same module (or the same package in the unnamed module), and each must declare one of:
final: the hierarchy stops there;sealed: it continues, but again restricted, with its own permits;non-sealed: it re-opens that branch to arbitrary extension.Records and enums are implicitly final, so they're natural leaves.
public sealed interface Payment permits CardPayment, UpiPayment, WalletPayment {}
public record CardPayment(String token, int last4) implements Payment {}
public record UpiPayment(String vpa) implements Payment {}
public non-sealed class WalletPayment implements Payment {} // partners may subclass wallet types
When to use them:
switch;They were final in Java 17 (preview in 15 and 16).
Learn it in depth → Sealed Classes
Short answer:
instanceof patterns (Java 16): if (obj instanceof Order o && o.isPaid()) {...}. They test, cast and bind in one step, with flow scoping.switch (Java 21): switch over any type, with type patterns, guards (case Order o when o.total() > 1000 ->), case null, and exhaustiveness checking for sealed hierarchies and enums.case Circle(Point(var x, var y), double r) -> ...._ (Java 22): case Point(var x, _) ->, and catch (Exception _).static String describe(Shape s) {
return switch (s) {
case Circle(var c, var r) when r > 100 -> "large circle at " + c;
case Circle c -> "circle r=" + c.radius();
case Rectangle(var w, var h) -> "rectangle " + w + "x" + h;
case Square sq -> "square " + sq.side();
}; // exhaustive over the sealed Shape: no default
}
Learn it in depth → Pattern Matching
Short answer: A switch expression yields a value, and uses arrow labels:
breaks;case SAT, SUN ->);yield to return a value from a block;default unless every enum constant or sealed subtype is covered.The classic statement form falls through, doesn't produce a value, and isn't checked for exhaustiveness.
int workingHours = switch (day) {
case SATURDAY, SUNDAY -> 0;
case FRIDAY -> 6;
default -> {
log.debug("regular day {}", day);
yield 8;
}
};
Short answer: Multi-line string literals, delimited by """. Incidental indentation is stripped automatically (based on the closing delimiter's position), and newlines are normalised to \n. The escapes are \ (join lines) and \s (keep trailing spaces). They make embedded JSON, SQL, HTML, YAML and test fixtures readable, with no concatenation or escaped quotes. Combine them with formatted(...) for values, and never to build SQL from user input (use bind parameters).
String query = """
SELECT o.id, o.total
FROM orders o
WHERE o.status = :status
AND o.created_at > :since
ORDER BY o.created_at DESC
""";
Short answer: Virtual threads (final in Java 21, JEP 444) are lightweight Thread instances managed by the JVM, not by the OS.
sleep, BlockingQueue.take, locks), the JDK unmounts it: its stack frames are copied to the heap, the carrier is freed for another virtual thread, and the blocking operation is implemented with non-blocking I/O and park/unpark underneath.Thread.ofVirtual().start(r), Thread.startVirtualThread(r), and Executors.newVirtualThreadPerTaskExecutor(). In Spring Boot 3.2+, spring.threads.virtual.enabled=true.Learn it in depth → Virtual Threads
Short answer: In thread-per-request servers, throughput is limited by the number of threads (Little's law: concurrency = throughput × latency). Platform threads are expensive (MBs of stack, OS scheduling), so pools of 200–500 cap throughput when requests spend most of their time waiting on I/O. Virtual threads make waiting cheap: a blocked virtual thread holds no OS thread, so you can have one virtual thread per request or task, with tens of thousands in flight.
The effect on blocking I/O: you keep simple, blocking, imperative code (JDBC, HttpClient, RestClient), and get near-reactive scalability, with readable stack traces and normal debugging and profiling.
Key points to cover:
Short answer:
| Platform thread | Virtual thread | |
|---|---|---|
| Backing | 1:1 with an OS thread | M:N, mounted on carrier platform threads |
| Stack | Fixed, about 1 MB reserved, native | Grows and shrinks on the heap |
| Creation cost | Expensive (a syscall, memory) | Very cheap (like an object) |
| How many | Thousands | Millions |
| Blocking | Blocks the OS thread | Unmounts, freeing the carrier |
| Pooling | Pooled (they're expensive) | Never pool them: one per task |
| Best for | CPU-bound work, long-lived background threads | I/O-bound, high-concurrency tasks |
| Priority and daemon | Configurable | Always daemon, normal priority |
ThreadLocals work on virtual threads, but are costly with millions of threads. Prefer ScopedValue (final in Java 25).
Short answer: A virtual thread is pinned when it can't unmount from its carrier while blocking, so the carrier thread is blocked too, which reduces scalability, and can even deadlock with few carriers. The causes:
synchronized monitor, or inside Object.wait(). That was the big one.<clinit>) blocking, and some file I/O operations (which are compensated by temporarily adding carriers).Java 24 (JEP 491) reimplemented monitors, so synchronized no longer pins.
Detection: the JFR event jdk.VirtualThreadPinned (enabled by default, with a threshold). The older -Djdk.tracePinnedThreads was removed in 24.
Mitigation on 21–23: replace synchronized around blocking I/O with ReentrantLock, and upgrade libraries (JDBC drivers and connection pools updated for Loom).
StructuredTaskScope?Short answer: Structured concurrency treats a group of concurrent subtasks as a single unit of work with a lexical scope, like a code block:
Status: it's a preview API (JEP 505, fifth preview in Java 25, with a redesigned API: StructuredTaskScope.open(Joiner), fork, join). It's designed for virtual threads, and inherits scoped values automatically.
// Java 25 preview API (--enable-preview)
Response handle(long userId) throws InterruptedException {
try (var scope = StructuredTaskScope.open()) { // default: fail if any subtask fails
Subtask<User> user = scope.fork(() -> userService.find(userId));
Subtask<List<Order>> orders = scope.fork(() -> orderService.recent(userId));
scope.join(); // waits; the siblings are cancelled on failure
return new Response(user.get(), orders.get());
}
}
Compared with ad-hoc CompletableFuture fan-out, you get automatic cancellation, no leaked tasks, clearer error handling, and better observability.
Short answer: jdk.incubator.vector expresses SIMD computations explicitly: FloatVector, IntVector, "species" matching the CPU's vector width, lane-wise operations, masks and reductions. The JIT compiles these to the platform's vector instructions (AVX2/AVX-512, NEON/SVE), with predictable performance, instead of relying on C2 auto-vectorising simple loops. It's used for numerical kernels, ML inference, image processing, and search or scoring. It's still incubating (10th incubator in Java 25), waiting on Project Valhalla value classes before finalisation. Enable it with --add-modules jdk.incubator.vector.
static final VectorSpecies<Float> S = FloatVector.SPECIES_PREFERRED;
static void scale(float[] a, float factor, float[] out) {
int i = 0;
for (; i < S.loopBound(a.length); i += S.length())
FloatVector.fromArray(S, a, i).mul(factor).intoArray(out, i);
for (; i < a.length; i++) out[i] = a[i] * factor; // tail
}
Short answer: Project Panama's replacement for JNI and Unsafe memory access, final in Java 22 (JEP 454):
Arena (it controls lifetime: confined, shared or auto), MemorySegment (bounds-checked, off-heap or on-heap memory, with deterministic deallocation when the arena closes) and MemoryLayout (describes C structs).Linker creates downcall handles (MethodHandles calling native functions) and upcall stubs (native code calling back into Java). jextract generates Java bindings from C headers.The benefits over JNI: no C glue code, safety (bounds and lifetime checks), performance comparable or better, and 64-bit memory offsets. It's used for native libraries (crypto, ML runtimes, databases), large off-heap data, and memory-mapped files with explicit unmapping. Native access needs --enable-native-access to avoid warnings.
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateFrom("Hello Panama");
long len = (long) strlen.invokeExact(cString); // 12; the memory is freed when the arena closes
}
Short answer: Use low-overhead, always-available tooling:
-XX:StartFlightRecording=...,settings=profile, about 1–2% overhead): dump on demand (jcmd <pid> JFR.dump) or on incidents, and analyse in JMC. It covers CPU samples, allocation, locks, GC, I/O and exceptions.Short answer: A million users is a system problem first. The JVM is one part of it.
-Xms=-Xmx sized to the live set with headroom, or MaxRAMPercentage about 70–75;ExitOnOutOfMemoryError and heap dumps;Short answer:
jlink (small runtimes).Q: Which features are final in Java 21 (the LTS)?
A: Virtual threads, record patterns, pattern matching for switch, sequenced collections, generational ZGC (as an option), and the key encapsulation mechanism API. String templates, structured concurrency and scoped values were previews in 21. Scoped values became final in Java 25, and string templates were withdrawn.
Q: Should you pool virtual threads, or limit them with a fixed-size pool?
A: Never pool them: create one per task. To limit concurrency to a resource (say, 50 database calls), use a Semaphore, or the resource's own pool. Thread-pool sizing was only ever an indirect way of limiting concurrency.
Q: Do virtual threads work with ThreadLocal and synchronized code?
A: Yes, they work. ThreadLocals cost memory per virtual thread (prefer ScopedValue). synchronized pinned carriers in Java 21–23, and doesn't since Java 24.
Q: What Java 25 LTS features are worth mentioning? A: Scoped values (final), flexible constructor bodies, compact source files and instance main methods, module import declarations, compact object headers (a product option), AOT method profiling and command-line ergonomics (Project Leyden), generational Shenandoah, and JFR CPU-time profiling.