JDK 22 Key Features — Unnamed Variables and Launch Multi-File Programs

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:

FeatureStatusRecommended Usage
Unnamed Variables (_)FinalImmediately applicable
Multi-File LaunchFinalPrototyping, education, scripting
FFM APIFinalActively use as JNI replacement
Stream GatherersPreviewExperiment in test environments
Statements before super()PreviewExperiment in test environments
Class-File APIPreviewFor framework/tool developers
Region Pinning (G1)FinalAuto-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.

Was this article helpful?