Java / Intermediate / Lesson 12
Packages, classpath, and modules
Understand how source organization, binary names, dependency resolution, and encapsulation affect a packaged Java application.
On this page
Lesson goals
- Resolve a qualified class name from its search root.
- Distinguish classpath and module path.
- Describe requires, exports, and opens without bypassing encapsulation.
Treat a package as part of type identity
A package declaration contributes to a class's binary name. training.app.Main is different from an unnamed-package Main, even if both source files are named Main.java. imports help source code refer to types; they do not download dependencies or change runtime search paths. Two libraries defining the same binary name can cause classpath conflicts that are not solved by adding another import.
Compile packaged sources with a clear output root, commonly javac -d out, and launch java -cp out training.app.Main. The search root is above training/app, not inside it. A JAR packages entries under their binary-name paths and can include a manifest entry point. Keep the packaged artifact and its declared dependencies reproducible rather than relying on an IDE's hidden launch configuration.
Observe named and unnamed modules
On the classpath, this course's ordinary Main program belongs to an unnamed module. The platform String class belongs to the named java.base module. The example prints false and java.base respectively. This is an observation of the isolated launch mode, not proof that every Main in every project is unnamed. Running the same application as a named module changes its module context.
The Java Platform Module System was introduced in Java 9 and is available in the Java 17 baseline. A named module declares dependencies and package exports through module-info.java. Module identity is separate from package identity: one module may contain several packages. A package remains the unit used in qualified type names, while a module adds dependency and encapsulation structure.
public class Main {
public static void main(String[] args) throws Exception {
System.out.println(Main.class.getModule().isNamed());
System.out.println(String.class.getModule().getName());
}
}
false
java.base
Validation uses the classpath, so Main is unnamed; java.lang.String remains in java.base.
State module dependencies and exposure
requires declares a dependency on another module; exports makes a package's public API available under module accessibility rules. opens permits reflective access according to its scope and is not interchangeable with exports. Use qualified exports or opens when exposure should be limited to selected consumers. Avoid opening every internal package to silence one framework integration error without understanding why it needs access.
The descriptor below is an explanatory fragment for a module that actually contains training.api classes; by itself it is not a complete project. java.base is implicitly required. ServiceLoader can support service-provider designs through uses and provides declarations, but it does not remove API versioning or lifecycle obligations. A module boundary is useful only when its public contracts and dependencies are deliberate.
module training.core {
exports training.api;
}
This fragment needs a matching module source layout and package classes, so it remains non-runnable.
Inspect binary identity before moving files
Class.getName reports the binary name that runtime lookup uses. The example's Main is in the unnamed package and prints that fact. For packaged code, the name includes its package; for nested classes the binary name can contain a dollar sign. Source spelling and binary spelling can differ, so use compiler and packaging tools instead of inventing paths from a casual string transformation.
Use jar --list --file to inspect a local artifact's entries and javap to inspect a class when diagnosing a packaging problem. These are local inspection tools, not commands to attach to a production process. A dependency missing during execution can produce NoClassDefFoundError even though the application compiled against that dependency earlier. Compare the actual runtime artifact set with the build graph.
public class Main {
public static void main(String[] args) throws Exception {
System.out.println(Main.class.getName());
System.out.println(Main.class.getPackageName().isEmpty());
}
}
Main
true
The complete example has no package declaration, so its binary name is Main and its package is unnamed.
Separate resolution, access, and version failures
A missing class, an unresolved module, an inaccessible package, and an unsupported class-file version are different failures. First capture the exact launcher command, JDK version, artifact identity, and error. Avoid mixing classpath and module-path fixes indiscriminately. --add-opens or --add-exports changes an access boundary; it does not supply a missing dependency or make newer bytecode load on an older JVM.
Test the packaged artifact using the intended launch mode, not only an IDE run. Verify clean builds, explicit dependencies, resource placement, and supported runtime versions in an isolated directory. Module and packaging discipline makes deployment behavior explainable and reproducible; it is not a reason to add complexity to a small single-file exercise before the boundary is needed.
Common errors and fixes
ClassNotFoundException
- Why it happens
- A requested binary name is absent from the configured loader search.
- How to fix it
- Verify the qualified name, artifact entries, and search root.
- Prevent it next time
- Test the packaged launch command.
FindException: Module not found
- Why it happens
- A required module is missing or incorrectly placed on the module path.
- How to fix it
- Inspect module identity and supply the approved dependency set.
- Prevent it next time
- Keep build and runtime dependency graphs aligned.
InaccessibleObjectException
- Why it happens
- Reflective access crossed a module boundary that is not open.
- How to fix it
- Use a supported API or a narrowly justified module configuration.
- Prevent it next time
- Avoid blanket access-bypass flags.
Check your understanding
1. The class is training.app.Main and output is out/training/app/Main.class. What launch command fits?
Show worked solution
java -cp out training.app.Main. The classpath root is out and the binary name includes the package. Using -cp out/training/app Main confuses the package with its search root.
2. Does exports training.api permit unrestricted reflection into all internal packages?
Show worked solution
No. exports exposes the named package's public API under access rules. opens controls reflective access and can be qualified. Neither declaration automatically exposes unrelated internal packages, so choose exposure deliberately.
Key takeaways
- A binary name includes package identity.
- Classpath and module path solve different resolution contracts.
- Exports, opens, and runtime-version compatibility are separate concerns.