☕ Java

Module System (JPMS)

The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.

Motivation — The Classpath's Two Core Problems

Before Java 9, the unit of deployment was the JAR file, placed on the classpath, and the classpath model had two structural weaknesses that JPMS was designed to address. The first was the absence of reliable configuration: nothing in a JAR declared what other JARs it depended on, so dependency information lived entirely outside the JVM, in build tool configuration (Maven, Gradle) or in documentation, and a missing dependency only surfaced at runtime as a NoClassDefFoundError or ClassNotFoundException when the missing class was actually touched, rather than as an immediate, comprehensible failure at startup. The second, more pervasive problem was weak encapsulation: every public class in every package in every JAR on the classpath was accessible to every other JAR on the classpath, with no way for a library to mark a public class as "public, but only for use by classes within this same JAR" — public meant public to the entire classpath, with no intermediate visibility level between package-private and fully open. This meant that internal implementation classes within the JDK (the sun.* packages, and similar internal classes in many widely-used libraries) were technically callable by any application code despite being explicitly documented as unsupported, internal-use-only API, because the language had no mechanism to actually enforce that intent. Many libraries and applications had quietly come to depend on these internal classes over the years, which constrained how freely the JDK's own maintainers could refactor or remove that internal code without breaking the ecosystem — directly motivating JPMS as the mechanism to finally decompose and properly encapsulate the JDK's own internals. JPMS introduces the module as a named, self-describing unit sitting above the package: a module explicitly declares (in a module-info.java file at its root) which of its packages are exported for external use, and which other modules it requires to compile and run, making both dependency information and access boundaries explicit, declarative, and enforced by the JVM at compile time, link time, and runtime — not merely documented and trusted.
Java
// ── The classpath problem: weak encapsulation ───────────────────────────
// Before modules, ANY public class on the classpath is reachable from anywhere:
package com.mylib.internal;
public class InternalHelper {   // "public" but documented as internal-use-only
    public static void doSensitiveThing() { /* ... */ }
}
// Any other JAR on the classpath could legally call this, despite documentation:
// com.mylib.internal.InternalHelper.doSensitiveThing();   // compiles fine pre-Java 9

// ── The classpath problem: missing dependency surfaces only at runtime ──
// No declared dependency information — a missing JAR fails only when the
// missing class is actually loaded and used, deep into program execution:
// Exception in thread "main" java.lang.NoClassDefFoundError: com/somelib/SomeClass

// ── JPMS: a module explicitly declares its boundary ─────────────────────
// module-info.java at the root of the module's source:
module com.mylib {
    exports com.mylib.api;          // only this package is visible to other modules
    // com.mylib.internal is NOT exported — invisible outside this module, ENFORCED

    requires java.sql;               // explicit, checked dependency
}

// Another module trying to access the unexported internal package:
// import com.mylib.internal.InternalHelper;   // COMPILE ERROR:
// "package com.mylib.internal is not visible — it is not exported"

module-info.java Directives — requires, exports, and opens

A module is declared by a module-info.java file at the root of its source tree, compiled into a module-info.class that ships alongside the module's regular classes. The requires directive declares a compile-time and runtime dependency on another module; if module A requires module B, A's code can reference B's exported types, and the module system verifies at both compile time and launch time that B is actually present and reachable, failing fast with a clear error if it is not — directly solving the deferred-failure problem of the classpath. The exports directive makes a specific package's public types accessible to other modules that require this module; any package not listed in an exports directive remains entirely inaccessible from outside the module, regardless of whether its classes are declared public, enforced by the JVM's module boundary checks, not merely by convention. A variant, exports ... to ..., restricts the export to a specific named list of modules (qualified exports), useful for exposing an internal API only to a small number of trusted, known consumer modules rather than to every module in the application. The opens directive (and the module-level open modifier) addresses a separate concern: reflective access. Many frameworks (dependency injection containers, serialization libraries, ORMs) use reflection to access fields and invoke methods that were never exported in the traditional sense, including private fields and methods. exports alone does not grant this kind of deep reflective access — opens specifically grants runtime reflective access to a package (including non-public members) without granting compile-time accessibility, which is the access pattern frameworks like Hibernate or Jackson typically need. requires transitive propagates a dependency: if module A requires transitive B, then any module that requires A automatically also has access to B's exports, without needing to declare requires B itself — used when A's exported API itself exposes types from B in its method signatures.
Java
// ── requires — explicit, verified dependency ────────────────────────────
module com.myapp {
    requires java.sql;                 // must be present, checked at compile+launch time
    requires com.fasterxml.jackson.databind;
}

// ── exports — unqualified, visible to any module that requires this one ─
module com.mylib.core {
    exports com.mylib.core.api;        // public API — visible to all requiring modules
    // com.mylib.core.impl is implicitly NOT exported — fully encapsulated
}

