1. Exporting packages: who can see your code?
Before Java 9, life was simple — just not very safe. If a class was public, anyone could see it as long as they had access to the JAR or classpath. This led to internal classes and even whole packages being used outside your project accidentally or intentionally. Yes, you could hide things with private or package-private, but if a class is public, it’s available to everyone.
With modules, this changed. Now even if a class is declared public, it can’t be used from another module unless the package it belongs to is explicitly exported via module-info.java.
What it looks like in practice
Suppose you have the following project structure:
my-app/
src/
core/
com/example/core/
CoreService.java
InternalHelper.java
module-info.java
app/
com/example/app/
Main.java
module-info.java
In core/module-info.java you write:
module core {
exports com.example.core;
}
Then only the classes from the com.example.core package will be available to other modules. Everything in other packages (for example, com.example.core.internal) will be invisible — even if there are public classes there. That’s modular encapsulation.
Mini demo: “visible/invisible”
Inside core:
// com/example/core/CoreService.java
package com.example.core;
public class CoreService {
public void doWork() {
System.out.println("Work done!");
}
}
Inside app:
// com/example/app/Main.java
package com.example.app;
import com.example.core.CoreService;
public class Main {
public static void main(String[] args) {
CoreService service = new CoreService();
service.doWork();
}
}
If you remove the line exports com.example.core; from core/module-info.java, you’ll get a compilation error:
error: package com.example.core is not visible
Even if CoreService is public!
2. Importing dependencies: requires and strict isolation
In JPMS, you can’t just use a class from another module. You need to declare the dependency explicitly via requires.
Example
In app/module-info.java:
module app {
requires core;
}
Now the app module can use everything that the core module exports.
If you forget to add requires core;, you’ll get a compilation error:
error: package com.example.core is not visible
or
error: cannot access CoreService
Important note
- requires works at the module level, not at the package level.
- You can’t “import” just one package — only the whole exported module.
3. Comparison: modules vs public/private
Modules add a new level of encapsulation that sits “above” classes and packages.
| Level | What does it control? | How does it work? |
|---|---|---|
| private | Access within a class | Only within a single file |
| package-private (default) | Access within a package | All classes in one package |
| public | Access for everyone | Any code anywhere |
| module | Access between modules | Only exported packages |
Key idea:
- A class can be public, but if its package isn’t exported, it’s available only within the module.
- Exporting a package via exports is like a “window” through which other modules can see your code.
Example: hidden implementation
// com/example/core/internal/SecretSauce.java
package com.example.core.internal;
public class SecretSauce {
public void addMagic() {}
}
If you do NOT export the com.example.core.internal package in module-info.java, no external module will be able to use this class, even if it’s public!
4. Example: core and app — API and implementation
Consider a typical scenario: the core module provides an API, the app module consumes it.
Structure:
core/
com/example/core/
CoreAPI.java
com/example/core/impl/
CoreImpl.java
module-info.java
app/
com/example/app/
Main.java
module-info.java
core/module-info.java:
module core {
exports com.example.core; // API only!
// We do not export com.example.core.impl
}
app/module-info.java:
module app {
requires core;
}
com/example/core/CoreAPI.java:
package com.example.core;
public interface CoreAPI {
void doSomething();
}
com/example/core/impl/CoreImpl.java:
package com.example.core.impl;
import com.example.core.CoreAPI;
public class CoreImpl implements CoreAPI {
@Override
public void doSomething() {
System.out.println("Doing something!");
}
}
com/example/app/Main.java:
package com.example.app;
import com.example.core.CoreAPI;
// import com.example.core.impl.CoreImpl; // This will cause a compilation error!
public class Main {
public static void main(String[] args) {
// CoreImpl impl = new CoreImpl(); // Error! The package is not exported.
// You can use only what is visible through the API.
}
}
Result:
- The app module sees only what core exports.
- The implementation (impl) is hidden, even if the classes there are public.
5. Practice: playing with exports and visibility
Step 1: remove the export
In core/module-info.java, comment out the line:
// exports com.example.core;
Now try to compile the project. Expect a compilation error in the app module — it won’t see classes from com.example.core.
Step 2: export only the API
Restore the line exports com.example.core;, but don’t export com.example.core.impl. Try to import a class from impl in the app module. You’ll get a compilation error again — fair enough!
Step 3: a public class in a non-exported package
Create a public class in a non-exported package. Try to use it from another module — it won’t work. That’s modular encapsulation in action.
6. How it looks in the IDE and in plain terms
- If you try to import a class from a non-exported package, the IDE will highlight an error.
- The hint will explain that the package is not exported by the module.
- Add exports for the required package — the error will disappear.
Diagram: access levels
+---------------------+
| MODULE |
| (module-info.java) |
+---------------------+
|
v
+---------------------+
| PACKAGES |
| (package, export) |
+---------------------+
|
v
+---------------------+
| CLASSES |
| (public/private) |
+---------------------+
|
v
+---------------------+
| METHODS/FIELDS |
| (public/private) |
+---------------------+
7. Important nuances and details
“Friends-only” export: exports ... to
Sometimes you need to export a package only to specific modules (for example, for tests or special extensions):
exports com.example.core.internal to my.special.module, my.test.module;
Now only the specified modules will see this package.
Can you export multiple packages?
Yes! Just add more exports lines:
exports com.example.core;
exports com.example.core.api;
Can you export “everything”?
No. In Java’s module system you always explicitly specify what to export. That’s its strength.
8. Common mistakes when working with modular encapsulation
Mistake #1: Expecting that a public class is always visible.
If a package isn’t exported via module-info.java, the class will be invisible to other modules, even if it’s public. This is the most common “gotcha” for beginners.
Mistake #2: Forgot to declare requires.
If your module uses classes from another module but doesn’t declare requires, you’ll get a compilation error. Don’t forget to declare dependencies explicitly.
Mistake #3: Trying to export the same package from two modules.
In JPMS, each package can be exported by only one module. If you violate this rule, the compiler will stop you.
Mistake #4: Broken structure — module-info.java is in the wrong place.
The module-info.java file must live at the root of the module’s sources; otherwise the module won’t be recognized.
Mistake #5: Cyclic dependencies between modules.
If module A requires B and B requires A, you’ll get an error. Avoid cycles in the dependency graph.
GO TO FULL VERSION