Feature Freeze for JDK 23: What Will the New Edition Bring? - JVM Weekly vol. 86
Today JDK 23 has entered the Rampdown phase - this means we now have the final list of JEPs that will be included in the new release. Let's take a closer look at all of them.
Stable
467: Markdown Documentation Comments
JEP 467 introduces the ability to write JavaDoc documentation comments using Markdown, in addition to the existing HTML format and JavaDoc tags.
What is the problem?: Current JavaDoc documentation comments use HTML and specific JavaDoc tags, which are difficult to write and read. HTML tends to "clutter" the content, discouraging developers from creating well-formatted documentation. Additionally, JavaDoc documentation tags are less known and often require consultation with JavaDoc’s own documentation (which, when you think about it, is somewhat meta).
Solution: JEP 467 introduces support for Markdown in documentation comments, enabling more concise and readable documentation formatting. Markdown is currently a standard among markup languages used by developers, popularized by platforms like GitHub, so the syntax is widely used and known (fun fact - all editions of JVM Weekly are originally written in Markdown). This allows developers to write documentation faster and with less effort, while still being able to use HTML and JavaDoc tags where necessary (many popular Markdown parsers are compatible with HTML inserts).
Code Example:
Traditional JavaDoc comment:
/**
* Returns a <b>hash code</b> value for the object.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Must consistently return the same integer for the same object,
* provided no information used in {@code equals} comparisons is modified.
* <li>If two objects are equal according to the {@link #equals(Object)} method,
* calling {@code hashCode} on each must produce the same result.
* <li>It is not required that unequal objects produce distinct integers,
* but this improves the performance of hash tables.
* </ul>
*
* @implSpec
* The {@code hashCode} method defined by class {@code Object} returns distinct
* integers for distinct objects as far as is practical.
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
The same Markdown comment:
/// Returns a **hash code** value for the object.
///
/// The general contract of `hashCode` is:
///
/// - Must consistently return the same integer for the same object,
/// provided no information used in `equals` comparisons is modified.
/// - If two objects are equal according to the [equals][#equals(Object)] method,
/// calling `hashCode` on each must produce the same result.
/// - It is not required that unequal objects produce distinct integers,
/// but this improves the performance of hash tables.
///
/// @implSpec
/// The `hashCode` method defined by class `Object` returns distinct
/// integers for distinct objects as far as is practical.
///
/// @return a hash code value for this object.
/// @see java.lang.Object#equals(java.lang.Object)
/// @see java.lang.System#identityHashCode
471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
JEP 471 aims to deprecate memory access methods in the sun.misc.Unsafe
class with the intent of removing them in future JDK releases. This proposal encourages developers to migrate to supported alternatives, allowing applications to smoothly transition to newer JDK versions.
What is the problem?: Memory access methods from sun.misc.Unsafe
are dangerous and can lead to undefined behavior, including JVM crashes. Although not intended for wide use, they have become popular among developers seeking higher performance and power than what standard APIs offer. Lack of safety checks before using them can lead to bugs and application crashes.
Solution: JEP 471 deprecates memory access methods in sun.misc.Unsafe
, encouraging developers to transition to safe and efficient alternatives: VarHandle
(JEP 193) and MemorySegment
(JEP 454). The proposal anticipates gradually removing access to sun.misc.Unsafe
methods in several phases, starting with compilation warnings, runtime warnings, and eventually the actual removal of the methods. New command-line options, such as --sun-misc-unsafe-memory-access={allow|warn|debug|deny}
, have been added to enable developers to test and evaluate the impact of deprecating memory access methods.
Code Example:
Example code using sun.misc.Unsafe
:
class Foo {
private static final Unsafe UNSAFE = ...;
private static final long X_OFFSET;
static {
try {
X_OFFSET = UNSAFE.objectFieldOffset(Foo.class.getDeclaredField("x"));
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryToDoubleAtomically() {
int oldValue = x;
return UNSAFE.compareAndSwapInt(this, X_OFFSET, oldValue, oldValue * 2);
}
}
Example code using VarHandle
:
class Foo {
private static final VarHandle X_VH;
static {
try {
X_VH = MethodHandles.lookup().findVarHandle(Foo.class, "x", int.class);
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryAtomicallyDoubleX() {
int oldValue = x;
return X_VH.compareAndSet(this, oldValue, oldValue * 2);
}
}
474: ZGC: Generational Mode by Default
JEP 474 changes the default operating mode of the Z Garbage Collector (ZGC) to generational mode. The current non-generational mode will be marked as deprecated and planned for removal in future JDK releases.
What is the problem?: Maintaining both generational and non-generational modes of ZGC slows down the development of new features. The generational mode of ZGC is considered a better solution for most use cases, rendering the non-generational mode redundant.
How does the proposal solve this problem?: The proposal changes the default value of the ZGenerational
option to true
, thus enabling the generational mode of ZGC by default. After these changes, running the JVM with the -XX:+UseZGC
option will default to the generational mode of ZGC. The non-generational mode will be marked as deprecated, with plans for its eventual removal in the future.
Preview
455: Primitive Types in Patterns, instanceof, and switch (Preview)
JEP 455 introduces pattern matching for primitive types in instanceof
and switch
, allowing the use of these types in nested contexts and at the top-level.
What is the problem?: Current limitations on primitive types in instanceof
and switch
make it difficult to write uniform and expressive code based on them. For example, switch
does not support primitive type patterns, and instanceof
does not support safe casting to primitive types, leading to inconsistent and potentially complicated code.
Solution: JEP 455 enables pattern matching of primitive types in instanceof
and switch
, allowing for safe and expressive casting. This eliminates the need for manual, potentially unsafe casts and allows for more concise code.
Code Example:
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i;
}
if (i instanceof byte b) {
... // use b as byte
}
466: Class-File API (Second Preview)
JEP 466 proposes a standard API for parsing, generating, and transforming Java class files. This is the second preview of the API, which has been improved based on experiences and feedback from the first preview introduced in JEP 457.
What is the problem?: Currently, there are many libraries for handling Java class files, each with its pros and cons. The more rapid evolution of the class file format (due to e.g. ongoing evolution of Valhalla) means these libraries are not always up-to-date with the latest JDK versions, leading to compatibility issues and making it difficult to introduce new features in the JDK.
Solution: JEP 466 introduces a standard API for handling class files that evolves with the class file format. This API offers immutability for class file elements, a tree structure mirroring the class file hierarchy, and user-driven navigation for efficient parsing. It features lazy parsing for better performance, integrates stream and materialized views, and supports transformations through flat mapping operations on streams of elements. By hiding complex implementation details like the constant pool and stack maps, it simplifies interactions and enhances developer productivity.
The Class-File API has a broad scope and must generate classes according to the Java Virtual Machine specification, requiring significant quality and compatibility testing. As usage of ASM in the JDK is replaced by the Class-File API, results will be compared to detect regressions, and extensive performance tests will be conducted.
What changes have been made since the last preview?
Simplified the
CodeBuilder
class by removing medium-level methods that duplicated low-level methods or were rarely used.Enhanced the
ClassSignature
class to more accurately model generic signatures of superclasses and superinterfaces.
Alternatives: An alternative considered was incorporating ASM into the JDK and taking responsibility for its further maintenance, but this was deemed inappropriate. ASM is an old, legacy codebase difficult to evolve, and the design priorities guiding its architecture may not fit today's realities.
Code Example
Parsing class files using patterns:
CodeModel code = ...
Set<ClassDesc> deps = new HashSet<>();
for (CodeElement e : code) {
switch (e) {
case FieldInstruction f -> deps.add(f.owner());
case InvokeInstruction i -> deps.add(i.owner());
// ... and so on for instanceof, cast, etc ...
}
}
Generating class files using builders:
ClassBuilder classBuilder = ...;
classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,
methodBuilder -> methodBuilder.withCode(codeBuilder -> {
Label label1 = codeBuilder.newLabel();
Label label2 = codeBuilder.newLabel();
codeBuilder.iload(1)
.ifeq(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int))
.goto_(label2)
.labelBinding(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))
.labelBinding(label2)
.return_();
});
Transforming class files:
ClassFile cf = ClassFile.of();
ClassModel classModel = cf.parse(bytes);
byte[] newBytes = cf.transform(classModel, (classBuilder, ce) -> {
if (ce instanceof MethodModel mm) {
classBuilder.transformMethod(mm, (methodBuilder, me)-> {
if (me instanceof CodeModel cm) {
methodBuilder.transformCode(cm, (codeBuilder, e) -> {
switch (e) {
case InvokeInstruction i
when i.owner().asInternalName().equals("Foo") ->
codeBuilder.invoke(i.opcode(), ClassDesc.of("Bar"),
i.name().stringValue(),
i.typeSymbol(), i.isInterface());
default -> codeBuilder.with(e);
}
});
} else {
methodBuilder.with(me);
}
});
} else {
classBuilder.with(ce);
}
});
473: Stream Gatherers (Second Preview)
JEP 473 proposes enhancing the Stream API to support custom intermediate operations, allowing for more flexible stream processing.
What is the problem?: The current Stream API offers a fixed set of intermediate operations that do not always enable the execution of more complex tasks. The inability to define custom intermediate operations limits the flexibility and expressiveness of the code, especially for tasks requiring specific data transformation logic.
Solution: JEP 473 introduces a new intermediate operation Stream::gather(Gatherer)
, which allows stream elements to be processed using user-defined gatherers. A gatherer is an instance of the java.util.stream.Gatherer
interface that represents the transformation of stream elements and enables manipulation of streams of infinite size.
What changes have been made since the last preview?: No changes were introduced compared to the previous preview (JEP 461). The goal is to gather additional feedback and experience using this API.
Code Example:
Grouping elements into fixed windows:
var result = Stream.iterate(0, i -> i + 1)
.gather(Gatherers.windowFixed(3))
.limit(2)
.toList();
// result ==> [[0, 1, 2], [3, 4, 5]]
Defining a custom gatherer:
record WindowFixed<TR>(int windowSize)
implements Gatherer<TR, ArrayList<TR>, List<TR>> {
public WindowFixed {
if (windowSize < 1) {
throw new IllegalArgumentException("window size must be positive");
}
}
@Override
public Supplier<ArrayList<TR>> initializer() {
return () -> new ArrayList<>(windowSize);
}
@Override
public Integrator<ArrayList<TR>, TR, List<TR>> integrator() {
return Gatherer.Integrator.ofGreedy((window, element, downstream) -> {
window.add(element);
if (window.size() < windowSize) {
return true;
}
var result = new ArrayList<TR>(window);
window.clear();
return downstream.push(result);
});
}
@Override
public BiConsumer<ArrayList<TR>, Downstream<? super List<TR>>> finisher() {
return (window, downstream) -> {
if (!downstream.isRejecting() && !window.isEmpty()) {
downstream.push(new ArrayList<TR>(window));
window.clear();
}
};
}
}
476: Module Import Declarations (Preview)
JEP 476 introduces the ability to import all packages exported by a module using a new import module
declaration. This facilitates the reuse of modular libraries without requiring developers to modularize their own code.
What is the problem?: Currently, when using modules, developers must manually import many packages, which can be time-consuming and complicated, especially for beginners. The inability to simply import entire modules leads to redundancy and complexity in the code.
How does the proposal solve this problem?: JEP 476 introduces a new import module
declaration, which allows importing all public classes and interfaces from packages exported by a module and modules it transitively requires. This allows developers to more easily and quickly access needed resources, simplifying code and reducing the number of import declarations.
However, there are some drawbacks - using multiple import module
declarations may lead to name conflicts that will be detected only at compile time. Resolving these conflicts may require adding individual type import declarations, which can be cumbersome and difficult to maintain.
Code Example:
Importing an entire module:
import module java.base;
String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
Stream.of(fruits)
.collect(Collectors.toMap(
s -> s.toUpperCase().substring(0,1),
Function.identity()));
Resolving name conflicts:
import module java.base;
import module java.sql;
import java.sql.Date;
Date d = ... // Ok! Date jest rozpoznane jako java.sql.Date
477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
JEP 477 aims to simplify writing first programs in Java by allowing developers to create programs without the need to understand advanced language constructs designed for large programs. This feature introduces the ability to declare implicit classes and instance main methods, allowing for more concise writing of small programs.
What is the problem?: Currently, novice programmers must understand many complex language constructs, such as classes, packages, modules, access, and static modifiers, before they can write a simple program. This can discourage new users and make learning difficult.
Solution: JEP 477 simplifies creating programs by introducing:
Instance main methods: Main methods do not need to be static, public, or take a
String[]
parameter.Implicit classes: Allows writing methods and fields at the top level, automatically included in a hidden class.
Automatic import of I/O methods: Methods for simple text input/output are automatically imported.
Automatic import of the java.base module: All public classes and interfaces of packages exported by the java.base module are automatically imported.
Code Example:
Simple "Hello, World!" program in an implicit class:
void main() {
println("Hello, World!");
}
Interactive program:
void main() {
String name = readln("Please enter your name: ");
print("Pleased to meet you, ");
println(name);
}
480: Structured Concurrency (Third Preview)
JEP 480 introduces an API for structured concurrency, simplifying concurrent programming by treating groups of related tasks as a single "unit of work". Structured concurrency improves error handling and simplifies task cancellation while providing observability of concurrent code.
What is the problem?: The current approach to concurrency, based on ExecutorService and Future, allows for concurrency patterns that are not bound by any logical scope, leading to problems with thread lifecycle management, error handling, and cancellation propagation. This makes the code harder to understand, debug, and maintain.
How does the proposal solve this problem?: JEP 480 introduces an API for structured concurrency, providing hierarchical relationships between tasks and their subtasks, similar to the call stack in standard code. StructuredTaskScope
, the main class of the API, allows developers to group related tasks, manage them as a single unit, and automatically propagate cancellation and handle errors. It also improves observability - tools of this kind can display the task hierarchy, making it easier to diagnose problems.
Code Example:
Using StructuredTaskScope in the handle method:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Dołącz do obu podzadań
.throwIfFailed(); // Propaguj błędy
// Here both subtasks have completed successfully, so compose their results
return new Response(user.get(), order.get());
}
}
481: Scoped Values (Third Preview)
JEP 481 introduces scoped values, which allow a method to share immutable data both with calls within a thread and with child threads.
What is the problem?: Thread-local variables (ThreadLocal) are difficult to manage and suffer from mutability, unbounded lifetime, and costly inheritance. These problems become even more apparent when using a large number of virtual threads.
How does the proposal solve this problem?: JEP 481 introduces scoped values as a safe and efficient way to share data between methods within the same thread and with child threads. They are immutable, and their limited lifetime makes them available only for a specific time during thread execution, simplifying management and improving performance. They can also be inherited by child threads created by StructuredTaskScope
.
Scoped Values are easy to use, offering a simple way to pass data between methods, and they provide transparency, as the shared data's lifetime is clearly visible in the code structure. They are easier to understand than thread-local variables and have lower memory and time costs, especially when used together with virtual threads (JEP 444) and structured concurrency (JEP 480).
Example of using scoped values in a web framework:
class Framework {
private final static ScopedValue<FrameworkContext> CONTEXT
= ScopedValue.newInstance();
void serve(Request request, Response response) {
var context = createContext(request);
ScopedValue.runWhere(CONTEXT, context,
() -> Application.handle(request, response));
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get();
var db = getDBConnection(context);
db.readKey(key);
}
}
In this example, CONTEXT
is a scoped value set in the serve
method and available in the readKey
method.
482: Flexible Constructor Bodies (Second Preview)
JEP 482 introduces the ability to place statements in constructors before calling another constructor (super(..)
or this(..)
) in the Java language. These statements cannot refer to the created instance but can initialize its fields.
What is the problem?: In the Java language, a constructor must begin with a call to another constructor (super(..)
or this(..)
). This limitation prevents placing initialization code before calling the superclass constructor, which can lead to issues with overriding methods and initializing fields.
Solution: JEP 482 introduces changes to the constructor grammar, allowing for statements to be placed before calling the constructor. This code can initialize fields but cannot refer to the created instance. This makes the class more reliable when methods are overridden.
Example of argument verification in the constructor before calling the superclass constructor:
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(long value) {
if (value <= 0) throw new IllegalArgumentException(..);
super(value);
}
}
In this example, the value
argument is verified before calling the superclass constructor BigInteger
.
Changes since the previous Preview: In the second preview (the first one was called JEP 447: Statements before super(...) ) a significant change has been introduced that allows constructors to initialize fields before invoking another constructor (super(..) or this(..)). This new capability ensures that a constructor in a subclass can initialize its fields before a superclass constructor executes, preventing the superclass constructor from encountering default field values (such as 0, false, or null) in the subclass.
This improvement addresses a common issue where the superclass constructor calls methods that are overridden in the subclass and use uninitialized fields, leading to potential bugs and errors.
Incubation
469: Vector API (Eighth Incubator)
JEP 469 extends the API that enables expressing vector computations, compiled to native vector instructions on supported CPU architectures, offering performance superior to equivalent scalar computations.
What issue do they address?: The concept of vector computations, which allows operations on multiple data simultaneously, was difficult to express in Java. As a result, the environment relied on the HotSpot auto-vectorization algorithm, limiting its practical usefulness and performance.
Solution: The Vector API in Java allows for creating complex vector algorithms with better predictability and reliability. It leverages the existing HotSpot auto-vectorizer, offering users a more predictable model.
Changes since the last incubator: The API was reintroduced for incubation in JDK 22, with minor improvements from JDK 21, such as bug fixes and performance enhancements. The completion of Project Valhalla is expected before introducing the Preview version.