Lesson 9 of 71
Packages, Modules, and Reflection
This lesson covers how the JVM finds classes (class path and module path), how modules control what other code may see, and how a program inspects and calls code at run time (reflection, dynamic proxies, method handles). After it the reader can write a module-info.java, diagnose an InaccessibleObjectException, and read the reflection code inside frameworks.
| Topic | API or file |
|---|---|
| Finding classes | class path, JAR files, Class.getPackageName |
| Modules | module-info.java, requires, exports, opens, provides, uses |
| Encapsulation | strong encapsulation (Java 16 and 17), --add-opens |
| Services | ServiceLoader |
| Imports | import module (Java 25) |
| Reflection | Class, Field, Method, Constructor, setAccessible |
| Indirection | java.lang.reflect.Proxy, MethodHandle |
Example 1: packages, the class path, and the unnamed module
A package is a namespace for classes; its name maps to a directory (com.acme.pricing lives in com/acme/pricing/). The class path is the list of directories and JAR files the JVM searches for classes, set with -cp. A JAR is a zip file of class files plus a META-INF/MANIFEST.MF file. Code on the class path belongs to the unnamed module, which reads every other module.
app.jar
META-INF/MANIFEST.MF Main-Class: com.acme.App
com/acme/App.class
com/acme/pricing/Pricer.class
java -cp app.jar:libs/fastutil.jar com.acme.AppThe program below prints where its own class and two JDK classes live.
public class WhereClassesLive {
public static void main(String[] args) {
Module own = WhereClassesLive.class.getModule();
System.out.println("own package '" + WhereClassesLive.class.getPackageName() + "'");
System.out.println("own module named: " + own.isNamed());
System.out.println("String in " + String.class.getModule().getName()
+ ", package " + String.class.getPackageName());
System.out.println("Connection in " + java.sql.Connection.class.getModule().getName());
System.out.println("java.base exports java.lang: "
+ String.class.getModule().isExported("java.lang"));
System.out.println("java.base exports jdk.internal.misc: "
+ String.class.getModule().isExported("jdk.internal.misc"));
}
}own package ''
own module named: false
String in java.base, package java.lang
Connection in java.sql
java.base exports java.lang: true
java.base exports jdk.internal.misc: falseThe class has no package declaration, so its package name is empty. The JDK itself is split into modules: java.base holds java.lang and java.util, java.sql holds JDBC. jdk.internal.misc exists inside java.base but is not exported.
The class path has two known problems. Two JARs can contain the same class, and the first one found wins (split packages and version conflicts). A missing JAR shows up only as NoClassDefFoundError when a class is first used.
Example 2: module-info.java
A module is a named set of packages with a descriptor, module-info.java, at the root of its sources. The descriptor states what the module needs and what it offers. A multi-module build needs several files, so the files are shown as text and are not run here.
// pricing/src/module-info.java
module com.acme.pricing {
requires java.sql; // reads java.sql
requires transitive com.acme.model; // readers of pricing also read model
exports com.acme.pricing.api; // public types usable at compile and run time
opens com.acme.pricing.dto to com.fasterxml.jackson.databind; // deep reflection, one module
uses com.acme.pricing.spi.RateSource; // looks up this service
}
// rates/src/module-info.java
module com.acme.rates {
requires com.acme.pricing;
provides com.acme.pricing.spi.RateSource with com.acme.rates.EcbRateSource;
}
javac -d out --module-source-path "./*/src" -m com.acme.pricing,com.acme.rates
java --module-path out -m com.acme.pricing/com.acme.pricing.Main| Directive | Effect |
|---|---|
requires M |
this module reads M; without it, M's types do not compile here |
requires transitive M |
modules that read this one also read M |
requires static M |
needed at compile time, optional at run time |
exports p |
public types of p are accessible to other modules |
exports p to M |
qualified export, only to M |
opens p |
all members of p, including private ones, are open to reflection at run time |
uses S / provides S with Impl |
service consumer and provider |
exports and opens are separate: an exported package allows normal calls to public members, an opened package allows setAccessible(true) on private members. Frameworks that read private fields (JSON mappers, ORMs) need opens. Modules on the module path are checked at startup: a missing required module or two modules with the same package stop the JVM before main runs.
Example 3: strong encapsulation and --add-opens
Since Java 16 the JDK's internal packages are strongly encapsulated by default, and Java 17 removed the --illegal-access switch that relaxed this. Deep reflection into a package that is not opened throws InaccessibleObjectException.
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
public class StrongEncapsulation {
public static void main(String[] args) throws Exception {
Field value = String.class.getDeclaredField("value");
System.out.println("found field " + value.getName() + " of type " + value.getType().getSimpleName());
try {
value.setAccessible(true);
System.out.println("accessible");
} catch (InaccessibleObjectException e) {
System.out.println("InaccessibleObjectException");
System.out.println("not opened: " + e.getMessage().contains("does not \"opens java.lang\""));
}
}
}found field value of type byte[]
InaccessibleObjectException
not opened: trueFinding the field works: reflection can always see that a member exists. Making it accessible fails because java.base does not open java.lang. The same program run with --add-opens java.base/java.lang=ALL-UNNAMED succeeds; ALL-UNNAMED means the class path.
import java.lang.reflect.Field;
public class AddOpens {
public static void main(String[] args) throws Exception {
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
byte[] bytes = (byte[]) value.get("abc");
System.out.println("internal length " + bytes.length);
}
}internal length 3--add-opens is a migration tool for old libraries. Each one in a start script is a dependency on a JDK internal that may change in any release.
Example 4: services with ServiceLoader
A service is an interface whose implementations are found at run time. A consumer calls ServiceLoader.load(S.class) and iterates the providers. Modules declare providers with provides; class path JARs list them in a file META-INF/services/<interface name> that contains implementation class names, one per line.
rates.jar
META-INF/services/com.acme.pricing.spi.RateSource contains: com.acme.rates.EcbRateSource
com/acme/rates/EcbRateSource.classThe JDK finds its own compiler this way. javax.tools.ToolProvider.getSystemJavaCompiler() is a thin wrapper over the same lookup.
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import javax.tools.JavaCompiler;
public class ServiceLookup {
public static void main(String[] args) {
List<String> providers = new ArrayList<>();
for (JavaCompiler compiler : ServiceLoader.load(JavaCompiler.class)) {
providers.add(compiler.getClass().getName()
+ " in " + compiler.getClass().getModule().getName());
}
System.out.println(providers);
}
}[com.sun.tools.javac.api.JavacTool in jdk.compiler]The interface lives in java.compiler and the implementation in jdk.compiler. The consumer depends only on the interface, so a different provider can replace the implementation without a code change. Providers need a public no-argument constructor or, in a module, a public static provider() method.
Example 5: module import declarations
Java 25 finalized module import declarations (JEP 511). import module M; imports every public top-level type in every package that M exports, plus the packages of modules that M requires transitively.
import module java.base;
public class ModuleImport {
public static void main(String[] args) {
List<String> symbols = new ArrayList<>(List.of("MSFT", "AAPL"));
Collections.sort(symbols);
Map<String, BigDecimal> prices = new TreeMap<>();
prices.put("AAPL", new BigDecimal("189.50"));
Path path = Path.of("quotes", "today.csv");
System.out.println(symbols + " " + prices + " " + path.getFileName());
}
}[AAPL, MSFT] {AAPL=189.50} today.csvOne line replaces imports from java.util, java.math, and java.nio.file. Two modules can export types with the same simple name (java.util.List and java.awt.List with import module java.desktop); a use of the ambiguous name is a compile error, fixed by adding a single-type import, which takes precedence. The import does not require the importing code to be in a module.
Example 6: reflection with Class, Field, Method, and Constructor
Reflection is the java.lang.reflect API for inspecting and using classes at run time. A Class object gives access to Constructor, Field, and Method objects. getDeclaredX returns members declared in that class of any visibility; getX returns public members including inherited ones.
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public class ReflectionBasics {
static class Account {
private long balance;
public Account(long balance) { this.balance = balance; }
public void deposit(long amount) { balance += amount; }
private String secret() { return "balance=" + balance; }
}
public static void main(String[] args) throws Exception {
Class<?> type = Class.forName("ReflectionBasics$Account");
System.out.println(type.getSimpleName() + " nested=" + type.isMemberClass());
Constructor<?> ctor = type.getConstructor(long.class);
Object account = ctor.newInstance(100L);
Method deposit = type.getMethod("deposit", long.class);
deposit.invoke(account, 50L);
Field balance = type.getDeclaredField("balance");
System.out.println("balance private: " + Modifier.isPrivate(balance.getModifiers()));
balance.setAccessible(true);
System.out.println("balance = " + balance.getLong(account));
Method secret = type.getDeclaredMethod("secret");
secret.setAccessible(true);
System.out.println(secret.invoke(account));
try {
type.getMethod("secret");
} catch (NoSuchMethodException e) {
System.out.println("getMethod finds public only: NoSuchMethodException");
}
}
}Account nested=true
balance private: true
balance = 150
balance=150
getMethod finds public only: NoSuchMethodExceptionClass.forName loads a class by its binary name; a nested class uses $. setAccessible(true) works here because the class is in the caller's own (unnamed) module. Exceptions thrown by an invoked method arrive wrapped in InvocationTargetException; getCause() returns the original. Reflective calls box primitive arguments and pass them in an Object[], so they are slower than direct calls. Since Java 18 (JEP 416) Method.invoke is itself implemented with method handles (Example 8).
Example 7: dynamic proxies
A dynamic proxy is a class generated at run time that implements given interfaces and sends every call to an InvocationHandler. Frameworks use proxies for transactions, retries, and remote calls. The handler here is an anonymous class.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class DynamicProxy {
interface PriceService {
long price(String symbol);
long spread(String symbol, int levels);
}
static class RealPriceService implements PriceService {
public long price(String symbol) {
if (symbol.isEmpty()) throw new IllegalArgumentException("empty symbol");
return symbol.length() * 100L;
}
public long spread(String symbol, int levels) { return levels * 2L; }
}
@SuppressWarnings("unchecked")
static <T> T logged(Class<T> api, T target) {
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
System.out.println("call " + m.getName() + Arrays.toString(args));
try {
Object result = m.invoke(target, args);
System.out.println(" returned " + result);
return result;
} catch (InvocationTargetException e) {
System.out.println(" threw " + e.getCause().getClass().getSimpleName());
throw e.getCause();
}
}
};
return (T) Proxy.newProxyInstance(api.getClassLoader(), new Class<?>[] {api}, handler);
}
public static void main(String[] args) {
PriceService service = logged(PriceService.class, new RealPriceService());
service.price("AAPL");
service.spread("MSFT", 3);
try {
service.price("");
} catch (IllegalArgumentException e) {
System.out.println("caller sees " + e.getMessage());
}
System.out.println("is proxy: " + Proxy.isProxyClass(service.getClass()));
}
}call price[AAPL]
returned 400
call spread[MSFT, 3]
returned 6
call price[]
threw IllegalArgumentException
caller sees empty symbol
is proxy: trueThe handler unwraps InvocationTargetException so the caller sees the original exception type. java.lang.reflect.Proxy only implements interfaces; libraries that proxy classes (Spring with CGLIB, Mockito with ByteBuddy) generate subclasses instead.
Example 8: MethodHandle basics
A method handle (java.lang.invoke.MethodHandle) is a typed, directly executable reference to a method, constructor, or field. A MethodHandles.Lookup object creates handles and checks access once, at lookup time, not on every call. A MethodType describes the return and parameter types.
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class MethodHandleBasics {
static long fee(long notional, int bps) { return notional * bps / 10_000; }
private String tag = "desk-1";
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle feeHandle = lookup.findStatic(MethodHandleBasics.class, "fee",
MethodType.methodType(long.class, long.class, int.class));
long fee = (long) feeHandle.invokeExact(1_000_000L, 25);
System.out.println("fee " + fee);
MethodHandle length = lookup.findVirtual(String.class, "length",
MethodType.methodType(int.class));
System.out.println("length " + (int) length.invokeExact("EURUSD"));
MethodHandle bound = MethodHandles.insertArguments(feeHandle, 1, 10);
System.out.println("10 bps of 500000 = " + (long) bound.invokeExact(500_000L));
MethodHandle getter = lookup.findGetter(MethodHandleBasics.class, "tag", String.class);
System.out.println("tag " + (String) getter.invokeExact(new MethodHandleBasics()));
try {
int wrong = (int) feeHandle.invokeExact(1_000_000L, 25);
System.out.println(wrong);
} catch (java.lang.invoke.WrongMethodTypeException e) {
System.out.println("invokeExact with wrong return type: WrongMethodTypeException");
}
}
}fee 2500
length 6
10 bps of 500000 = 500
tag desk-1
invokeExact with wrong return type: WrongMethodTypeExceptioninvokeExact requires the call site's types, including the cast on the result, to match the handle's type exactly; a mismatch throws WrongMethodTypeException. invoke converts arguments instead. lookup() in the class itself can reach its private field tag. A handle stored in a static final field can be inlined by the JIT compiler like a direct call. The JDK uses method handles, through the invokedynamic instruction, to implement lambdas and string concatenation. Lambdas and functional interfaces cover the lambda side.
| Mechanism | Access check | Typical use |
|---|---|---|
Method.invoke |
at call time, result cached per caller class | frameworks, tooling, one-off calls |
MethodHandle |
once at lookup | libraries needing near-direct speed |
Proxy |
none, routes calls to a handler | interface interception |
Interview points
- The class path has no notion of dependencies; split packages and missing JARs appear only at run time. The module path checks both at startup.
exportsgives access to public members;opensgives deep reflection, including private members. Frameworks that bind to private fields needopens.- JDK internals are strongly encapsulated since Java 16; Java 17 removed
--illegal-access.--add-opens module/package=targetis the remaining escape hatch. requires transitivere-exports readability;requires staticis compile-time only.ServiceLoaderfinds providers throughprovidesin modules orMETA-INF/servicesfiles on the class path.import module(Java 25) imports all exported packages of a module; ambiguous simple names need a single-type import.getDeclaredMethodsreturns all members declared in the class;getMethodsreturns public members including inherited ones.- Reflective calls wrap thrown exceptions in
InvocationTargetException. Proxyonly implements interfaces; class proxies need bytecode generation libraries.MethodHandlechecks access at lookup;invokeExactrequires an exact type match.
Summary
- Packages are namespaces; JARs are zips of classes; the class path makes all of them part of the unnamed module.
module-info.javadeclaresrequires,exports,opens,uses, andprovides.InaccessibleObjectExceptionmeans the target package is not opened to the caller.ServiceLoaderdecouples an interface from its implementations.- Reflection reads and calls members by name;
setAccessibleneeds the package to be open to the caller. - Dynamic proxies and method handles are the building blocks of framework interception and fast indirect calls.
