Can a type be `public` in Java yet still be inaccessible from another module? Explain why.
answer
- public is no longer absolute under JPMS
- Cross-module access = public + package exported + reader reads module
- Non-exported public class = visible inside its module only
- Two extra tiers: public-to-some (qualified exports), public-within-module (no exports)
- exports controls normal access; opens controls reflection — both independent of `public`
basics
~20 sYes. Before modules, public meant accessible everywhere. With modules, a public type is only accessible from another module if its package is also exported and the other module reads yours. So public no longer guarantees access.
solid answer
~50 sYes — this is one of the biggest behavioral changes JPMS introduced. The Java Language Specification redefined accessibility so that for code in module M2 to access a `public` member of a type in module M1, three things must all hold: the type's package is exported by M1 (or qualified-exported to M2), the member is `public`, and M2 reads M1. If the package is not exported, the `public` type is invisible across the module boundary even though the `public` keyword is present — within the same module it is still accessible. People summarize this as JPMS adding two new access levels above the old four: 'public only within a module' (exported to nobody) and 'public to specific modules' (qualified `exports ... to`). So `public` now means 'public *to whoever can read and is allowed to see the package*', not 'public to the whole world'. This is what enforces strong encapsulation of internal-but-public utility classes.
code
java · 14 lines// module M1 — com.m1.internal is intentionally NOT exported
module M1 {
exports com.m1.api; // visible to others
// com.m1.internal: no exports => hidden across modules
}
package com.m1.internal;
public class Helper { // public, but only usable INSIDE M1
public void run() {}
}
// module M2
module M2 { requires M1; }
// new com.m1.internal.Helper(); // ERROR: package not exported, inaccessiblego deeper
Recognizes that with modules public alone may not be enough to use a class from elsewhere.
States the three conditions for cross-module access and that a non-exported public class is module-internal.
Explains the redefined accessibility, the qualified-exports tier, that exports/opens are independent of public, and the migration --add-exports/--add-opens escape hatches.
Uses these semantics to design clean module APIs (internal public helpers vs exported API), governs --add-exports usage in builds, and plans classpath-to-module migrations without leaking internals.
## The old meaning of `public` Before Java 9, the four Java access levels were `private`, package-private (default), `protected`, and `public`. `public` was absolute: a `public` type or member was accessible from *any* code on the classpath. There was no way for a library to have a class that was `public` (so its own packages could use it across package boundaries) yet hidden from external callers — the infamous `sun.misc.Unsafe` problem, where 'internal' public classes leaked everywhere. ## What JPMS changed The **Java Platform Module System (JPMS)** redefined accessibility. Now, for code in **module M2** to access a `public` member of a type declared in **module M1**, ALL of the following must be true: 1. **The package is exported.** M1's `module-info.java` contains `exports p;` for the package `p` of the type — or a qualified `exports p to M2;`. 2. **The member is public** (and the type is public), as before. 3. **M2 reads M1.** M2 has `requires M1;` (or readability is implied). If the package is *not* exported, then a `public` type in it is **not accessible from another module** — full stop. Inside M1's own module it remains accessible across its packages, but the module boundary hides it from outsiders. ## Why this means `public` ≠ accessible Consider: ```java // module M1 module M1 { // note: package com.m1.internal is NOT exported } package com.m1.internal; public class Helper { // public! public void doWork() { } } ``` From another module M2 (even with `requires M1;`), `new com.m1.internal.Helper()` **fails to compile / is inaccessible at run time**, because `com.m1.internal` is not exported. The `public` keyword is there, but accessibility across modules is gated by `exports`. This is intentional: it lets a module mark a class `public` so its *own* packages can collaborate, while keeping it hidden from the outside world — true strong encapsulation, which was impossible before. ## The 'new access levels' framing A common interview framing: JPMS effectively adds two more access tiers on top of the original four: - **public to everyone** — package is exported unqualified (`exports p;`). - **public to specific modules** — package is qualified-exported (`exports p to M2, M3;`); only those modules see it. - **public only within the module** — package is not exported at all; `public` works inside M1 but nowhere else. So there are now, in effect, three flavors of 'public' depending on the `exports` declaration. ## Reflection is a separate axis Note that even an *exported* `public` type does not automatically permit *reflective* access to its non-public members from another module — that needs `opens`. And a non-exported package can still be made reflectable with `opens` (e.g., `opens com.m1.internal to com.framework;`) without becoming normally accessible. So 'accessible for normal use' (exports) and 'accessible for deep reflection' (opens) are independent of the `public` keyword. ## Practical consequences - Migrating a classpath app to modules can break code that relied on internal `public` classes — they may no longer be exported. - Library authors gain a real tool: keep helper classes `public` for internal cross-package use but never export their package. - Tooling that previously reached any public class now needs `--add-exports`/`--add-opens` workarounds when packages are not exported. ## One-line answer Yes: with modules, `public` only grants cross-module access when the type's package is also exported and the consumer reads the module. `public` now means 'public to whoever can see the package', not 'public to all'.
- How can you access a non-exported public type from another module without changing module-info?Use the launcher/compiler flags `--add-exports M1/com.m1.internal=M2` (for normal access) or `--add-opens` (for reflective access). These are escape hatches for migration and tooling; they are not a design substitute for proper `exports`/`opens` directives.
- Does exporting a package give reflective access to a public type's private fields?No. `exports` only grants normal access to public members. Reflective access to non-public members (including `setAccessible(true)`) requires `opens` on the package, independently of `exports` and of the `public` keyword.
public used to be a megaphone heard by the whole city. Under modules it's more like speaking loudly inside your own building; outsiders only hear you if you also open a window to them (export the package).
saying these in an interview costs you the question
- Insisting `public` always means accessible everywhere — false under modules.
- Saying a non-exported public class is unusable even within its own module — it is usable inside the module.
- Conflating exports with opens when explaining why a public type is or isn't reachable.
- Claiming `--add-exports` changes the `public` keyword's meaning — it just opens a specific package edge.