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
ThreadLocalfor 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.
Compact Source Files & Instance Main Methods
✅ Key JEPs
- JEP 445: Unnamed Classes and Instance Main Methods (Preview, Java 21)
- JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview, Java 22)
- JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview, Java 23)
- JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview, Java 24)
- JEP 512: Compact Source Files and Instance Main Methods (Final, Java 25)
🔍 What’s New
JEP 512 finalizes features that simplify writing small Java programs by removing boilerplate and allowing more natural entry points. It combines two ideas:
- 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.basemodule are automatically imported.
-
Instance main methods — Java can now start programs from instance methods (like
void main()orvoid main(String[] args)), not only frompublic 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.
💡 Why It matters
- 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
.javafiles.
⚠️ Considerations
- Compact files can’t define package statements or named classes.
- These files must have exactly one top-level declared class.
-
IOmethods are not implicitly imported; you must qualify them asIO.println()or useimport static IO.*. - IDE and build tool support may vary during early adoption.
Primitive types in patterns, instanceof, and switch
✅ Key JEPs
- JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview, Java 23)
- JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview, Java 24)
-
JEP 507: Primitive Types in Patterns,
instanceof, andswitch(Third Preview, Java 25)
🔍 What’s new
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 iorcase int iinswitch. - Extending
instanceofso that it can test a value for conversion to a primitive type safely: e.g.,if (i instanceof byte b) { … }means “ifican safely convert tobytewithout loss”. - Extending
switchso that the selector and case labels may be any primitive type, andcasepatterns can bind primitives: e.g.,
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.
💡 Why it matters
- Improves uniformity and expressiveness: Previously Java’s pattern matching,
instanceof, andswitchwere 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
switchflexibility: 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.
⚠️ Considerations
-
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.
Scoped Values (Preview)
✅ Key JEPs
- JEP 446: Scoped Values (Preview, Java 21)
- JEP 464: Scoped Values (Second Preview, Java 22)
- JEP 481: Scoped Values (Third Preview, Java 23)
- JEP 487: Scoped Values (Fourth Preview, Java 24)
- JEP 506: Scoped Values (Final, Java 25)
🔍 What’s new
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 likeScopedValue.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 andV.get()becomes invalid (or must be checked). - Scoped values are preferred over
ThreadLocalfor many use-cases: they require immutable data, allow sharing across virtual threads efficiently, and trace the lifetime of the binding in the code structure.
Code sample:
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
}
💡 Why it matters
- 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.
⚠️ Considerations
- 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 (orisBound()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.
ZGC: Low-latency garbage collector
✅ Key JEPs
- JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental, Java 11)
- JEP 351: ZGC: Uncommit Unused Memory (Experimental, Java 13)
- JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production, Java 15)
- JEP 439: Generational ZGC (Final, Java 21)
- JEP 474: ZGC: Generational Mode by Default (Java 23)
- JEP 490: ZGC: Remove the Non-Generational Mode (Java 24)
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.
🔍 What it does
- 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:
java -XX:+UseZGC -Xmx8g
💡 Why it matters
- Ideal for latency-sensitive systems (finance, real-time analytics).
- Scales efficiently to very large heaps.
- Requires minimal tuning compared to older collectors.
⚠️ Considerations
- Slightly higher memory overhead due to concurrent design.
- Throughput may be a bit lower than G1 in CPU-bound tasks.
Ahead-of-time class loading & linking
✅ Key Jeps
- JEP 483: Ahead-of-Time Class Loading & Linking (Final, Java 24)
🔍 What the JEP does
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:
- You run your application once (a “training run”) to record which classes are loaded and linked.
- You create an AOT cache (archive) containing that information.
- On the next startup, the JVM reuses that cache, skipping much of the load/link process to start faster.
💡 Why it matters
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.
⚙️ How it works (high Level)
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:
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar
This records which classes are loaded and linked.
- Cache creation phase:
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar
This generates the AOT cache file app.aot.
- Production run:
java -XX:AOTCache=app.aot -cp app.jar
The JVM uses the cache to start up faster. If the cache isn’t valid, it gracefully falls back to normal startup.
⚠️ Considerations
- 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.
Conclusion
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.
Top comments (0)