JDK 9 Key Features — The Module System and the Arrival of JShell

Where JDK 9 Fits In

Released in 2017, JDK 9 was the release that undertook a structural redesign of the Java platform. The biggest change was the introduction of the module system (Project Jigsaw), which split the JDK itself into approximately 90 modules. At the same time, developer productivity features like JShell and collection factory methods were also added.

Module System (Project Jigsaw)

The module system is a unit of encapsulation at a higher level than packages. Using a building block analogy: previously all blocks were mixed together in one box, but modules put each block set in a separate box and explicitly declare “this box requires that box” as a dependency.

You define a module by placing a module-info.java file at the project root.

// module-info.java — module declaration file
// This module is defined with the name com.myapp
module com.myapp {
    // Declare dependency on the java.net.http module
    requires java.net.http;
    // Declare dependency on the java.sql module
    requires java.sql;

    // Expose the com.myapp.api package to other modules
    exports com.myapp.api;
    // The com.myapp.internal package is not exported — inaccessible from outside
}

The module system provides three key benefits. First, packages not declared with exports cannot be accessed by other modules, ensuring strong encapsulation. Second, since dependencies are explicitly declared with requires, circular dependencies are detected at compile time. Third, the jlink tool can create custom runtime images containing only the necessary modules, drastically reducing deployment size.

JShell — Java REPL

Before JDK 9, testing a single line of code required creating a class, writing a main() method, compiling, and running it. JShell eliminates this entire process.

Running jshell in the terminal lets you immediately type Java code and see results.

$ jshell
|  Welcome to JShell -- Version 9

jshell> var list = List.of("Java", "Kotlin", "Scala")
list ==> [Java, Kotlin, Scala]

jshell> list.stream().filter(s -> s.length() > 4).toList()
$2 ==> [Kotlin, Scala]

jshell> /exit

It is extremely useful for quickly verifying API behavior or prototyping algorithm logic.

Collection Factory Methods

Until JDK 8, creating an immutable list required verbose code like Collections.unmodifiableList(Arrays.asList(...)). In JDK 9, List.of(), Set.of(), and Map.of() accomplish the same thing in a single line.

import java.util.List;
import java.util.Set;
import java.util.Map;

public class CollectionFactory {
    public static void main(String[] args) {
        // List.of() — create an immutable list
        List<String> colors = List.of("Red", "Green", "Blue");
        System.out.println("Colors: " + colors);
        // Output: Colors: [Red, Green, Blue]

        // Set.of() — create an immutable set (throws IllegalArgumentException on duplicates)
        Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
        System.out.println("Primes: " + primes);
        // Output: Primes: [2, 3, 5, 7, 11] (order not guaranteed)

        // Map.of() — create an immutable map (up to 10 key-value pairs)
        Map<String, Integer> scores = Map.of(
            "Korean", 95,
            "Math", 88,
            "English", 92
        );
        System.out.println("Scores: " + scores);
        // Output: Scores: {Korean=95, Math=88, English=92} (order not guaranteed)

        // Map.ofEntries() — use for more than 10 entries
        Map<String, String> config = Map.ofEntries(
            Map.entry("host", "localhost"),
            Map.entry("port", "8080"),
            Map.entry("protocol", "https")
        );
        System.out.println("Config: " + config);
        // Output: Config: {host=localhost, port=8080, protocol=https}

        // Attempting to modify throws UnsupportedOperationException since the list is immutable
        try {
            colors.add("Yellow");
        } catch (UnsupportedOperationException e) {
            System.out.println("Immutable list cannot be modified!");
            // Output: Immutable list cannot be modified!
        }
    }
}

Collections created with List.of() do not allow null elements. Inserting null immediately throws a NullPointerException, helping you catch null-related bugs early.

Process API Enhancements

Previously, obtaining OS process information required native code or external libraries. JDK 9’s ProcessHandle API makes it easy to query information about the current process and its children.

import java.time.Duration;
import java.time.Instant;

public class ProcessApiExample {
    public static void main(String[] args) {
        // Query current process information
        ProcessHandle current = ProcessHandle.current();

        System.out.println("PID: " + current.pid());
        // Output: PID: 12345

        current.info().command()
            .ifPresent(cmd -> System.out.println("Command: " + cmd));
        // Output: Command: /usr/bin/java

        current.info().startInstant()
            .ifPresent(start -> {
                Duration uptime = Duration.between(start, Instant.now());
                System.out.println("Uptime: " + uptime.toMillis() + "ms");
            });
        // Output: Uptime: 128ms

        // Query total number of processes
        long processCount = ProcessHandle.allProcesses().count();
        System.out.println("Running processes: " + processCount);
        // Output: Running processes: 287
    }
}

try-with-resources Improvement and Private Interface Methods

JDK 9 introduced two small but useful syntax improvements.

import java.io.BufferedReader;
import java.io.StringReader;

public class SyntaxImprovements {

    // Private Interface Method (JDK 9)
    // Allows extracting common logic shared between default methods in interfaces
    interface Logger {
        default void logInfo(String msg) {
            log("INFO", msg);
        }

        default void logError(String msg) {
            log("ERROR", msg);
        }

        // Common logic extracted into a private method — not exposed externally
        private void log(String level, String msg) {
            System.out.println("[" + level + "] " + msg);
        }
    }

    public static void main(String[] args) throws Exception {
        // try-with-resources improvement: directly use effectively final variables
        BufferedReader reader = new BufferedReader(new StringReader("Hello JDK 9"));

        // JDK 8: try (BufferedReader r = reader) { ... } — required a new variable
        // JDK 9: existing variable can be used directly
        try (reader) {
            System.out.println("Content read: " + reader.readLine());
            // Output: Content read: Hello JDK 9
        }

        // Using Private Interface Methods
        Logger logger = new Logger() {};
        logger.logInfo("Server started");
        logger.logError("Connection failed");
        // Output:
        // [INFO] Server started
        // [ERROR] Connection failed
    }
}

Performance Improvements

G1 GC becomes the default: Starting with JDK 9, G1 (Garbage-First) GC became the default garbage collector. Compared to the previous Parallel GC, it offers shorter Stop-the-World pauses, improving response times for applications with large heaps.

Compact Strings: Before JDK 9, String internals were stored as char[] (2 bytes per character). Starting with JDK 9, strings containing only Latin-1 characters are stored as byte[] (1 byte per character). This significantly reduces memory usage in applications that primarily use English text. Strings containing multi-byte characters (such as CJK characters) are still stored as UTF-16 as before.

Summary

FeatureCore Value
Module SystemStrong encapsulation, explicit dependencies, lightweight runtimes
JShellQuick code experimentation and prototyping
Collection Factory MethodsCreate immutable collections in one line with List.of()
Process APIQuery OS process information via standard API
try-with-resources improvementDirectly use effectively final variables
Private Interface MethodsReuse common logic within interfaces
G1 GC as defaultImproved response time for large heaps
Compact Strings50% memory savings for English strings

The module system in JDK 9 was controversial when first introduced, but it provides library developers with a powerful tool for protecting internal APIs. For typical application development, List.of(), the try-with-resources improvement, and the G1 GC default switch have a more immediate practical impact than the module system.

Was this article helpful?