Java Evolution — Part 2

Java 9, released on September 21, 2017, was the first version to ship after the new six-month release cadence was announced — though it was itself delayed several times. Its headline feature, Project Jigsaw, restructured the entire JDK into modules and gave developers a formal way to declare dependencies and encapsulation boundaries.


The Java Platform Module System (JPMS)

Before Java 9, the JDK was a monolithic runtime. Every application loaded the entire standard library, even if it only used a fraction of it. More critically, there was no enforced encapsulation at the package level — public meant accessible from anywhere, making it impossible to hide internal APIs.

Project Jigsaw (JEP 261) solved both problems by introducing the module as a first-class concept in the Java platform.

What Is a Module?

A module is a named, self-describing unit of code. It declares:

  • What packages it exports (makes accessible to other modules)
  • What other modules it requires (depends on)
  • What services it provides or uses

This information lives in a file called module-info.java at the root of the module’s source tree.

// module-info.java for a hypothetical "com.myapp.core" module
module com.myapp.core {
requires java.sql; // depends on the java.sql module
requires transitive java.logging; // re-exports java.logging to consumers
exports com.myapp.core.api; // only this package is public
exports com.myapp.core.model to com.myapp.web; // qualified export
opens com.myapp.core.internal to com.fasterxml.jackson.databind; // for reflection
}

The JDK Is Now Modular

The JDK itself was split into approximately 90 modules. The most important ones are:

ModuleContents
java.baseCore types: java.lang, java.util, java.io, java.nio
java.sqlJDBC API
java.loggingjava.util.logging
java.desktopAWT, Swing
java.xmlXML processing (JAXP)
jdk.jshellThe JShell REPL tool

Every module implicitly requires java.base, so you never need to declare that dependency.

Because the JDK is modular, you can now create a minimal runtime image containing only the modules your application actually needs:

Terminal window
jlink \
--module-path $JAVA_HOME/jmods \
--add-modules com.myapp.core,java.sql \
--output my-custom-runtime

The resulting image can be 30–80% smaller than a full JDK, which is particularly valuable for containerised deployments.

Strong Encapsulation

With modules, public no longer means universally accessible. A type is only accessible from outside its module if the module explicitly exports the package containing it. This broke a number of frameworks and tools that relied on reflective access to JDK internals — the --add-opens flag was introduced as an escape hatch during the transition period.


JShell — Java’s First REPL

JShell (JEP 222) is an interactive Read-Eval-Print Loop (REPL) for Java. Before Java 9, experimenting with a snippet of Java code required creating a class, writing a main method, compiling, and running. JShell removes all of that friction.

$ jshell
| Welcome to JShell -- Version 9
| For an introduction type: /help intro
jshell> int x = 10
x ==> 10
jshell> int y = 20
y ==> 20
jshell> x + y
$3 ==> 30
jshell> String greet(String name) {
...> return "Hello, " + name + "!";
...> }
| created method greet(String)
jshell> greet("Duke")
$5 ==> "Hello, Duke!"
jshell> /list
1 : int x = 10;
2 : int y = 20;
3 : x + y
4 : String greet(String name) { return "Hello, " + name + "!"; }
5 : greet("Duke")

JShell is invaluable for quickly testing API behaviour, exploring new features, and teaching Java without the ceremony of a full project.


Collection Factory Methods

Java 9 added convenient static factory methods to List, Set, and Map for creating small, immutable collections without the verbose Arrays.asList() or Collections.unmodifiableList() boilerplate.

// Before Java 9
List<String> oldList = Collections.unmodifiableList(
Arrays.asList("a", "b", "c")
);
// Java 9
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("x", "y", "z");
Map<String, Integer> map = Map.of(
"one", 1,
"two", 2,
"three", 3
);
// For larger maps, use Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("a", 1),
Map.entry("b", 2),
Map.entry("c", 3),
Map.entry("d", 4)
);

Important: Collections created with List.of(), Set.of(), and Map.of() are truly immutable — any attempt to add, remove, or replace elements throws UnsupportedOperationException. They also do not permit null elements.


The New HTTP Client (Preview)

Java 9 introduced a new HttpClient API (JEP 110) as an incubating module (jdk.incubator.httpclient). It was designed to replace the old, cumbersome HttpURLConnection with a modern, fluent API supporting HTTP/2 and WebSockets. It became standard in Java 11 — we will cover it there in detail.


Stream API Enhancements

Java 9 added four useful methods to the Stream interface:

// takeWhile — take elements while predicate holds (stops at first failure)
Stream.of(1, 2, 3, 4, 5, 1, 2)
.takeWhile(n -> n < 4)
.forEach(System.out::println); // 1, 2, 3
// dropWhile — drop elements while predicate holds, then take the rest
Stream.of(1, 2, 3, 4, 5, 1, 2)
.dropWhile(n -> n < 4)
.forEach(System.out::println); // 4, 5, 1, 2
// iterate with a predicate (like a for-loop)
Stream.iterate(1, n -> n <= 10, n -> n + 1)
.forEach(System.out::println); // 1 through 10
// ofNullable — create a stream of 0 or 1 elements
Stream.ofNullable(null).count(); // 0
Stream.ofNullable("hello").count(); // 1

Optional Enhancements

Optional gained three new methods in Java 9:

Optional<String> opt = Optional.empty();
// ifPresentOrElse — handle both present and absent cases
opt.ifPresentOrElse(
s -> System.out.println("Found: " + s),
() -> System.out.println("Not found")
); // Not found
// or — provide an alternative Optional if empty
Optional<String> result = opt.or(() -> Optional.of("default"));
// Optional["default"]
// stream — convert Optional to a Stream (0 or 1 elements)
long count = opt.stream().count(); // 0

Private Interface Methods

Java 9 extended the interface evolution started in Java 8 by allowing private methods inside interfaces. This enables default methods to share common implementation logic without exposing that logic as part of the interface’s contract.

interface Logger {
default void logInfo(String message) {
log("INFO", message);
}
default void logError(String message) {
log("ERROR", message);
}
// Private helper — not part of the public contract
private void log(String level, String message) {
System.out.printf("[%s] %s%n", level, message);
}
}

Process API Improvements

Java 9 significantly enhanced the ProcessHandle API, making it easier to manage and inspect OS processes from Java code:

// Get information about the current process
ProcessHandle current = ProcessHandle.current();
System.out.println("PID: " + current.pid());
System.out.println("Command: " + current.info().command().orElse("unknown"));
// List all running processes
ProcessHandle.allProcesses()
.filter(p -> p.info().command().isPresent())
.forEach(p -> System.out.println(p.pid() + " " + p.info().command().get()));
// Start a process and react when it finishes
ProcessBuilder pb = new ProcessBuilder("sleep", "5");
Process process = pb.start();
process.onExit().thenAccept(p -> System.out.println("Process " + p.pid() + " exited"));

Summary

Java 9 was a structural release. Its most lasting impact was the module system, which changed how the JDK itself is organised and gave developers the tools to build well-encapsulated, deployable applications.

FeatureKey Benefit
JPMS (Modules)Strong encapsulation, reliable configuration, smaller runtimes
JShellInteractive experimentation without boilerplate
Collection FactoriesConcise, immutable collection creation
Stream EnhancementstakeWhile, dropWhile, iterate with predicate, ofNullable
Optional EnhancementsifPresentOrElse, or, stream
Private Interface MethodsShared logic in default methods without leaking internals
Process APIBetter OS process management and inspection