skip to content

In the Java Platform Module System (JPMS), why isn't an import enough to use a type from another module, and how do imports relate to module readability and exported packages?

level: seniorimportance: nice to knowfreq 24%

answer

  1. Import = naming only; module system = access/reachability
  2. exports (in provider) + requires (in consumer) = visibility
  3. public + non-exported package = unreachable across modules
  4. Classpath code = unnamed module reads everything (old behavior)
  5. opens governs reflection; exports governs compile-time use

basics

~20 s

An import only tells the compiler how to read a short name. Under the module system (since Java 9), the type's package must also be exported by its module and your module must require that module. Without those, the import fails even if the class exists.

solid answer

~50 s

Imports are purely a **compile-time naming** mechanism — they map a simple name to a fully qualified type. They do NOT grant access. Since Java 9, the **Java Platform Module System (JPMS)** adds a second, stronger gate: a module declares `exports some.package;` to make that package's public types visible to others, and a consuming module declares `requires that.module;` to establish **readability**. Only when both hold can you compile and run code referencing the type — and the import then just shortens its name. So you can have a perfectly valid `import com.lib.Service;` that fails because `com.lib` isn't exported, or because your module doesn't `require` the library module. Strong encapsulation means even public types in non-exported packages are unreachable. On the classpath (no module-info) the old behavior persists via the 'unnamed module', which reads everything — which is why pre-modular code still works.

code

java · 14 lines
java
// module-info.java of the library
module com.acme.lib {
    exports com.acme.lib.api;     // visible to consumers
    // package com.acme.lib.internal is hidden (strong encapsulation)
}

// module-info.java of the consumer
module com.acme.app {
    requires com.acme.lib;        // readability
}

// In the consumer's code:
import com.acme.lib.api.Service;      // compiles: package exported + module required
// import com.acme.lib.internal.Impl; // FAILS: package not exported, despite the import

go deeper

for a junior

Can say imports just shorten names and that newer Java has a module system; not expected to know the export/requires details.

for a middle

Knows imports don't grant access and that modules add export/requires rules, but may be fuzzy on edge cases.

for a senior

Clearly separates naming (import) from access (modifiers + module readability/exports), explains strong encapsulation of non-exported public types, and knows the unnamed-module fallback.

for a principal

Reasons about module API design (which packages to export, qualified exports, opens for reflection), migration strategy from classpath to module path, and how strong encapsulation shapes a library's published surface.

## Two separate concerns: naming vs. access It's vital to separate two ideas that beginners conflate: 1. **Naming** — how a short name like `List` maps to a type. This is what `import` does. Imports are *only* about names; they emit no bytecode and grant no permission. 2. **Access / reachability** — whether your code is *allowed* to use that type at all. This is governed by access modifiers (`public`/`private`/…) and, since Java 9, by the **module system**. An import can be syntactically valid yet the code still won't compile because access is denied. ## Pre-Java-9 world Before modules, access was governed only by access modifiers and the classpath. If a `public` type was on the classpath, an `import` plus the public modifier was enough. Everything public was reachable. ## JPMS (Java 9+): the module gate The **Java Platform Module System** introduced *strong encapsulation*. A module is described by a `module-info.java`: ```java // in the library module module com.acme.lib { exports com.acme.lib.api; // only this package is visible to others // com.acme.lib.internal is NOT exported -> hidden } // in the consuming module module com.acme.app { requires com.acme.lib; // establishes 'readability' } ``` Now, to use `com.acme.lib.api.Service` from `com.acme.app`: 1. `com.acme.lib` must `exports com.acme.lib.api;` (the package must be exported). 2. `com.acme.app` must `requires com.acme.lib;` (readability). 3. The type must be `public` (access modifier). Only then does `import com.acme.lib.api.Service;` compile. The import itself is still just a name shortcut — it never overrides the module rules. ### Consequences - A **public** class in a **non-exported** package (`com.acme.lib.internal`) is **unreachable** from other modules, even with a correct import. This is *strong encapsulation* — 'public' no longer means 'public to the whole world'. - Forgetting `requires` produces a compile error even though the package is exported and the import is correct. - `exports ... to some.module;` (qualified export) restricts visibility to named modules only. - `opens` (vs `exports`) governs *reflective* access, separate from compile-time imports. ## The unnamed module and the classpath Code placed on the **classpath** (not the module path, no `module-info`) lands in the **unnamed module**, which: - *reads* every other module, and - can access exported packages of all modules. This backwards-compatibility bridge is why pre-modular applications keep working: their imports behave as before. Mixing classpath and module path has nuanced rules, but the headline is: on the plain classpath, imports + public access suffice (old behavior); on the module path, the export/requires gate also applies. ## Mental model Think of it as **three locks** a cross-module reference must pass: (1) the **import** (name), (2) the **access modifier** (`public`), (3) the **module readability + export** (`requires` + `exports`). Imports are the cheapest, weakest lock — they decide nothing about permission.

  • You wrote a correct `import com.lib.api.Service;` but it won't compile under JPMS. Name two module-level causes.
    Either the providing module didn't `exports com.lib.api;`, or your module is missing `requires com.lib;`. Both establish cross-module visibility/readability that the import alone doesn't provide.
  • Why does old classpath-based code keep compiling without module-info?
    It runs in the 'unnamed module', which reads all modules and accesses their exported packages, preserving the pre-Java-9 'public + import is enough' behavior.

saying these in an interview costs you the question

  • Thinking an import grants access to a type
  • Believing 'public' alone makes a type usable across modules
  • Confusing `exports` (compile-time) with `opens` (reflection)
  • Assuming sub-packages are exported when the parent is exported

context