Java / Java 25 latest features Interview questions
Last updated
1. What is Java 25?
Java 25 (JDK 25) is the Java SE release that shipped in September 2025. It is a long-term support (LTS) release, the successor to Java 21, and it bundles 18 JEPs delivered across the 22 to 25 feature releases of the past two years.
Finalized in 25: scoped values, module import declarations, compact source files with instance main methods, flexible constructor bodies, the Key Derivation Function API, compact object headers and Generational Shenandoah.
Still in preview or incubation: stable values, structured concurrency (fifth preview), primitive types in patterns (third preview), PEM encodings and the Vector API (tenth incubator).
Take quiz
A non-LTS release supported for six months
A preview-only release with no final features
An LTS release that follows Java 21
A bug-fix update to Java 21
Structured concurrency
The Vector API
Stable values
Scoped values
2. What does LTS mean for Java 25?
LTS stands for long-term support. Vendors such as Oracle, Eclipse Temurin, Amazon Corretto and Azul keep shipping security and bug-fix updates for an LTS release for years, while regular releases are dropped after about six months.
Oracle now ships an LTS every two years, so Java 21 was followed by Java 25. Most production teams standardize on LTS versions and skip the ones in between.
| Release type | Examples | Update window |
| LTS | Java 21, Java 25 | Several years from multiple vendors |
| Non-LTS | Java 22, 23, 24 | About six months |
LTS only covers the supported JDK. Preview features in Java 25 still need --enable-preview and can change in later releases.
Take quiz
It removes the need for any testing during upgrades
It is the only version that supports virtual threads
Preview features become permanent in it
Security and bug-fix updates continue for years
Java 21
Java 22
Java 24
Java 23
3. What are compact source files in Java 25?
Compact source files (JEP 512, final) let you write a Java program without declaring a class. You put methods and fields at the top level of the file, and the compiler wraps them in an implicit, final class named after the file.
// Hello.java void main() { IO.println("Hello, Java 25"); }
Run it with java Hello.java. The file also gets an automatic import module java.base;, so common classes such as List and Path need no imports.
The goal is a gentler first program for beginners and quick scripts, without hiding the language. You can later wrap the code in a normal class without changing the logic.
Take quiz
Wraps them in an implicit class named after the file
Turns them into static methods of Main
Moves them into java.lang.Object
Rejects the file with a compile error
jdk.compiler
java.base
java.desktop
java.se
4. What is the purpose of the java.io.IO class?
java.io.IO is a small class added in Java 25 for simple console input and output. It exists so beginner programs do not have to start with System.out.println and a Scanner.
It offers static methods: print(Object), println(Object) and readln(String prompt), plus no-argument variants.
void main() { String name = IO.readln("Your name: "); IO.println("Hello, " + name); }
In the final JEP 512 you must write IO.println. The earlier previews statically imported these methods, but that was removed. Inside a compact source file the class is visible because java.io comes from java.base.
Take quiz
println("text") with no prefix, always
IO.println("text")
Console.println("text")
System.IO.println("text")
Opens a GUI dialog
Prints the text and returns nothing
Prints the prompt and reads a line of input
Reads a file called Name
5. What are module import declarations?
A module import declaration, import module M;, imports on demand every public top-level class and interface from all packages exported by module M. Java 25 finalizes it in JEP 511.
import module java.base; List<String> names = Stream.of("a", "b").toList(); Path p = Path.of("data.txt");
Here List, Stream and Path come from java.util, java.util.stream and java.nio.file with no separate imports. It also covers packages from modules that M reads transitively, so import module java.se; pulls in nearly the whole platform.
It is most useful in scripts, small tools and compact source files. Large codebases usually still prefer explicit single-type imports for clarity.
Take quiz
Only classes you later use in the file
Private classes of the module
All public types in the packages exported by java.base
Only classes in java.lang
Loading modules at runtime
Hiding the origin of classes in large codebases
Replacing the module-info.java file
Scripts and small programs that use many platform classes
6. What are flexible constructor bodies?
Flexible constructor bodies (JEP 513, final in 25) allow statements before the explicit super(...) or this(...) call. That region is called the prologue.
Before this, the explicit constructor call had to be the first statement. To validate or transform an argument you had to use a static helper method or an awkward expression inside the call.
class Teenager extends Person { Teenager(int age) { if (age < 13 || age > 19) throw new IllegalArgumentException("Not a teenager: " + age); super(age); } }
The prologue cannot use the object under construction, so it cannot read instance fields or call instance methods. It can validate arguments, compute values and initialize fields of its own class.
Take quiz
Only after the object is fully constructed
Only inside a static initializer
Nowhere, super(...) must always be first
Before the super(...) call, in the prologue
Checking and transforming constructor arguments
Reading inherited fields
Returning a different object
Calling instance methods of this
7. What are scoped values?
A ScopedValue holds immutable data that a method can share with the code it calls, including child threads started through structured concurrency, without passing it as a parameter. JEP 506 makes it final in Java 25.
static final ScopedValue<String> USER = ScopedValue.newInstance(); void handle() { ScopedValue.where(USER, "alice").run(this::process); } void process() { IO.println("User: " + USER.get()); }
The binding exists only while run executes. When it returns, the value is gone, which makes the lifetime easy to reason about. Typical uses are request context, user identity and trace IDs.
Take quiz
Only while the bound run or call is executing
Until the JVM restarts
Until set() is called again
Until the thread ends
ScopedValue.fetch(USER)
USER.get()
Thread.current().get(USER)
USER.read()
8. What is the Key Derivation Function API?
The Key Derivation Function (KDF) API, javax.crypto.KDF, is final in Java 25 (JEP 510). A KDF takes a secret plus extra input such as a salt, and produces new key material that is safe to use for encryption or authentication.
The JDK ships with HKDF (RFC 5869) in several hash variants, for example HKDF-SHA256. Protocols such as TLS 1.3 and Hybrid Public Key Encryption rely on this construction.
Before 25 you had to implement HKDF yourself on top of Mac, which is easy to get wrong. The new API is also designed so that other algorithms, such as Argon2, can be plugged in later.
Take quiz
Base64Kdf
HKDF
MD5-Crypt
Argon2 only
A digital certificate
A random IV for CBC mode only
New key material derived from a secret and other inputs
A hash of a password for storage in a file
9. What are compact object headers?
Compact object headers (JEP 519) shrink the per-object header in HotSpot from 96 to 128 bits down to 64 bits on 64-bit platforms. It was experimental in Java 24 and is a product feature in Java 25.
Every Java object has a header holding the mark word and class pointer. By packing both into one 8-byte word, each object gets smaller, and apps with millions of small objects use noticeably less heap.
Oracle and Amazon have reported double-digit heap savings and some CPU improvement from less memory traffic. It is not on by default, so you opt in with a JVM flag and measure your own workload.
Take quiz
Larger maximum array size
Faster class loading from disk
Smaller objects, so less heap is used
Removing the garbage collector
32 bits
256 bits
128 bits
64 bits
10. What is Generational Shenandoah?
Generational Shenandoah (JEP 521) adds a young and old generation to the Shenandoah low-pause collector. It was experimental in Java 24 and became a product feature in Java 25.
Most objects die young. Collecting the young generation more often means less work per cycle, which improves throughput and lowers the memory overhead, while pauses stay short.
Non-generational Shenandoah is still the default mode. You choose the generational mode with -XX:ShenandoahGCMode=generational once Shenandoah is selected.
In practice, the young generation is collected frequently and cheaply, and only survivors get promoted to the old generation. That cuts the amount of memory Shenandoah has to scan on every cycle, and reduces the extra headroom the heap needs to avoid allocation stalls.
Take quiz
Automatic thread pinning
A stop-the-world full compaction every second
Support for 32-bit heaps
Separate young and old generations
No, it must be enabled explicitly
Yes, but only on Linux
Yes, it replaced the old mode
No, it was removed in 25
11. What are stable values in Java 25?
A StableValue (JEP 502, preview) is a holder that is set at most once and then never changes. After it is set, the JVM can treat its contents like a constant, which gives final-like speed with the flexibility of lazy initialization.
class OrderService { private final StableValue<Logger> logger = StableValue.of(); Logger logger() { return logger.orElseSet(() -> Logger.create(OrderService.class)); } }
The supplier runs on the first call. Later calls return the same instance. It is a preview API, so compile and run with --enable-preview.
Take quiz
At most once
Only at class load
Any number of times
Exactly twice
It makes the value mutable across threads
The JVM can treat the value as a constant after it is set
It avoids needing any supplier
It disables garbage collection of the value
12. What is structured concurrency in Java 25?
Structured concurrency treats a group of related tasks as one unit of work. You open a StructuredTaskScope, fork subtasks, join them, and the scope closes only when all subtasks are done. JEP 505 is the fifth preview in Java 25.
If one subtask fails, the scope can cancel the others instead of leaving them running. Errors and cancellation follow the code structure, and thread dumps show the parent-child relationship.
flowchart TD
A["open scope"] --> B["fork subtask 1"]
A --> C["fork subtask 2"]
B --> D[join]
C --> D
D --> E{all succeeded?}
E -- yes --> F["use results"]
E -- no --> G["cancel the rest and throw"]
It needs --enable-preview in Java 25.
Take quiz
A single database transaction only
A group of related subtasks in one scope
Each thread in a pool independently
A whole application process
--add-modules java.concurrent
--enable-structured
--enable-preview
No flag, it is final
13. What is the PEM encodings API?
The PEM API (JEP 470, preview) converts cryptographic objects to and from the PEM text format, the familiar -----BEGIN ...----- blocks used for keys and certificates.
Previously you had to strip headers, Base64-decode and parse DER by hand. Now PEMEncoder and PEMDecoder handle it, working with types that implement the new DEREncodable interface, such as keys, certificates and CRLs.
String pem = PEMEncoder.of().encodeToString(privateKey); PrivateKey back = PEMDecoder.of().decode(pem, PrivateKey.class);
It needs --enable-preview in Java 25.
PEM is plain text, so these blocks are easy to paste into config files, Kubernetes secrets and TLS tooling. The API also supports encrypted private keys, so a password-protected key can be read and written without a third-party library like Bouncy Castle for the basic cases.
Take quiz
XML signature documents
PKCS12 binary files
-----BEGIN ...----- Base64 blocks
JSON Web Key sets
PEMWriter
KeyEncoder
Base64Pem
PEMEncoder
14. What are primitive patterns in Java 25?
Primitive types in patterns, instanceof and switch (JEP 507, third preview) let you use int, long, double, boolean and the other primitives as pattern types, and allow switch over long, float, double and boolean.
int count = 300; if (count instanceof byte b) { IO.println("fits in a byte: " + b); } else { IO.println("too big for a byte"); }
The test succeeds only when the conversion is exact and lossless. Here 300 does not fit in a byte, so the else branch runs. It needs --enable-preview.
Take quiz
Never, instanceof rejects primitives
Only when count is zero
Always, the value is truncated
When the value converts to byte without losing information
long, float, double and boolean
Only record types
Only String
Only int and char
15. What is JFR CPU-time profiling?
JFR CPU-time profiling (JEP 509, experimental) adds a new event, jdk.CPUTimeSample, that samples Java threads according to the CPU time they actually burn, not wall-clock time.
The older ExecutionSample event only looked at threads that were running Java code at that moment. It could miss time spent in native code and gave a skewed view for CPU-bound services.
The new sampler uses the Linux kernel's per-thread CPU timers, so it is Linux only. It is experimental, so expect the event details to evolve.
Take quiz
Linux
macOS only
All platforms equally
Windows only
jdk.ThreadCpu
jdk.CPUTimeSample
jdk.CpuLoadSample
jdk.ProfilerTick
16. What is AOT command-line ergonomics?
AOT command-line ergonomics (JEP 514) simplifies creating an ahead-of-time cache. Before Java 25 you needed two steps with separate flags: record a training run, then create the cache. Now a single command does both.
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App
The JVM runs the app, observes class loading and linking, and writes app.aot when it exits. Later runs use it with -XX:AOTCache=app.aot. You can also choose the behavior explicitly with -XX:AOTMode.
Under the hood the JVM still does a record phase and a create phase, but it chains them for you. Keep the old -XX:AOTMode=record and create options in mind if you need separate steps, for example when training on one machine and building the cache in a CI job.
Take quiz
-XX:+WriteAOTNow
-XX:AOTCacheOutput=app.aot
-Xshare:dump=app.aot
-XX:SaveCache=app.aot
-XX:+LoadAOT
-cp app.aot
-XX:AOTCache=app.aot
-XX:AOTMode=delete
17. What is AOT method profiling?
AOT method profiling (JEP 515) stores method execution profiles in the AOT cache. At startup the JIT compiler can use data from the training run instead of starting from scratch.
Normally the JVM interprets code, collects profiles, and only then compiles hot methods. That warmup phase delays peak speed. With cached profiles the JIT can make good optimization decisions earlier.
The benefit is largest for short-lived apps such as command-line tools and serverless functions, where warmup is a big share of total runtime. It needs a representative training run to be useful.
Take quiz
Source code of hot methods
Compiled GPU kernels
Method execution profiles from a training run
Heap dumps of the application
Only apps using the Vector API
Applications that run for months with no restarts
Programs with no hot methods
Short-lived apps where warmup dominates runtime
18. What is ahead-of-time class loading and linking?
Ahead-of-time class loading and linking (JEP 483, Java 24) lets the JVM store loaded and linked classes from a training run in an AOT cache, so the next start can skip parsing, verifying and linking them.
It builds on the old class-data sharing (CDS) idea but goes further, because the classes are already in a ready-to-use state. Java 25 builds on it with simpler flags (JEP 514) and method profiles (JEP 515).
| Step | What happens |
| Training run | App runs once; JVM records loaded classes |
| Cache creation | Classes are loaded and linked, stored in a file |
| Production run | JVM maps the cache and starts faster |
Take quiz
Garbage collection
Running main()
Loading of native libraries
Parsing, verifying and linking of cached classes
A training run of the application
A garbage collection cycle
The javac compile step
The JVM installer
19. What is the Vector API in Java 25?
The Vector API (JEP 508) lets you write explicit SIMD computations that the JIT compiler maps to CPU vector instructions. Java 25 ships the tenth incubator round, so it is still not a permanent API.
var sp = IntVector.SPECIES_PREFERRED; int i = 0; for (; i < sp.loopBound(a.length); i += sp.length()) { IntVector.fromArray(sp, a, i) .add(IntVector.fromArray(sp, b, i)) .intoArray(c, i); } for (; i < a.length; i++) c[i] = a[i] + b[i];
The second loop handles the leftover elements. It lives in jdk.incubator.vector, so you need --add-modules jdk.incubator.vector.
Take quiz
jdk.incubator.vector
java.base.vector
java.vector
jdk.simd
Second preview
Tenth incubator
Deprecated
Final
20. What are Stream Gatherers?
Stream Gatherers (JEP 485, final in Java 24) add custom intermediate operations to the Stream API through Stream.gather(Gatherer). Before this you were limited to the built-in operations such as map and filter.
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5) .gather(Gatherers.windowFixed(2)) .toList(); // [[1, 2], [3, 4], [5]]
Built-ins in java.util.stream.Gatherers include windowFixed, windowSliding, fold, scan and mapConcurrent.
A gatherer can keep state between elements and can end the stream early, which ordinary intermediate operations cannot do cleanly. You can also chain gatherers with andThen, or write your own by implementing the Gatherer interface when the built-ins are not enough.
Take quiz
flatMap
gather
collect
reduce
Sums pairs of numbers
Sorts the stream in pairs
Groups elements into lists of two, with a shorter last list
Drops every second element
21. What is the Class-File API?
The Class-File API (JEP 484, final in Java 24) lives in java.lang.classfile. It lets you parse, generate and transform .class files with a standard JDK API.
ClassModel cm = ClassFile.of().parse(Path.of("Hello.class")); cm.methods().forEach(m -> IO.println(m.methodName().stringValue()));
Frameworks used to bundle libraries like ASM for this job. Because the class-file format changes with each Java release, those libraries had to catch up each time. A built-in API follows the format automatically.
The same API can generate and rewrite classes. You describe a class with builders, and transform an existing one by dropping, replacing or adding elements. This matters most to framework and tool authors, such as code generators, profilers and testing libraries. Most application developers will never call it directly.
Take quiz
java.lang.reflect.classes
javax.tools.bytecode
java.lang.classfile
java.io.classfile
Missing generics at runtime
Slow garbage collection
Unreadable stack traces
Third-party bytecode libraries lagging behind new class-file versions
22. What are unnamed variables and patterns?
Unnamed variables and patterns (JEP 456, final in Java 22) let you use _ where you must declare a variable but never use it.
try { Integer.parseInt(text); } catch (NumberFormatException _) { IO.println("not a number"); } for (var _ : orders) count++; if (shape instanceof Point(int x, _)) { use(x); }
It works for local variables, catch parameters, lambda parameters, loop variables and record pattern components. It signals intent and stops linters from warning about unused names.
Using _ as an identifier for a normal variable is a compile error since Java 9, so the feature has no clash with older code. Use it only when ignoring the value is deliberate. If you will read the variable later, give it a real name.
Take quiz
The previous exception
A wildcard that catches all exceptions
A reserved keyword for finally
A variable that is declared but intentionally unused
Java 22
Java 21
Java 25
Java 17
23. What is the Foreign Function and Memory API?
The Foreign Function and Memory (FFM) API (JEP 454, final in Java 22) lives in java.lang.foreign. It lets Java call native libraries and manage off-heap memory without writing JNI glue code.
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 s = arena.allocateFrom("Java 25"); long len = (long) strlen.invokeExact(s); }
Arena controls the memory lifetime and frees it deterministically. invokeExact declares Throwable, so handle or rethrow it.
Compared with JNI, there is no C stub to compile and no native keyword. The jextract tool can read a C header file and generate Java bindings for you. Native access is guarded, so pass --enable-native-access to avoid warnings in modular apps.
Take quiz
Arena
NativeScope
MemoryHeap
UnsafeBuffer
Java serialization
JNI glue code for calling native libraries
The garbage collector
The module system
24. What is JFR method timing and tracing?
JFR method timing and tracing (JEP 520) adds two events. jdk.MethodTiming records statistics such as invocation count and duration for chosen methods. jdk.MethodTrace records individual invocations with a stack trace.
It works through bytecode instrumentation. You pick methods with a filter when starting the recording, with no change to application code.
java -XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=trace.jfr -jar app.jar
This helps when sampling is too coarse and you need exact answers such as how often a method ran and who called it.
Timing is the cheaper option when you only need totals and averages. Tracing costs more per call, so keep the filter narrow, and do not trace hot methods in production unless you must. Results show up in JDK Mission Control or through the jfr command line tool.
Take quiz
jdk.CPUTimeSample
jdk.MethodTrace
jdk.MethodTiming
jdk.GCHeapSummary
By calling System.profile()
By editing the source with annotations only
With a filter when starting the recording
By rebuilding the JDK
25. What is JFR cooperative sampling?
JFR cooperative sampling (JEP 518) changes how JDK Flight Recorder samples Java thread stacks. Stacks are now walked only at safepoints, while the design tries to keep the safepoint bias small.
The old sampler suspended a thread at an arbitrary point and tried to walk a stack that might be in an inconsistent state. That could produce crashes and unreliable data under heavy load.
The new approach is more stable and scales better with many threads, including large numbers of virtual threads. The trade-off is that sample positions are tied to safepoint locations.
Take quiz
Only inside native code
At arbitrary instruction boundaries
Only at safepoints
Only when the heap is full
Missing GC logs
Slow class loading
Large Base64 encodings
Stability when sampling asynchronously
26. How do you run a compact source file in Java 25?
You launch it in source-file mode: java Hello.java. The launcher compiles the file in memory, then looks for a main method to run.
With compact source files, the launcher picks the first match from this list:
static void main(String[] args)static void main()void main(String[] args)(instance method)void main()(instance method)
For an instance main, the launcher creates an object using a non-private no-argument constructor, then calls the method. The method need not be public.
You can also compile with javac Hello.java and run java Hello. The implicit class takes the file name, so a file called Hello.java gives a class called Hello.
Take quiz
java -source Hello
jshell Hello.java -run
javac Hello.java only
java Hello.java
A non-private no-argument constructor in its class
An @EntryPoint annotation
A main(String[]) signature only
A public static modifier
27. How do you resolve ambiguity with module imports?
Two modules can export packages that contain a class with the same simple name. If you import both on demand and use that name, the compiler reports an ambiguity error.
A well-known case is Date: java.util.Date from java.base and java.sql.Date from java.sql.
import module java.base; import module java.sql; Date d; // error: reference to Date is ambiguous
The fix is a single-type import, which shadows any on-demand import:
import module java.base; import module java.sql; import java.util.Date; Date d; // java.util.Date
You can also use the fully qualified name. The same rule applies to List when java.desktop brings in java.awt.List.
Take quiz
A single-type import of the class
A comment above the declaration
Another import module line
Adding module-info.java
java.lang.String and java.sql.String
java.util.Date and java.sql.Date
java.io.File and java.sql.File
java.util.Map and java.sql.Map
28. How does the constructor prologue differ from the epilogue?
A constructor with an explicit super(...) or this(...) call has two parts. The prologue is everything before that call. The epilogue is everything after it.
| Prologue | Epilogue |
| Runs before the superclass is initialized | Runs after the superclass constructor finishes |
| Cannot read this, instance fields or call instance methods | Full access to this |
| Can validate and compute arguments | Can use the fully built superclass state |
| Can assign fields declared in the same class | Can do any further initialization |
The prologue rules protect you from using an object whose superclass part does not exist yet. If a constructor has no explicit call, the compiler inserts super() at the start and the whole body is the epilogue.
Take quiz
The epilogue
The prologue
The finalizer
The static block of the subclass
Calling static methods
Declaring local variables
Reading instance fields of the object being built
Throwing an exception
29. Why do early field assignments matter in constructors?
A superclass constructor may call a method that a subclass overrides. That override runs while the subclass fields still hold default values, because the subclass constructor body has not started yet.
class Base { Base() { show(); } void show() { } } class Child extends Base { private final String label; Child(String label) { this.label = label; // allowed in the prologue super(); } @Override void show() { IO.println(label); } }
In Java 25 the field is assigned first, so show() prints the real value, not null. Before JEP 513 this took tricks such as a static helper or two-phase setup.
Take quiz
A NullPointerException from the compiler
A compile error
The default value of the subclass field
The final value always
Fields of any loaded class
Only static fields
Fields of the superclass
Fields declared in the same class
30. How do scoped values differ from ThreadLocal?
Both pass data without extra parameters, but they solve it differently. ThreadLocal is a mutable, per-thread variable that lasts until you remove it. ScopedValue is immutable and bound only for the duration of a call.
| ThreadLocal | ScopedValue |
| Mutable with set() | Immutable, rebind in a nested scope |
| Lives until remove() or thread end | Lives only inside run/call |
| Copied to child threads with inheritable variant | Shared with structured-concurrency children without copying |
| Can leak data in thread pools | Cannot leak after the scope exits |
Scoped values suit virtual threads well because there are no per-thread copies to allocate. Use ThreadLocal only when you truly need mutable per-thread state.
Take quiz
Both of them
ThreadLocal
Neither of them
ScopedValue
The value stays on the pooled thread until removed
It is copied to all threads
It is deleted at once after each task
It is stored in the class file
31. How do scoped values propagate to child threads?
Scoped value bindings are inherited by subtasks forked through a StructuredTaskScope. Plain new Thread() or an ExecutorService does not inherit them.
ScopedValue.where(USER, "alice").run(() -> { try (var scope = StructuredTaskScope.open()) { var t = scope.fork(() -> USER.get()); // sees "alice" scope.join(); } });
The forked subtask shares the same binding, with no copy. This is safe because the scope guarantees the children finish before the parent's binding ends.
Outside structured concurrency, pass the value explicitly or rebind it in the new thread.
Rebinding also nests: inside the scope you can bind the same ScopedValue to a new value with another where(...).run(...), and the inner code sees the new one. When the inner call returns, the outer binding becomes visible again.
Take quiz
Subtasks forked in a StructuredTaskScope
Only the main thread
Threads of any ExecutorService
Every thread in the JVM
The JVM locks the value
Children finish before the binding scope ends
The value is serialized to each child
Children run on the same carrier thread
32. How does the StructuredTaskScope API change in Java 25?
The fifth preview redesigns how you create scopes. Instead of new StructuredTaskScope.ShutdownOnFailure(), you call the static factory StructuredTaskScope.open().
Policy moves into a Joiner that you pass to open. Ready-made joiners include awaitAllSuccessfulOrThrow() (the default), anySuccessfulResultOrThrow() and awaitAll().
try (var scope = StructuredTaskScope.open()) { Subtask<String> user = scope.fork(() -> findUser()); Subtask<Integer> orders = scope.fork(() -> countOrders()); scope.join(); return new Profile(user.get(), orders.get()); }
join() waits per the joiner's rule and throws if the policy fails. Both fork and join must be called by the thread that opened the scope.
Another change is that you can set a name, a timeout or a thread factory through a Configuration function passed to open. Those options were previously spread across subclasses and constructors, which made them harder to find and to combine.
Take quiz
Executors.newScope()
StructuredTaskScope.open()
new StructuredTaskScope().start()
ThreadScope.create()
In module-info.java
In a subclass of StructuredTaskScope
In a Joiner passed to open
In a system property
33. When should you use Joiner.anySuccessfulResultOrThrow?
Use it when you only need the first successful result from several tasks that do the same job, such as querying replicas or mirrors. When one subtask succeeds, the scope cancels the rest.
try (var scope = StructuredTaskScope.open( StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow())) { scope.fork(() -> fetchFrom("mirror-a")); scope.fork(() -> fetchFrom("mirror-b")); scope.fork(() -> fetchFrom("mirror-c")); String fastest = scope.join(); }
Here join() returns the result directly. If every subtask fails, it throws an exception. Choose awaitAllSuccessfulOrThrow() instead when you need every result.
Typical examples are hedged requests to several service instances, trying multiple data centers, or racing a cache lookup against a database. Make sure the tasks are safe to cancel, because the losing ones are interrupted as soon as a winner is found.
Take quiz
Always null
The slowest result
The result of the first subtask that succeeds
A list of all results
Their results are merged
They are restarted
They keep running after the scope closes
They are cancelled
34. What happens when a subtask fails in a StructuredTaskScope?
With the default joiner, awaitAllSuccessfulOrThrow(), the first failure cancels the scope. The remaining subtasks are interrupted, and join() throws a StructuredTaskScope.FailedException with the original exception as the cause.
sequenceDiagram participant P as Parent thread participant A as Subtask A participant B as Subtask B P->>A: fork P->>B: fork A-->>P: fails P->>B: cancel (interrupt) P->>P: join() throws FailedException
Calling Subtask.get() on a failed or unfinished subtask throws IllegalStateException. Read results only after join() returns normally.
Closing the scope, which the try-with-resources block does, waits until every subtask has actually stopped. Because of this, no thread outlives its parent block, and a failure cannot leave stray work running in the background.
Take quiz
TimeoutException from the pool
ExecutionException from Future
No exception, it returns null
StructuredTaskScope.FailedException
They are cancelled
They always complete normally
They move to a new scope
They are restarted
35. How does StableValue lazily initialize a value?
The usual entry point is orElseSet(Supplier). If the value is not yet set, the supplier runs, its result is stored, and every caller gets the same object.
The state is thread-safe: if two threads race, only one result wins, and both see it. The core methods are of(), trySet(v), orElseSet(supplier), isSet() and orElseThrow().
There are also factory methods that return lazy views:
StableValue.supplier(...)for a lazily computed single valueStableValue.list(size, mapper)for a lazily filled listStableValue.map(keys, mapper)for a lazily filled mapStableValue.function(...)andintFunction(...)for computed-once lookups
It is a preview API, so remember --enable-preview.
Take quiz
Returns the stored value without calling the supplier
Calls the supplier again
Throws IllegalStateException
Replaces the value
StableValue.collect()
StableValue.list(size, mapper)
StableValue.array()
List.lazy()
36. Why use StableValue over double-checked locking?
Double-checked locking needs a volatile field, a synchronized block and careful ordering. It is easy to get wrong, and the field cannot be final, so the JIT cannot treat it as a constant.
// Double-checked locking private volatile Logger logger; Logger logger() { Logger l = logger; if (l == null) { synchronized (this) { if ((l = logger) == null) logger = l = create(); } } return l; } // StableValue private final StableValue<Logger> logger = StableValue.of(); Logger logger() { return logger.orElseSet(this::create); }
StableValue removes the boilerplate and the memory-model risk. After the value is set, the JVM can constant-fold the field in many cases, so access is as fast as a normal final.
Take quiz
It disables inlining
The set value can be treated as a constant
It forces interpretation
It keeps values off the heap
It cannot be used with strings
It is removed from the JDK
It needs volatile and careful ordering
It always deadlocks
37. How do primitive patterns work in switch?
In Java 25 preview, switch accepts long, float, double and boolean selectors, and primitive type patterns can appear in case labels.
boolean ok = response.isSuccess(); String label = switch (ok) { case true -> "success"; case false -> "failure"; }; switch (statusCode) { case int i when i >= 500 -> IO.println("server error"); case int i when i >= 400 -> IO.println("client error"); case int i -> IO.println("other: " + i); }
The switch must be exhaustive. A boolean switch with both true and false needs no default.
It needs --enable-preview.
Guards with when work as with any other pattern. Dominance rules apply too, so a broad case int i placed before a narrower guarded case would be a compile error. Remember that this is still a preview feature in Java 25.
Take quiz
Only enums
Only Object
boolean
Only int
When it covers true
Never, default is required
When it has a single case
When it covers both true and false
38. What is the difference between a primitive instanceof check and a cast?
A cast always converts the value, even if information is lost. A primitive instanceof pattern converts only when the conversion is exact, and tells you whether it worked.
int big = 300; byte cast = (byte) big; // 44, silently truncated if (big instanceof byte b) { // false, 300 does not fit use(b); }
| Cast | instanceof pattern |
| Always succeeds | Succeeds only if lossless |
| May truncate or round silently | Returns false instead |
| No branch needed | Binds a variable only on match |
It replaces manual range checks such as x >= Byte.MIN_VALUE && x <= Byte.MAX_VALUE. It is still a preview feature in Java 25.
Take quiz
It throws an exception
true, with b = 44
true, with b = 300
false
No silent loss of information
It runs without a JVM
It works on null values
It changes the int type to long
39. How do you derive an AES key with HKDF in Java 25?
Create a KDF for HKDF, build parameters with the extract and expand steps, then call deriveKey.
KDF hkdf = KDF.getInstance("HKDF-SHA256"); AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract() .addIKM(initialKey) // input key material .addSalt(salt) .thenExpand(info, 32); // context info, 32 bytes out SecretKey aesKey = hkdf.deriveKey("AES", params);
Extract mixes the input key material and salt into a fixed-size pseudo-random key. Expand stretches it to the length you need, bound to the info context. Use a different info value for each purpose, so one secret yields independent keys.
If the input key material is already a strong, uniformly random key (for example a shared secret from a key exchange), you can skip the salt or use a fixed value. Never use HKDF directly on a low-entropy password. Use a password hashing function for that.
Take quiz
Produces output key material of the requested length
Stores the key in a keystore
Encrypts the salt
Hashes the password with bcrypt
To make the salt shorter
So one secret yields independent derived keys
To skip the extract step
To enable preview mode
40. How do you encode and decode keys as PEM in Java 25?
Use PEMEncoder to write and PEMDecoder to read. Both are created with of().
String pem = PEMEncoder.of().encodeToString(privateKey); PrivateKey key = PEMDecoder.of().decode(pem, PrivateKey.class); Certificate cert = PEMDecoder.of().decode(certPem, Certificate.class);
To protect a private key with a password, use an encrypting encoder:
String encrypted = PEMEncoder.of() .withEncryption(password) // char[] .encodeToString(privateKey);
If you do not know the type in advance, decode(String) returns a DEREncodable, which can be a key, certificate or a raw PEMRecord for unknown labels. The API is in preview, so use --enable-preview.
The decoder can also read a whole file with several blocks, one block at a time, from an InputStream. Wrong password or damaged input raises an exception, so wrap the call in a try block when you process untrusted files.
Take quiz
setPassword()
withEncryption(password)
encodeSecure()
lockWith(password)
A byte array only
Always a String
A DEREncodable
Always a PublicKey
41. How do you enable compact object headers?
Pass the JVM flag -XX:+UseCompactObjectHeaders. In Java 25 it is a product flag, so you no longer need -XX:+UnlockExperimentalVMOptions as you did in Java 24.
java -XX:+UseCompactObjectHeaders -jar app.jar
It is off by default, so existing deployments are unaffected. Test it first: compare heap usage, GC time and throughput against your baseline.
Agents or tools that rely on the old header layout may break, for example code that inspects object internals. Standard Java code is unaffected.
Because the class pointer shrinks, there is a limit on the number of loaded classes the compact layout can address, about 4 million, which is far above what normal applications load. Rollout tip: enable it on one instance first, then watch the heap and latency dashboards.
Take quiz
-Xcompact:headers
-XX:HeaderSize=8
-XX:+UseCompactObjectHeaders
-XX:+SmallHeaders
Yes, on all platforms
Only in debug builds
Yes, but only with G1
No
42. How do you enable Generational Shenandoah?
Select the Shenandoah collector, then set its mode to generational:
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -jar app.jar
In Java 24 this also required -XX:+UnlockExperimentalVMOptions. Since Java 25 it is a product feature, so that flag is gone.
The default Shenandoah mode stays non-generational. Run a load test before switching, and compare pause times, throughput and memory overhead. Workloads that create many short-lived objects usually benefit most.
Pair it with a sensible heap size using -Xms and -Xmx, and check the GC log with -Xlog:gc to confirm young collections are happening. If your app has mostly long-lived data and little garbage, the generational mode may bring little benefit.
Take quiz
ShenandoahGCMode=young
UseGenerationalShenandoah=true
ShenandoahGCMode=split
ShenandoahGCMode=generational
The experimental unlock flag is no longer needed
It became the default
It now needs a license key
The feature was removed
43. How do you profile CPU time with JFR in Java 25?
Enable the experimental jdk.CPUTimeSample event when you start a recording. This works on Linux only.
java -XX:StartFlightRecording:jdk.CPUTimeSample#enabled=true,filename=cpu.jfr -jar app.jar jfr print --events jdk.CPUTimeSample cpu.jfr
You can also open cpu.jfr in JDK Mission Control to see hot methods and flame graphs.
The event reports CPU time spent by the thread, including time in native code. If a thread is blocked on I/O it burns no CPU, so it will not show up. Use it alongside the wall-clock ExecutionSample to separate CPU work from waiting.
The sampling interval is controlled by the event's throttle setting, so a lower rate means less overhead. Start with the default, look at the hottest methods, and only then raise the rate if you need more detail. Remember that the feature is experimental and may change.
Take quiz
jdk.CPUTimeSample
jdk.NativeSample
jdk.ExecutionSample only
jdk.CPULoad
I/O always runs in native code
It uses no CPU while it waits
JFR ignores all blocked threads
The sampler skips virtual threads
44. How do you create an AOT cache in Java 25?
Run the application once with -XX:AOTCacheOutput to record and create the cache, then start it with -XX:AOTCache.
# 1. training run: creates app.aot when the JVM exits java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App # 2. normal runs java -XX:AOTCache=app.aot -cp app.jar com.example.App
flowchart LR A["Training run"] --> B["app.aot written at exit"] B --> C["Production run with AOTCache"] C --> D["Faster startup"]
The training run should exercise typical code paths, so the right classes and method profiles are captured. The cache is tied to the same JDK build and classpath. If they change, rebuild it.
Do the training run on the same hardware class as production if possible, using a realistic but short workload, such as startup plus a few key requests. For a long-running server you can stop it with a normal shutdown, so the JVM gets a chance to write the cache file.
Take quiz
-XX:AOTCache=app.aot
-XX:AOTCacheOutput=app.aot
-XX:+CreateAOT
-Xshare:off
Only on Windows
Never, it is permanent
When the JDK build or classpath changes
After every request to the app
45. Why was the 32-bit x86 port removed?
JEP 503 removed the Linux x86-32 port from the JDK. The Windows 32-bit x86 port had already been removed in Java 24 (JEP 479).
The main reasons are cost and age. The Windows 10 end of support, in October 2025, was the last major platform for 32-bit x86, and Linux distributions had been dropping 32-bit x86 support. Meanwhile the port needed special fallbacks for new features such as virtual threads and the FFM API, which slowed development.
Removing it lets the team simplify the code and spend effort on 64-bit platforms. Only the 32-bit x86 port is affected. If you must run 32-bit x86, you have to stay on an older JDK.
Take quiz
The 64-bit x86 port
All ARM ports
The Linux 32-bit x86 port
The Windows 64-bit port
Java 21
Java 25
Java 17
Java 24
46. Why is the Vector API still incubating?
The Vector API has been in incubation since Java 16, and Java 25 is its tenth round. The team is waiting for Project Valhalla (value classes) before finalizing it.
Vector types like IntVector are meant to behave like values, with no identity, and the JIT can then keep them in CPU registers without allocating objects. Today they are ordinary heap objects, so the compiler relies on heavy escape-analysis work to avoid allocation.
Because the API depends on that language change, finalizing it early would lock in a design that is hard to optimize. Meanwhile, each incubator round keeps the API stable enough to use. You need --add-modules jdk.incubator.vector, and no preview flag.
Take quiz
GPU support in the JDK
A new garbage collector
Removal of JNI
Project Valhalla value classes
Add the jdk.incubator.vector module
Import java.util.vector
No flags are needed
Pass --enable-preview only
47. Which Java 25 features still need --enable-preview?
Several Java 25 features are not final. Preview features need --enable-preview at compile and run time. The Vector API is an incubator module and uses --add-modules instead.
| Feature | JEP | Status in Java 25 |
| Stable Values | 502 | Preview |
| Structured Concurrency | 505 | Fifth preview |
| Primitive types in patterns | 507 | Third preview |
| PEM encodings | 470 | Preview |
| Vector API | 508 | Tenth incubator |
| Scoped Values | 506 | Final |
| Module imports, compact source files, flexible constructors | 511, 512, 513 | Final |
javac --release 25 --enable-preview Main.java java --enable-preview Main
Class files compiled with preview features only run on the same Java version.
A good rule: use preview features in experiments, internal tools and learning projects, and give feedback to the OpenJDK team. For production libraries, wait until the API is final, because a preview API can change or disappear in the next release.
Take quiz
Structured concurrency
Scoped values
Flexible constructor bodies
Module import declarations
--add-exports java.base
--add-modules jdk.incubator.vector
-XX:+UseVectors
--enable-vector
48. What is the difference between Java 21 and Java 25?
Both are LTS releases. Moving from 21 to 25 gives you everything finalized across Java 22, 23, 24 and 25.
| Area | New since Java 21 |
| Language | Unnamed variables (22), flexible constructor bodies (25), compact source files and module imports (25) |
| APIs | FFM API (22), Stream Gatherers (24), Class-File API (24), scoped values (25), KDF API (25) |
| Runtime | Compact object headers (25), Generational Shenandoah (25), generational ZGC by default (23) |
| Startup | AOT class loading and linking (24), simpler AOT flags and method profiles (25) |
| Tooling | JFR CPU-time sampling, method timing and tracing (25) |
| Removed | Windows and Linux 32-bit x86 ports |
Virtual threads, record patterns, pattern matching for switch and sequenced collections are already final in Java 21, so they are not new in 25.
Take quiz
Compact object headers
Virtual threads
Scoped values
Stream Gatherers
Flexible constructor bodies
Compact source files
Stream Gatherers
Module import declarations
49. How do you migrate from Java 21 to Java 25?
Treat it as a staged upgrade, not a single jump.
- Upgrade your build tools and plugins (Maven, Gradle, compiler and test plugins) to versions that support Java 25.
- Build and run tests on JDK 25 with your current
--release 21to separate runtime from language changes. - Fix warnings: check for removed or restricted APIs, JNI usage and
sun.misc.Unsafememory access, which now warns. - Update frameworks, bytecode libraries and agents, since class-file versions have changed.
- Check GC and memory flags. Compare behavior before using the new options.
- Raise
--releaseto 25 and adopt new features gradually.
Watch for the removal of the security manager (permanently disabled in Java 24), and any 32-bit x86 deployment. Always read the release notes for each version between 21 and 25.
Take quiz
The operating system kernel only
Only the application's logo
Build tools and plugins that support the new JDK
Nothing, JDK 25 is drop-in
It converts bytecode to Java 25 automatically
It disables the JIT compiler
It skips unit tests
To separate runtime changes from language changes
50. How can you reduce startup time and memory using Java 25?
Java 25 gives you three levers that you can combine, each one switched on with a flag.
| Goal | Feature | Flag |
| Faster startup | AOT cache with method profiles | -XX:AOTCacheOutput / -XX:AOTCache |
| Less heap | Compact object headers | -XX:+UseCompactObjectHeaders |
| Lower pauses and overhead | Generational Shenandoah | -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational |
flowchart LR A["Measure baseline"] --> B["Add AOT cache"] B --> C["Try compact headers"] C --> D["Test GC mode"] D --> E["Compare startup, heap, pauses"]
Change one thing at a time and compare against the baseline. The AOT cache is the biggest win for short-lived processes, and compact headers help most when the heap is full of small objects.