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
| Feature | Core Value |
|---|---|
| Module System | Strong encapsulation, explicit dependencies, lightweight runtimes |
| JShell | Quick code experimentation and prototyping |
| Collection Factory Methods | Create immutable collections in one line with List.of() |
| Process API | Query OS process information via standard API |
| try-with-resources improvement | Directly use effectively final variables |
| Private Interface Methods | Reuse common logic within interfaces |
| G1 GC as default | Improved response time for large heaps |
| Compact Strings | 50% 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.