Welcome back to our series exploring the latest innovations in Java!
In Part 1, we covered foundational features like pattern matching and records. Now we turn our attention to features that dramatically improve developer experience and application performance.
This installment covers five game-changing additions to modern Java:
- Compact Source Files & Instance Main Methods — Write Java programs without boilerplate, making the language more accessible to beginners and perfect for scripting
- Primitive Types in Patterns — Extend Java's pattern matching capabilities to work seamlessly with primitive types
- Scoped Values — A modern, efficient alternative to ThreadLocal for passing context in concurrent applications
- ZGC — A production-ready garbage collector that keeps pause times under 1ms, even with massive heaps
- AOT Class Loading — Dramatically reduce startup times by caching class loading work
Whether you're building microservices that need instant startup, teaching Java to newcomers, or running latency-sensitive applications, these features offer practical solutions to real-world challenges.
Let's dive in.

- JEP 445: Unnamed Classes and Instance Main Methods (Preview)
- JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
- JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
- JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview)
- JEP 512: Compact Source Files and Instance Main Methods
JEP 512 finalizes features that simplify writing small Java programs by removing boilerplate and allowing more natural entry points. It combines two ideas:
1. Compact source files — Java files that omit explicit class declarations.
* Methods and fields can appear directly at the top level.
* The compiler implicitly wraps them in a generated class.
* All public types from the java.base module are automatically imported.
2. Instance main methods — Java can now start programs from instance methods (like void main() or void main(String[] args)), not only from public static void main(...).
* The launcher instantiates the implicit class and calls the method automatically.
A new helper class, java.lang.IO, provides simple console I/O utilities such as IO.println() and IO.readln().
void main() {
IO.println("Hello, world!");
}This code compiles and runs directly — no class, no static, no public.
• Makes Java friendlier for beginners and small scripts.
• Reduces boilerplate for quick demos, exercises, or tools.
• Preserves full compatibility: compact source files are still standard .java files.
• Compact files can’t define package statements or named classes.
• These files must have exactly one top-level declared class.
• IO methods are not implicitly imported; you must qualify them as IO.println() or use import static IO.*.
• IDE and build tool support may vary during early adoption.
- JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)
- JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview)
- JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview, Java 25)
This JEP expands the pattern-matching, instanceof, and switch capabilities of Java to primitive types (such as int, long, double, boolean) — not only reference types.
Key changes include:
• Allowing primitive type patterns in both nested and top-level pattern contexts, e.g., x instanceof int i or case int i in switch.
• Extending instanceof so that it can test a value for conversion to a primitive type safely: e.g., if (i instanceof byte b) means “if i can safely convert to byte without loss”.
• Extending switch so that the selector and case labels may be any primitive type, and case patterns can bind primitives. For example:
switch (v) {
case int i -> System.out.println("int value: " + i);
case double d -> System.out.println("double value: " + d);
}
• Aligning pattern matching uniformly across types: whether reference or primitive, you can decompose and test with patterns.
• Improves uniformity and expressiveness: previously Java’s pattern matching, instanceof, and switch were limited or uneven when it came to primitives versus reference types. Now primitives join the party.
• Reduces boilerplate and error-prone code for primitive conversions and range checking. For example, the old pattern:
int i = …;
if (i >= Byte.MIN_VALUE && i <= Byte.MAX_VALUE) {
byte b = (byte) i;
…
}
This can now become:
if (i instanceof byte b) {
…
}* Enhances switch flexibility: You can switch on long, float, double, boolean, etc., and use pattern guards and binding for primitives, making certain control-flow clearer and more powerful.
* Preview feature: This capability is still under preview in Java 25, meaning the specification is finalized for now but may still change and requires --enable-preview.
* Safety & conversions: The feature does not introduce new implicit conversions that lose information. Patterns only match when the conversion is safe (i.e., no information loss). Developers still need to understand when a primitive value can fit into a target type.
* Tooling / backwards compatibility: Because this is preview, IDE support, build tool integration, and migration for large codebases may lag or require extra configuration.
* Readability for teams: While this adds expressive power, over-use in contexts not suited to pattern matching (especially primitives) might reduce clarity for developers used to more explicit code.
This feature brings Java's type system and pattern matching into better alignment, making primitive types first-class citizens in modern control flow constructs.
- JEP 446: Scoped Values (Preview)
- JEP 464: Scoped Values (Second Preview)
- JEP 481: Scoped Values (Third Preview)
- JEP 487: Scoped Values (Fourth Preview)
The JEP introduces the concept of scoped values — immutable values that can be bound to a thread (and its descendant tasks) for a well-defined lifetime, allowing code deep in a call chain to access context without explicitly passing parameters.
Key characteristics:
* A scoped value is created via ScopedValue.newInstance() and bound for execution via something like ScopedValue.where(USER, value).run(…).
* Inside that dynamic scope, any method — even far down the stack or running in a child thread (via structured concurrency) — can call V.get() and retrieve the bound value.
* The binding has a bounded lifetime: once run(...) completes, the binding ends and V.get() becomes invalid (or must be checked).
* Scoped values are preferred over ThreadLocal for many use-cases: they require immutable data, allow sharing across virtual threads efficiently, and trace the lifetime of the binding in the code structure.
static final ScopedValue<String> USER = ScopedValue.newInstance();
void handleRequest(Request req) {
String userId = /* extract user id from req */;
ScopedValue.where(USER, userId)
.run(() -> processRequest(req));
}
void processRequest(Request req) {
System.out.println("Current user: " + USER.get());
// deeper calls can also see USER.get() without passing userId explicitly
}* Cleaner argument passing: Instead of threading contextual data (like user IDs, request-context, tracing IDs) through many method parameters, scoped values let you bind once and let nested methods access when needed.
* Better suitability for many threads (including virtual threads): Because the data is immutable and the lifetime is bounded, there’s far less memory overhead versus thread-locals in large-scale concurrent systems.
* Improved reasoning: The dynamic scope is explicit in the code (via the where().run() structure), so it’s easier to understand when the data is valid, and when it is not. This contrasts with thread-locals that may leak or remain bound beyond desired lifetimes.
* As a preview feature, the API or semantics may change in future releases.
* Scoped values are immutable once bound; they are not a replacement for thread-locals in cases where you need mutable per-thread state or long-lived caching.
* Code needs to ensure that get() is only called within a valid bound scope; outside that, an exception may be thrown (or isBound() should be checked).
* Tooling, frameworks, and IDE support may lag since this is in preview; plus, migrating from existing thread-local heavy code will need careful design.
* Scopes propagate to child threads only when using compatible concurrency constructs (such as via the preview structured concurrency features) — simple new Threads might not inherit the binding automatically unless explicitly supported.
- JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)
- JEP 351: ZGC: Uncommit Unused Memory (Experimental)
- JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production)
- JEP 474: ZGC: Generational Mode by Default
- JEP 490: ZGC: Remove the Non-Generational Mode
ZGC (Z Garbage Collector) is a low-latency, concurrent GC designed to keep pause times below 1 ms, regardless of heap size. It supports heaps from MBs to multi-terabyte ranges and is fully production-ready since JDK 15.
* Performs most GC work concurrently with the application.
* Uses colored pointers and load barriers to relocate objects without long stop-the-world pauses.
* From JDK 21, supports Generational ZGC (JEP 439) for better throughput in allocation-heavy workloads.
Enable it via:
bash
java -XX:+UseZGC -Xmx8g* Slightly higher memory overhead due to concurrent design.
* Throughput may be a bit lower than G1 in CPU-bound tasks.
- JEP 483: Ahead-of-Time Class Loading & Linking
JEP 483 introduces a JVM feature that allows Java applications to load and link classes ahead of time, then store that state in a cache for later runs. The goal is to reduce startup time by shifting part of the class-loading work from runtime to a prior “training” phase.
Essentially:
1. You run your application once (a “training run”) to record which classes are loaded and linked.
2. You create an AOT cache (archive) containing that information.
3. On the next startup, the JVM reuses that cache, skipping much of the load/link process to start faster.
Applications, especially large frameworks like Spring, spend noticeable time at startup loading and linking classes. By caching this work, the JVM can cut startup time dramatically — benchmarks show up to around 40% improvement in some cases.
Because this mechanism works without changing your code, it’s easy to adopt in existing projects. It also supports Java’s broader effort to improve startup and footprint, especially for cloud-native and serverless workloads.
JEP 483 makes it possible for Java to load and link classes ahead of time, caching that work for faster subsequent startups. It’s a simple yet powerful feature for improving startup performance in modern Java applications — particularly useful for microservices, short-lived workloads, and cloud deployments. Here is how it works (you need to have the jar file of your app with the Manifest file):
* Training phase:
bash
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jarThis records which classes are loaded and linked.
* Cache creation phase:
bash
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jarThis generates the AOT cache file app.aot.
* Production run:
bash
java -XX:AOTCache=app.aot -cp app.jarThe JVM uses the cache to start up faster. If the cache isn’t valid, it gracefully falls back to normal startup.
* The classpath and module configuration during the training phase must match those used later.
* The optimization targets startup performance, not runtime throughput.
* The training and caching workflow adds a bit of setup overhead, especially in containerized or CI/CD environments.
The features covered in this article reflect Java's commitment to staying relevant in a rapidly changing software landscape. From compact source files that make Java approachable for beginners and scripters, to ZGC delivering microsecond-level pause times for demanding production workloads—these aren't just incremental improvements. They represent fundamental shifts in how we can use Java.
Java continues to evolve rapidly, balancing its enterprise heritage with modern development needs. Whether you're building cloud-native microservices, teaching programming fundamentals, or optimizing high-frequency trading systems, these features provide concrete solutions to real problems.