// ── exports ... to ... — qualified export, restricted to named modules ──
module com.mylib.core {
    exports com.mylib.core.internalapi to com.mylib.testsupport, com.mylib.adminconsole;
    // only these two named modules can see this package — everyone else cannot
}

// ── opens — grants reflective access without compile-time accessibility ─
module com.myapp.model {
    exports com.myapp.model;            // normal compile-time API access
    opens com.myapp.model.entities to org.hibernate.orm.core;
    // Hibernate can reflectively read/write private fields on entities,
    // even though entities package is NOT exported for normal compile-time use
}

// Whole-module shortcut — opens every package in the module reflectively:
open module com.myapp.legacy {
    requires java.base;
    // every package in this module is open to reflection by any module
}

// ── requires transitive — propagates a dependency to downstream consumers
module com.mylib.api {
    requires transitive com.mylib.types;   // com.mylib.types appears in this API's signatures
    exports com.mylib.api;
}
module com.consumer {
    requires com.mylib.api;     // automatically also gets access to com.mylib.types' exports,
    // without separately declaring "requires com.mylib.types"
}

Module Path vs Classpath, Automatic Modules, and jlink

JPMS introduces the module path as an alternative to the classpath: JARs placed on the module path are treated as modules, while JARs placed on the classpath continue to behave as before, meaning the classpath and the old unrestricted-access behavior were preserved for full backward compatibility — JPMS was deliberately designed so that pre-existing applications and libraries without any module-info would keep working unmodified. This compatibility is achieved through two mechanisms: the unnamed module, which is where all classpath-resident classes effectively live (it can read every other module and exports all of its own packages, replicating classic classpath semantics), and automatic modules, which are ordinary, non-modularized JARs placed on the module path — the JVM derives a module name automatically (usually from the JAR's manifest or filename) and treats the entire JAR as if it exported all of its packages and required every other module present, providing an incremental migration path for libraries that haven't yet been modularized but whose consumers want to start using the module path. A module that has no module-info.java but is placed on the classpath is simply part of the unnamed module and is invisible to named modules that try to requires it by name (since it has no declared name) — this is one practical reason migrating a multi-module project to JPMS generally requires either modularizing every JAR in the dependency chain or relying on automatic modules as an interim step. jlink, introduced alongside JPMS, is a tool that assembles a custom, minimal Java runtime image containing only the specific JDK modules (and application modules) actually required by a given application, rather than shipping the entire JDK runtime. Because the JDK itself was decomposed into modules as part of Project Jigsaw, jlink can compute the transitive closure of required modules from an application's module-info and produce a standalone, executable runtime image significantly smaller than a full JDK install — directly useful for containerized deployments, embedded systems, and any context where minimizing image size and startup footprint matters.
Java
# ── Classic classpath — unrestricted, no module declarations needed ────
javac -cp libs/*.jar -d out src/com/myapp/Main.java
java -cp out:libs/*.jar com.myapp.Main
# Works exactly as it always did — full backward compatibility preserved

# ── Module path — JARs treated as modules ───────────────────────────────
javac -p mods -d out src/com.myapp/module-info.java src/com.myapp/com/myapp/Main.java
java -p out:mods -m com.myapp/com.myapp.Main

# ── Automatic modules — non-modularized JAR placed on the module path ──
# legacy-lib.jar has no module-info.class, but is placed on the module path:
java -p mods:legacy-lib.jar -m com.myapp/com.myapp.Main
# JVM derives a module name (e.g. "legacy.lib" from the filename) and treats
# the WHOLE jar as exporting everything and requiring everything — interim bridge

// module-info.java referencing an automatic module by its derived name:
module com.myapp {
    requires legacy.lib;   // works even though legacy-lib.jar has no module-info
}

# ── jlink — building a minimal custom runtime image ─────────────────────
jlink --module-path $JAVA_HOME/jmods:mods \
      --add-modules com.myapp \
      --output custom-runtime \
      --launcher run=com.myapp/com.myapp.Main \
      --strip-debug --no-header-files --no-man-pages

# Resulting "custom-runtime" directory is a self-contained, executable JVM
# image containing only java.base + whatever com.myapp's module-info actually
# requires (transitively) — often a fraction of a full JDK install's size
./custom-runtime/bin/run

Related Topics in Java 9 to 21 Features

JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.
var Keyword
The var keyword, introduced in Java 10 as local variable type inference, lets a local variable declaration omit its explicit type and have the compiler infer it from the initializer expression on the right-hand side, while the variable remains just as statically typed at compile time as if the type had been written out explicitly — var is purely a source-code convenience, not a shift toward dynamic typing. This is a narrower, more conservative feature than type inference in languages like C# (var) or Kotlin (val/var): it applies only to local variables with an initializer, never to fields, method parameters, or return types, and the compiler still performs full static type checking using the inferred type. This entry covers the motivation and explicit scope restrictions, how type inference actually resolves (including with generics, diamond operator interaction, and anonymous classes), the readability tradeoffs and style guidance the OpenJDK team itself published, and the specific cases where var cannot be used.