Guide contents

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.

Text
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.App

The program below prints where its own class and two JDK classes live.

Java
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"));
    }
}
Output
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: false

The 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.

Text
// 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.

Java
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\""));
        }
    }
}
Output
found field value of type byte[]
InaccessibleObjectException
not opened: true

Finding 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.

Java
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);
    }
}
Output
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.

Text
rates.jar
  META-INF/services/com.acme.pricing.spi.RateSource    contains: com.acme.rates.EcbRateSource
  com/acme/rates/EcbRateSource.class

The JDK finds its own compiler this way. javax.tools.ToolProvider.getSystemJavaCompiler() is a thin wrapper over the same lookup.

Java
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);
    }
}
Output
[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.

Java
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());
    }
}
Output
[AAPL, MSFT] {AAPL=189.50} today.csv

One 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.

Java
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");
        }
    }
}
Output
Account nested=true
balance private: true
balance = 150
balance=150
getMethod finds public only: NoSuchMethodException

Class.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.

Java
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()));
    }
}
Output
call price[AAPL]
  returned 400
call spread[MSFT, 3]
  returned 6
call price[]
  threw IllegalArgumentException
caller sees empty symbol
is proxy: true

The 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.

Java
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");
        }
    }
}
Output
fee 2500
length 6
10 bps of 500000 = 500
tag desk-1
invokeExact with wrong return type: WrongMethodTypeException

invokeExact 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.
  • exports gives access to public members; opens gives deep reflection, including private members. Frameworks that bind to private fields need opens.
  • JDK internals are strongly encapsulated since Java 16; Java 17 removed --illegal-access. --add-opens module/package=target is the remaining escape hatch.
  • requires transitive re-exports readability; requires static is compile-time only.
  • ServiceLoader finds providers through provides in modules or META-INF/services files on the class path.
  • import module (Java 25) imports all exported packages of a module; ambiguous simple names need a single-type import.
  • getDeclaredMethods returns all members declared in the class; getMethods returns public members including inherited ones.
  • Reflective calls wrap thrown exceptions in InvocationTargetException.
  • Proxy only implements interfaces; class proxies need bytecode generation libraries.
  • MethodHandle checks access at lookup; invokeExact requires 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.java declares requires, exports, opens, uses, and provides.
  • InaccessibleObjectException means the target package is not opened to the caller.
  • ServiceLoader decouples an interface from its implementations.
  • Reflection reads and calls members by name; setAccessible needs the package to be open to the caller.
  • Dynamic proxies and method handles are the building blocks of framework interception and fast indirect calls.