Skip to content
Neelu edited this page Jul 26, 2026 · 1 revision

☕ Java 9: The Modular Revolution

Release Date: September 21, 2017 | Type: Standard (non-LTS) | Status: ⚠️ End of Life


📋 Table of Contents

  1. Overview
  2. Core Features
  3. Best Practices
  4. Migration Notes

🎯 Overview

Java 9's headline feature was Project Jigsaw — the Java Platform Module System (JPMS), the biggest structural change to the JDK since its inception. It also quietly shipped a huge set of Streams/Optional/Collections quality-of-life improvements.

flowchart LR
    A[Monolithic JDK\npre-Java 9] -->|Project Jigsaw| B[Modular JDK\nJava 9+]
    B --> C[java.base]
    B --> D[java.sql]
    B --> E[java.xml]
    B --> F[Your App Module]
    F -->|requires| C
    F -->|requires| D
Loading

🚀 Core Features

1️⃣ Module System (module-info.java)

What: Group packages into named modules with explicit dependencies and exports — strong encapsulation at the JVM level, not just public/private.

// module-info.java
module com.myapp.orders {
    requires java.sql;
    requires com.myapp.common;

    exports com.myapp.orders.api;
    // com.myapp.orders.internal is NOT exported — truly hidden
}
graph TD
    OrdersModule["com.myapp.orders"] -->|requires| SqlModule["java.sql"]
    OrdersModule -->|requires| CommonModule["com.myapp.common"]
    OrdersModule -->|exports| API["com.myapp.orders.api"]
    OrdersModule -.->|hidden| Internal["com.myapp.orders.internal"]
Loading

Why it matters: Before Java 9, any code on the classpath could reflectively access any internal class. Modules make encapsulation real, reduce the "classpath hell" of large applications, and let the JDK itself ship smaller runtime images via jlink.

# Build a custom minimal runtime with only the modules you need
jlink --module-path $JAVA_HOME/jmods:mods \
      --add-modules com.myapp.orders \
      --output custom-runtime

2️⃣ JShell — The REPL

What: An interactive Read-Eval-Print Loop for Java, finally.

$ jshell
jshell> int x = 5
x ==> 5

jshell> x * 2
$2 ==> 10

jshell> List.of(1, 2, 3).stream().map(n -> n * n).forEach(System.out::println)
1
4
9

Why it matters: Rapid prototyping and learning without the edit-compile-run cycle — great for testing a Stream pipeline or API idea in seconds.


3️⃣ Private Methods in Interfaces

What: Interfaces can now have private and private static methods to share code between default methods.

public interface ReportGenerator {

    default String generatePdfReport(String data) {
        return format(data, "PDF");
    }

    default String generateCsvReport(String data) {
        return format(data, "CSV");
    }

    // Shared logic — not exposed to implementers
    private String format(String data, String type) {
        return "[" + type + "] " + data.trim().toUpperCase();
    }
}

4️⃣ Stream API Enhancements

// takeWhile() — take elements while condition is true, stop at first failure
Stream.of(1, 2, 3, 4, 1, 2)
      .takeWhile(n -> n < 4)
      .forEach(System.out::println);   // 1, 2, 3

// dropWhile() — opposite: drop while true, then take the rest
Stream.of(1, 2, 3, 4, 1, 2)
      .dropWhile(n -> n < 4)
      .forEach(System.out::println);   // 4, 1, 2

// iterate() with a predicate — bounded iteration, no need for limit()
Stream.iterate(1, n -> n < 100, n -> n * 2)
      .forEach(System.out::println);   // 1, 2, 4, 8, 16, 32, 64

// ofNullable() — a stream of 0 or 1 elements
Stream.ofNullable(getMaybeNull())
      .forEach(System.out::println);
flowchart LR
    subgraph Source["Stream: 1, 2, 3, 4, 1, 2"]
        direction LR
    end
    Source --> TW["takeWhile(n &lt; 4)"]
    Source --> DW["dropWhile(n &lt; 4)"]
    TW --> TWResult["1, 2, 3\n(stops at first failure)"]
    DW --> DWResult["4, 1, 2\n(drops until first failure, keeps rest)"]

    style TWResult fill:#d4f4dd,stroke:#22863a
    style DWResult fill:#fff4e0,stroke:#cc8800
Loading

5️⃣ Optional Enhancements

Optional<String> name = Optional.of("Neelu");

// ifPresentOrElse — handle both branches
name.ifPresentOrElse(
    n -> System.out.println("Hello, " + n),
    () -> System.out.println("No name provided")
);

// or() — fallback to another Optional
Optional<String> result = Optional.<String>empty()
    .or(() -> Optional.of("default value"));

// stream() — bridge Optional into Stream pipelines
List<String> names = List.of(opt1, opt2, opt3).stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

6️⃣ Collection Factory Methods

// Immutable collections in one line
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("x", "y", "z");
Map<String, Integer> map = Map.of("a", 1, "b", 2);

// list.add("d");  // ❌ UnsupportedOperationException — truly immutable

7️⃣ Try-With-Resources Improvement

// Java 8: had to declare a new variable
BufferedReader reader1 = new BufferedReader(new FileReader("file.txt"));
try (BufferedReader r = reader1) {
    // use r
}

// Java 9: use an effectively-final variable directly
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
try (reader) {
    // use reader directly — cleaner
}

💡 Best Practices

✅ Adopt modules for new, large applications — not worth retrofitting small existing apps
✅ Use takeWhile/dropWhile instead of manual loops for ordered-stream slicing
✅ Use Map.of()/List.of() for constant, small immutable collections — not for large or mutable data
❌ Don't fight the module system with --add-opens/--add-exports flags everywhere — that's a sign your module boundaries are wrong


🔄 Migration Notes

  • Libraries relying on internal JDK classes (sun.misc.*, etc.) may break — strong encapsulation started here (fully enforced by default in Java 17)
  • java.xml.bind (JAXB), java.xml.ws (JAX-WS) and other Java EE modules were deprecated for removal — add them as explicit dependencies if needed
  • String internal representation changed to byte[] (Latin-1/UTF-16 compact strings) — transparent, but reduces memory footprint significantly

Clone this wiki locally