12

Information Hiding (Parnas, 1972)

#
  • heuristic
  • established
  • fragment

Versions. The module system is JDK 9+, and for modules on the module path strong encapsulation has applied from the start. The --illegal-access relaxation only concerned the JDK’s own internal packages: deny by default since JDK 16 (JEP 396), option ignored since JDK 17 (JEP 403).

Conflicts with: DOD ↔ information hiding

References: Module (Java SE 25 API), JEP 403 — Strongly Encapsulate JDK Internals

A module hides a decision that is likely to change. The criterion is testable: “what breaks if this decision is revisited”. SRP (1) is a blurry retelling of this principle.

A secret is a concrete decision: storage format, encoding, field order, algorithm. An interface that returns “a list of ASCII strings” has already leaked the encoding — moving to UTF-8 breaks every consumer. The same interface described as “grocery item names” would have survived that move unnoticed. The difference is not in the code but in what is declared to the outside.

Why Java needed a separate tier of visibility. Before the module system, public meant “visible to the whole classpath”: the language offered no way to hide a decision between packages of one library, and a helper class made public for a neighbouring package became, by Hyrum’s Law (16), someone else’s contract. exports adds a second tier: public and exported is API; public in a non-exported package does not exist from the outside. Nothing changes inside the module — packages still see each other’s public types, exported or not, and private/protected/public work as before; the module merely filters which part of the public surface is presented to the world. The file’s other directives have nothing to do with hiding: requires defines the dependency graph, opens selectively permits reflection, uses/provides wire up ServiceLoader.

Three words next to module path. Classpath is a list of locations on disk (directories and JARs) where the JVM may look for classes: launch configuration, not a mechanism, and it is flat — hence the whole story above. Module path is the same list, but with module-info read; only there does the second tier of visibility appear. ClassLoader is what finds bytecode by class name and loads it into the JVM, lazily, on first use; a custom loader is a unit of isolation (Tomcat gives one to each application, Flink to each job, child-first, so a job can bring its own library versions), and one class loaded by two loaders is two different types to the JVM: hence ClassCastException “between identical classes”. ServiceLoader means “find all implementations of an interface whose names I don’t know at compile time”: that is how JDBC finds drivers; uses/provides (88) is its contract in the module system, and before modules a text file in META-INF/services did the same job.

Code example (fragment)
fragment
Java
// module system (module-info.java) — hiding at the artifact level
module billing.core {
    requires transitive billing.model;  // visible to consumers: part of the API
    exports com.acme.billing.api;       // API only

    // com.acme.billing.internal is neither exported nor opened:
    // normal access is denied, and so is deep reflection (setAccessible, the
    // historical bypass of any modifier). Access can still be granted explicitly —
    // opens, Module.addOpens(), --add-opens: this is a default boundary,
    // not a physical impossibility.
}
Rust
// pub(crate) / private by default — hiding is built into the language
pub struct RateLimiter { /* fields are private */ }

pub(crate) mod internal;   // visible inside the crate, invisible to consumers
Go
// internal/ package — the compiler forbids imports from outside the subtree
// myservice/internal/ratelimit — physically unreachable from other modules
Python
# _prefix + __all__ convention; no enforced hiding — and that's honest
__all__ = ["RateLimiter"]   # the public contract is declared explicitly

Last technical review: 2026-08-04 · Reference · 112 principles