skip to content

What is 'strong encapsulation' in JPMS, and how does it change the meaning of 'public'?

level: middleimportance: must knowfreq 62%

answer

  1. Hidden by default; exports opens the door
  2. public AND exported AND read by consumer
  3. Non-exported public class = internal
  4. opens = reflection access, exports = API access
  5. Enforced by compiler/runtime, not convention

basics

~20 s

Strong encapsulation means only the packages a module explicitly exports are usable by other modules. A public class in a non-exported package is hidden, so 'public' no longer means 'visible to everyone' — it means visible only within the module unless the package is exported.

solid answer

~40 s

Strong encapsulation is JPMS's second core goal: a module's internals are hidden by default, and only packages listed with `exports` are accessible to other modules. This redefines `public`. Before modules, `public` effectively meant 'reachable by all code on the classpath'. Now reachability has two requirements: the type must be `public` AND its package must be exported by its module (and the consuming module must read yours). A `public` class in a non-exported package is internal — other modules cannot compile against it or reflect into it. This lets library authors keep implementation packages truly private while exposing a deliberate API surface. For reflection-heavy frameworks, `opens` grants deep reflective access without granting compile-time access, separating the two concerns. The result is enforced, not just conventional, encapsulation.

code

java · 12 lines
java
module com.example.lib {
    // Public API: usable by other modules
    exports com.example.lib.api;

    // Only this module may compile against the spi package
    exports com.example.lib.spi to com.example.plugin;

    // Let a DI framework reflect over entities, but not compile against them
    opens com.example.lib.model to spring.core;

    // com.example.lib.internal is neither exported nor opened -> fully hidden
}

go deeper

for a junior

Knows that only exported packages are visible to other modules and that public alone is no longer enough.

for a middle

States the three conditions for cross-module access, distinguishes exports vs opens, and explains qualified exports/opens.

for a senior

Explains how strong encapsulation gives library authors a real enforced API surface, how it changes reflection behaviour, and migration impact on frameworks.

for a principal

Designs module API surfaces deliberately (what to export vs keep internal), reasons about qualified exports for SPI/plugin boundaries, and weighs encapsulation enforcement against framework/reflection needs across an organization.

## What 'encapsulation' meant before modules **Encapsulation** is hiding internal details behind a controlled interface. Java's access modifiers (`private`, package-private, `protected`, `public`) handle this *within and between classes*. But at the JAR level there was a gaping hole: anything declared **`public`** was reachable by **all** code on the classpath. If you wanted a class usable by other classes in your own library, it had to be `public` — and then it was equally usable by everyone else, including consumers who should never touch your internals. Library authors documented 'do not use' and put classes in `internal` packages, but nothing **enforced** it; reflection could reach anything regardless. ## What strong encapsulation adds **Strong encapsulation** is JPMS's guarantee that a module's packages are **hidden by default** and become accessible to other modules **only when the module explicitly exports them**. The directive is `exports`: ```java module com.example.lib { exports com.example.lib.api; // visible to other modules // com.example.lib.internal is NOT exported -> hidden } ``` Here `com.example.lib.internal` stays private to the module even if its classes are declared `public`. ## The new meaning of `public` The key conceptual shift: **`public` is necessary but no longer sufficient** for another module to use a type. Cross-module accessibility now requires **all** of: 1. the member/type is `public` (the language-level modifier), AND 2. the package containing it is **exported** by its module, AND 3. the consuming module **reads** the providing module (has a `requires` edge to it). So within a module, `public` works as it always did. Across modules, a `public` type in a non-exported package is invisible — you cannot import it, compile against it, or (by default) reflect into it from another module. People summarize this as 'public no longer means public to everyone.' ## Reflection and the `opens` directive Reflection (inspecting/invoking code at runtime via the `java.lang.reflect` API) used to bypass all access checks — it could read private fields of any class anywhere. Strong encapsulation closes that door too: by default you cannot reflect into a non-open package of another module. But frameworks (dependency injection, serialization, ORMs) rely on deep reflection. JPMS provides two graduated tools: - **`exports pkg;`** → grants *compile-time and normal* access to a package's public API. - **`opens pkg;`** → grants *deep reflective* access (including to non-public members) at runtime, but **not** compile-time API access. This separation lets a module say 'frameworks may reflect over my entities, but no one may compile against this package as API.' You can also target a specific module: `exports pkg to other.module;` (a **qualified export**) or `opens pkg to framework.module;`. ## Why 'strong' It is called *strong* encapsulation because, unlike the old convention-and-documentation approach, it is **enforced by the runtime and compiler**. An attempt to access a non-exported package fails to compile, and a reflective attempt throws `IllegalAccessException` unless the package is open. Boundaries that used to be honor-system are now hard walls. ## Relationship to reliable configuration The two goals are complementary. **Reliable configuration** decides *which modules and dependencies exist* (the graph). **Strong encapsulation** decides *what inside each module is reachable* across that graph. A consumer needs both: a `requires` edge (configuration) and an `exports` on the target package (encapsulation) to use another module's API. ## Practical payoff Library authors gain a real, enforced API surface: implementation packages can finally be private, free to change without breaking consumers; consumers get a smaller, intentional API and can no longer accidentally couple to internals. That is the durable architectural value of strong encapsulation.

  • If a class is public but its package is not exported, can another module use it?
    No. Cross-module access requires both public AND an exported (or for reflection, opened) package, plus a requires edge from the consumer. A public class in a non-exported package is effectively internal.
  • What is the difference between exports and opens?
    exports grants normal compile-time and runtime access to a package's public API. opens grants deep reflective access at runtime (even to non-public members) but not compile-time access — it is for frameworks that reflect over your code.

A house where every room (package) is sealed by default. exports installs a front door to a specific room so guests (other modules) can enter; opens is a service hatch that lets inspectors (reflection) reach inside without giving them the front-door key (compile-time API).

saying these in an interview costs you the question

  • Saying public still means visible everywhere — across modules it requires an export
  • Using opens when you mean exports (or vice versa) — opens is reflection-only, exports is API access
  • Thinking reflection bypasses encapsulation as before — by default it does not; the package must be open
  • Forgetting the consumer also needs a requires edge, not just the producer's export

context