Before Java 9, if a library's JAR was on the classpath, every public class in every package it contained was callable by consumers — including 'internal' implementation packages the library author never intended to expose (a real historical example: sun.misc.Unsafe). What does the Java Platform Module System (JPMS) change with module-info.java, and what do the exports and opens directives actually control?
answer
- module-info.java declares exports/requires/opens
- exports = compile+runtime visibility for outside modules
- opens = reflection-only access, narrower than exports
- strong encapsulation = enforced by JVM, not just compiler
- split packages become hard errors on the module path
basics
~20 sJPMS lets a JAR declare, in a module-info.java file, exactly which packages it shares with the outside world using exports. Any public class in a package NOT listed stays hidden from other modules even though it's technically public, so the compiler and the JVM both block access from outside.
solid answer
~60 smodule-info.java turns 'package is public' into a two-step gate: a class must be public AND its package must be exports-ed by the module for outside code to see it at compile time and link/reflect against it at runtime — this is 'strong encapsulation,' enforced by the JVM itself, not just javac. Packages left out of exports are invisible outside the module no matter their class-level modifiers, closing the pre-JPMS hole where 'on the classpath' implied 'fully public.' opens is a separate, narrower directive for reflection-only access — frameworks like Hibernate/Jackson that need to reach private/package-private fields via reflection at runtime without a compile-time public API — granting reflective access without also granting compile-time linkage via exports. The cost is real adoption friction: every dependency, transitively, needs to be a well-formed module (or a fragile 'automatic module' derived from JAR filename), and split packages — the same package name shipped by two different JARs — become a hard build error instead of an ambiguity, which broke a lot of older library ecosystems and is why JPMS adoption stayed low for years despite solving a real problem.
go deeper
Only needs to know JPMS/module-info.java exists as a stronger visibility mechanism than plain public/package-private; deep exports/opens distinction not expected.
Should be able to state what exports controls and give the sun.misc.Unsafe-style motivating problem.
Should distinguish exports from opens precisely, and explain the split-package and automatic-module adoption costs.
Should be able to make and defend an adoption decision — when JPMS's runtime guarantee justifies its migration cost versus lighter-weight alternatives like architecture tests — for a specific system's dependency graph.
## What `module-info.java` declares A module declares a `module-info.java` file at the root of its source tree naming itself (`module com.billing.core { ... }`) and listing directives inside braces. - **`requires other.module;`** declares a compile/runtime dependency on another module. - **`exports com.billing.core.api;`** makes every public type in that package visible to any module that requires this one, both for compiling against it and for the JVM's class loader to link against it at runtime — packages left out of any exports line stay invisible to outside modules no matter how many of their classes are marked public, because the module system checks readability at the package level as a prerequisite before Java's own class-level access modifiers even get consulted. - **`opens com.billing.core.internal;`** is the separate, narrower directive for reflective access only: it lets frameworks call `setAccessible(true)` and reach private/package-private members reflectively at runtime, without granting the broader compile-time-linkable visibility that exports gives. ## The problem it was built for Before JPMS (Java 9, Project Jigsaw), the classpath had no concept of "this package is meant to be internal" beyond convention — if a JAR contained a package, every public class in it was fully usable the moment the JAR was on the classpath, whether the author intended that or not. The canonical example is `sun.misc.Unsafe`: a JDK-internal class never meant for application use, but because it was public and its package was reachable, huge swaths of the ecosystem came to depend on it directly, which then made it effectively impossible for the JDK team to remove or change for years without breaking the world. **JPMS's strong encapsulation exists precisely to prevent this:** a module author can now say "these packages are my real API, and everything else — however public the classes look — is not accessible to you," and have the JVM itself enforce it. ## Strength, and the cost of adoption **The strength is real.** JPMS-enforced boundaries hold: - even against reflection, unless the module explicitly opens the package; - even against the split-package trick where two JARs declare the same package name, which the module system makes a hard build/launch error rather than an ambiguous runtime resolution. **The cost is substantial adoption friction.** - Every dependency in the graph, transitively, needs to either be a proper named module or fall back to being treated as an "automatic module" — JPMS derives a module name from the JAR's filename when it has no `module-info.class` — which is fragile, since renaming a JAR or changing a version suffix can silently change its derived module name and break requires declarations elsewhere. - Split packages, common in older multi-JAR libraries that intentionally shared one package across artifacts, become outright compile/launch errors under the module path, forcing painful repackaging of legacy dependencies. This friction is a major reason why, more than a decade after Java 9, most JVM projects — including typical Spring Boot applications — still run on the unnamed classpath rather than fully adopting the module path. ## Failure modes in production 1. **The most common production failure is an `InaccessibleObjectException`**, thrown when a reflection-heavy framework — an ORM, a serialization library, a DI container — tries to reach a field or invoke a constructor in a package the target module never opens, often surfacing only when an application first runs on the module path, or after a JDK upgrade tightens illegal-access warnings into hard errors. 2. **A second failure mode** is "module not found" or split-package errors at launch, typically from a legacy multi-JAR dependency never designed for the module path. 3. **A third, more insidious one:** teams add a blanket `opens ... to ...;` or even `open module`, opening every package to reflection, just to make errors go away, which quietly re-creates the exact "everything is reachable" problem JPMS was meant to solve. ## Where it shows up — the JDK itself The JDK's own modularization is the flagship real-world case — `java.base`, `java.sql`, and dozens of other JDK modules each declare exactly which packages are public API via exports, while internal packages like `jdk.internal.misc` stay closed to application code, which is precisely why applications that depended on JDK-internal classes like `sun.misc.Unsafe` had to migrate to supported replacements, such as `VarHandle`, as later JDK releases tightened the module system's enforcement from warnings to hard failures.
- If a class is declared public but its package isn't listed in an exports directive, can another module still see it via reflection with setAccessible(true)?No, not unless the module also opens that package, either to all modules or to a specific named module — exports and opens are independent grants, and without either, JPMS's strong encapsulation blocks even reflective access, throwing an InaccessibleObjectException. This is the key difference from pre-JPMS package-private, where reflection could always bypass visibility.
- What is an 'automatic module' and why is it considered fragile?It's a module name JPMS derives automatically for a plain JAR on the module path with no module-info.class of its own, typically from the JAR's filename or an Automatic-Module-Name manifest entry. It's fragile because the derived name can change if the JAR's filename changes, such as a different version suffix, silently breaking any requires statement elsewhere that referenced the old derived name.
- Why did split packages — the same package name shipped across two different JARs — work fine on the classpath but become a hard error on the module path?On the classpath, the class loader just resolves classes from whichever JAR it finds them in first, silently and ambiguously merging the two JARs' packages. JPMS requires every package to be owned by exactly one module for its readability graph to be well-defined, so if two modules declare the same package the module system refuses to resolve the module graph at all and fails at launch rather than silently picking one.
module-info.java is like a building directory that lists only the offices open to visitors (exports); everything else stays behind a locked door even if the office itself has a sign reading 'public' on it, unless the building manager specifically hands out a reflection-only visitor badge (opens) for maintenance staff.
saying these in an interview costs you the question
- Thinks module-info.java is optional metadata with no enforcement, just documentation
- Confuses exports and opens, or thinks they're the same directive
- Believes JPMS boundaries can always be bypassed by reflection the same way package-private can
- Unaware that most Spring Boot / typical JVM projects still run on the classpath, not the module path
- Can't explain why sun.misc.Unsafe is the canonical example of the problem JPMS fixes