JDK 22’s Direction
JDK 22, released in March 2024, is a non-LTS release focused on developer convenience improvements. Unnamed Variables, which was a preview in JDK 21, was finalized, and the ability to run multi-file Java programs without compilation was added. Additionally, Stream Gatherers, which allows defining custom intermediate operations for the Stream API, appeared as a preview.
Unnamed Variables & Patterns (Final, JEP 456)
The feature that was previewed in JDK 21 is now finalized. Using _ (underscore) for unused variables explicitly communicates “this value is intentionally ignored.”
import java.util.List;
import java.util.Map;
public class UnnamedVariablesExample {
sealed interface Event permits Click, Scroll, KeyPress {}
record Click(int x, int y, String target) implements Event {}
record Scroll(int delta) implements Event {}
record KeyPress(char key, boolean ctrl, boolean shift) implements Event {}
public static void main(String[] args) {
List<Event> events = List.of(
new Click(100, 200, "button"),
new Scroll(3),
new KeyPress('S', true, false),
new Click(50, 75, "link")
);
// Use _ in switch for unused fields
for (Event event : events) {
switch (event) {
// Ignore x, y coordinates and use only target
case Click(var _, var _, var target) ->
System.out.println("Click: " + target);
case Scroll(var delta) ->
System.out.println("Scroll: " + delta);
// Ignore ctrl, shift
case KeyPress(var key, var _, var _) ->
System.out.println("Key press: " + key);
}
}
// Output:
// Click: button
// Scroll: 3
// Key press: S
// Click: link
// try-with-resources when variable name is unnecessary
// Example: only acquiring a lock without referencing the variable itself
// try (var _ = acquireLock()) { ... }
// Enhanced for when acting as index (value not needed)
int count = 0;
for (var _ : events) {
count++;
}
System.out.println("Total events: " + count);
// Output: Total events: 4
// Map iteration when ignoring key or value
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87, "Charlie", 92);
int total = 0;
for (var entry : scores.entrySet()) {
// When ignoring keys and summing only values
// Map.Entry doesn't allow _ directly, so use getValue()
total += entry.getValue();
}
System.out.println("Score total: " + total);
// Output: Score total: 274
}
}
_ can be used multiple times in the same scope. Having two _s in case Click(var _, var _, var target) is perfectly fine. Previously, unused variables had to be named ignored or unused, but now _ unifies this, making code intent clearer.
Launch Multi-File Source-Code Programs (JEP 458)
Single-file execution introduced in JDK 11 (java Hello.java) has been extended to multiple files. Without a compilation step, java Main.java alone automatically finds and runs other Java files in the same directory.
// --- File: Calculator.java ---
public class Calculator {
// Basic arithmetic operations
public static int add(int a, int b) { return a + b; }
public static int subtract(int a, int b) { return a - b; }
public static int multiply(int a, int b) { return a * b; }
public static double divide(int a, int b) {
if (b == 0) throw new ArithmeticException("Cannot divide by zero");
return (double) a / b;
}
}
// --- File: Main.java ---
public class Main {
public static void main(String[] args) {
// No need to compile Calculator.java separately
System.out.println("10 + 3 = " + Calculator.add(10, 3));
System.out.println("10 - 3 = " + Calculator.subtract(10, 3));
System.out.println("10 * 3 = " + Calculator.multiply(10, 3));
System.out.println("10 / 3 = " + Calculator.divide(10, 3));
}
}
// Run: java Main.java
// Output:
// 10 + 3 = 13
// 10 - 3 = 7
// 10 * 3 = 30
// 10 / 3 = 3.3333333333333335
Just java Main.java is all you need. No javac compilation step required. This is especially useful for prototyping, education, and scripting purposes. For production projects, build tools (Gradle, Maven) are still recommended.
Stream Gatherers (Preview, JEP 461)
A gather() method for defining custom intermediate operations was added to the Stream API as a preview. Previously, only predefined operations like filter, map, flatMap were available, but Gatherers allow defining custom logic like window sliding, deduplication, and stateful transformations as intermediate operations.
import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
public class GathererExample {
// Requires --enable-preview at runtime
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// windowFixed: group into fixed-size windows
List<List<Integer>> windows = numbers.stream()
.gather(Gatherers.windowFixed(3))
.toList();
System.out.println("Fixed window (size 3): " + windows);
// Output: Fixed window (size 3): [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
// windowSliding: sliding window (useful for moving averages)
List<Double> movingAvg = numbers.stream()
.gather(Gatherers.windowSliding(3))
.map(window -> window.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0))
.toList();
System.out.println("Moving average (window 3): " + movingAvg);
// Output: Moving average (window 3): [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
// fold: emit accumulated result as stream
List<Integer> runningSum = numbers.stream()
.gather(Gatherers.fold(() -> 0, Integer::sum))
.toList();
System.out.println("Total sum: " + runningSum);
// Output: Total sum: [55]
// scan: similar to fold but emits all intermediate results
List<Integer> scanSum = numbers.stream()
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
System.out.println("Running sum: " + scanSum);
// Output: Running sum: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
}
}
Gatherers.windowFixed() and Gatherers.windowSliding() are frequently used patterns in data analysis, and scan() corresponds to functional programming’s scanLeft. Previously, implementing such operations required creating complex custom Collectors in collect() or falling back to for loops.
Statements before super() (Preview, JEP 447)
A feature allowing validation logic before super() calls in constructors. Previously, super() had to be the first statement in a constructor.
public class SuperBeforeExample {
// Requires --enable-preview at runtime
static class Animal {
final String name;
Animal(String name) {
this.name = name;
System.out.println("Animal created: " + name);
}
}
static class Dog extends Animal {
final int age;
Dog(String name, int age) {
// JDK 22 preview: validation possible before super() call
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name is required");
}
if (age < 0) {
throw new IllegalArgumentException("Age must be 0 or greater");
}
// Call super after validation
super(name.strip());
this.age = age;
System.out.println("Dog created: " + name + ", age " + age);
}
}
public static void main(String[] args) {
Dog dog = new Dog(" Buddy ", 3);
System.out.println("Name: '" + dog.name + "', Age: " + dog.age);
// Output:
// Animal created: Buddy
// Dog created: Buddy , age 3
// Name: 'Buddy', Age: 3
try {
new Dog("", 5); // Validation failure
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
// Output: Error: Name is required
}
}
Previously, workaround patterns like static factory methods or helper methods were needed to validate before super(). This feature allows naturally placing validation logic within the constructor.
Foreign Function & Memory API (Final, JEP 454)
The FFM API, which went through previews since JDK 19, is finally officially released. It replaces JNI for safe and efficient native library calls. Arena manages off-heap memory lifetime, and Linker directly calls C functions.
The same pattern as JDK 20’s FFM API example, but since it’s officially released, no --enable-preview flag is needed.
Performance: Region Pinning for G1 (JEP 423)
JNI Critical Region handling was improved in G1 GC. Previously, when a JNI Critical Section was active, G1 GC had to halt the entire collection. Starting with JDK 22, only the affected Region is pinned while the rest are collected normally, significantly reducing GC pause times for applications that heavily use JNI.
# Automatically applied when using G1 GC (no separate configuration needed)
java -XX:+UseG1GC -jar app.jar
# Check Region Pinning effect with GC logs
java -XX:+UseG1GC -Xlog:gc* -jar app.jar
This is noticeable in environments using JNI libraries such as Apache Spark, Hadoop, and TensorFlow Java.
Class-File API (Preview, JEP 457)
A standard API for reading, writing, and transforming bytecode was added as a preview. Previously, third-party libraries like ASM and ByteBuddy were required, but now bytecode manipulation is possible with a built-in JDK API.
This API is primarily for framework and tool developers and rarely used directly in application development. However, once frameworks like Spring and Hibernate adopt this API internally, third-party bytecode library dependencies will decrease and JDK upgrade compatibility issues will be reduced.
Practical Tips
Key points for applying JDK 22 in practice:
| Feature | Status | Recommended Usage |
|---|---|---|
Unnamed Variables (_) | Final | Immediately applicable |
| Multi-File Launch | Final | Prototyping, education, scripting |
| FFM API | Final | Actively use as JNI replacement |
| Stream Gatherers | Preview | Experiment in test environments |
| Statements before super() | Preview | Experiment in test environments |
| Class-File API | Preview | For framework/tool developers |
| Region Pinning (G1) | Final | Auto-applied when using JNI |
JDK 22 focused on “making the code developers write every day more convenient” rather than revolutionary large features. Using _ to clarify intent, running multiple files without compilation, and adding custom Stream operations all improve the daily development experience. Though non-LTS, the finalized features (Unnamed Variables, FFM API, Region Pinning) carry forward to JDK 23 and beyond, so you can learn them with confidence.