Practice: Modules
Five exercises covering what the slides in this module introduced —
requiresvs.requires transitive(implied readability),exportsvs.exports ... to(qualified exports),opensfor reflective access, theuses/provides ... withServiceLoader pattern, and the syntax rules governingmodule-info.javaitself. Try to answer before opening each explanation.
Exercise 1 — requires vs requires transitive
java// module com.example.api
module com.example.api {
exports com.example.api;
}java// module com.example.service
module com.example.service {
requires com.example.api; // plain requires, NOT transitive
exports com.example.service.api;
}java// com.example.service.api.ServiceFactory
package com.example.service.api;
import com.example.api.Widget;
public class ServiceFactory {
public static Widget createWidget() {
return new Widget();
}
}java// module com.example.client
module com.example.client {
requires com.example.service; // does NOT requires com.example.api
}javapackage com.example.client;
import com.example.service.api.ServiceFactory;
import com.example.api.Widget; // <-- line in question
public class Main {
public static void main(String[] args) {
Widget w = ServiceFactory.createWidget();
}
}Does com.example.client compile as written? If not, what's the minimal
fix to module-info.java that resolves it without touching client's
own module-info.java?
Answer
It does not compile. com.example.client only has requires com.example.service; — it never reads com.example.api directly, and
because com.example.service used a plain requires com.example.api;
(not requires transitive), that readability is not propagated
downstream. import com.example.api.Widget; and the Widget w = ...
declaration both fail: client simply cannot see the com.example.api
module.
The fact that ServiceFactory.createWidget() — a method client can
see — returns a Widget doesn't help; readability is checked per
consuming module, not inferred from what types happen to flow through a
method signature.
Minimal fix (without touching client): change service's
directive to requires transitive com.example.api;. This grants
implied readability — any module that requires com.example.service
automatically reads com.example.api too, exactly because service's
own exported API (ServiceFactory.createWidget()) exposes an
com.example.api type in its signature. That's precisely the rule of
thumb from the slides: use requires transitive whenever your exported
API leaks a dependency's types through parameters or return values.
(The alternative fix — adding requires com.example.api; directly to
client's own module-info.java — also works, but the prompt asked for a
fix that doesn't touch client.)
Exercise 2 — Qualified exports ... to and who actually gets access
java// module com.example.library
module com.example.library {
exports com.example.library.api;
exports com.example.library.internal to com.example.testing;
}java// module com.example.testing
module com.example.testing {
requires com.example.library;
}javapackage com.example.testing;
import com.example.library.internal.Diagnostics; // (1)
public class DiagnosticsRunner {
public static void run() {
Diagnostics.dump();
}
}java// module com.example.consumer
module com.example.consumer {
requires com.example.library;
}javapackage com.example.consumer;
import com.example.library.internal.Diagnostics; // (2)
public class App {
public static void main(String[] args) {
Diagnostics.dump();
}
}Both com.example.testing and com.example.consumer declare requires com.example.library;. Do lines (1) and (2) both compile? If not, why
does requiring the module not guarantee the same package access for
both?
Answer
Line (1) compiles; line (2) does not.
requires only establishes module-level readability — it lets
testing and consumer see whatever com.example.library chooses to
export, nothing more. Package-level access is a separate, finer-grained
decision made by the exporting module's exports directives.
com.example.library.internal is exported with a qualifier: exports com.example.library.internal to com.example.testing;. That restricts
compile-time visibility of that specific package to only the modules
named in the to list — here, exclusively com.example.testing. Any
module not on that list, including com.example.consumer (which reads
the module just fine but isn't on the package's allow-list), sees
com.example.library.internal as if it doesn't exist. The import in
App fails to compile for exactly the same reason an import of a
genuinely nonexistent package would fail.
This is the key distinction the slides draw: requires controls "can I
read this module at all," while exports/exports ... to controls
"which of that module's packages, and for whom."
Exercise 3 — opens grants reflection, not compile-time visibility
java// module com.example.app
module com.example.app {
exports com.example.app.api;
opens com.example.app.model; // unqualified opens — no exports
}java// module com.example.tool
module com.example.tool {
requires com.example.app;
}javapackage com.example.tool;
import com.example.app.model.User; // (1)
public class Loader {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.example.app.model.User"); // (2)
var field = clazz.getDeclaredField("name"); // (3)
field.setAccessible(true); // (4)
System.out.println(field.get(clazz.getDeclaredConstructor().newInstance()));
}
}Which line fails to compile? If you delete just that line (the rest of
the class doesn't actually need it — Class.forName looks the type up
by string, and everything after that works purely through the
reflection API), does the remaining code run successfully at runtime?
Answer
Line (1), import com.example.app.model.User;, fails to compile.
com.example.app.model is opens-ed but never exports-ed. opens
grants runtime reflective access only — Class.getDeclaredFields(),
setAccessible(true), and similar. It does not make the package
visible at compile time: no other module can import a type from it or
declare a variable of that type in source. Only exports (or exports ... to, if tool were on the list) grants that.
With the import removed, the rest compiles and runs successfully.
Nothing else in Loader references User as a static type — Class .forName(String) resolves the class purely by its fully-qualified name
at runtime, and getDeclaredField, setAccessible, and
getDeclaredConstructor are all methods on java.lang.reflect types
declared in java.base, so they need no compile-time visibility into
com.example.app.model. At runtime, com.example.tool already reads
com.example.app (via requires), and com.example.app.model is
opened unqualified to every module, so setAccessible(true) succeeds
without throwing InaccessibleObjectException.
The takeaway: a package can be exports-ed, opens-ed, both, or
neither — each directive answers a different question ("can you import
it?" vs. "can you reflect into it at runtime?"), and one does not imply
the other.
Exercise 4 — uses / provides ... with and the ServiceLoader pattern
java// module com.example.spi
module com.example.spi {
exports com.example.spi;
}javapackage com.example.spi;
public interface MessageFormatter {
String format(String msg);
}java// module com.example.app (the consumer)
module com.example.app {
requires com.example.spi;
uses com.example.spi.MessageFormatter;
}java// module com.example.json (the provider)
module com.example.json {
provides com.example.spi.MessageFormatter
with com.example.json.JsonMessageFormatter;
}javapackage com.example.json;
import com.example.spi.MessageFormatter;
public class JsonMessageFormatter implements MessageFormatter {
@Override
public String format(String msg) {
return "{\"message\":\"" + msg + "\"}";
}
}Does com.example.json's module-info.java, exactly as written above,
compile? Note it never depends on com.example.app, and its class
directly references and implements com.example.spi.MessageFormatter.
Answer
It does not compile — com.example.json is missing requires com.example.spi;.
It's true, and central to the pattern, that a service provider never
needs to depend on the consumer module — com.example.json and
com.example.app remain fully decoupled from each other, discovered and
consumed only through ServiceLoader at runtime. That decoupling is the
whole point of uses/provides ... with.
But the provider does need a real dependency on whichever module
declares the service interface itself — here, com.example.spi.
Both the provides com.example.spi.MessageFormatter with ... directive
and the JsonMessageFormatter implements MessageFormatter declaration
reference the type com.example.spi.MessageFormatter directly, so
com.example.json needs ordinary compile-time readability of
com.example.spi, exactly as any other module referencing a type from
another module would. Without requires com.example.spi;, that type is
unreachable and both the provides clause and the implements clause
fail to compile.
Fixed version:
javamodule com.example.json {
requires com.example.spi;
provides com.example.spi.MessageFormatter
with com.example.json.JsonMessageFormatter;
}Exercise 5 — Syntax rules for module-info.java itself
java// src/com.example.app/module-info.java
package com.example.app;
module com.example.app {
exports com.example.app.api;
}Assume this file sits exactly where a normal module-info.java should
— at the root of com.example.app's source tree, alongside (not inside)
its package directories. Does it compile?
Answer
No — the package com.example.app; statement is illegal here and the
file fails to compile.
module-info.java is not a member of any package — it isn't a class or
interface at all, it's a special, singular file recognized by its exact
name (module-info.java) that declares the module itself. Its correct
location is literally the root of the module's source tree, sitting
alongside the top-level package directories, precisely so that it is
outside any package. Prefacing it with a package declaration
contradicts that positioning and is rejected by the compiler.
The corrected file simply omits the package statement:
java// src/com.example.app/module-info.java
module com.example.app {
exports com.example.app.api;
}This ties back to the two placement rules the slides emphasize: the file
must be named exactly module-info.java, and it must sit at the root of
the module's source directory rather than nested inside a package
folder like com/example/app/. A module-info.java found nested inside
a package directory isn't recognized as that module's descriptor at all
— javac's --module-source-path resolution expects it as a direct
child of the module's source root, not several package levels deep.