Java Platform Module System (JPMS) practice questions

From OCP Java SE 17 (1Z0-829) · 18 questions on this topic

Java Platform Module System (JPMS) practice questions from OCP Java SE 17 (1Z0-829). This pack has 18 questions tagged Java Platform Module System (JPMS), drawn from its timed mock exams. 8 of them are worked through in full below — the question, every option, why each is right or wrong, and the explanation.

Worked examples for Java Platform Module System (JPMS)

  1. Question 1

    The directory `mods` holds three compiled modules. Two of them contain the **same** package, `com.shared`: ```java module com.first { exports com.shared; } // contains com.shared.Alpha module com.second { exports com.shared; } // contains com.shared.Beta module com.app { requires com.first; // note: com.second is NOT required } package com.app; import com.shared.Alpha; public class Main { public static void main(String[] args) { System.out.println(Alpha.who()); // Alpha.who() returns "alpha from com.first" } } ``` All three modules were compiled successfully, com.app with `--module-path mods`. You now run: ``` java --module-path mods -m com.app/com.app.Main ``` What happens?

    1. A. It prints `alpha from com.first` — com.second is observable but is never resolved, so the module graph that gets built contains no split package.Correct answer

      Correct: com.app requires only com.first, so com.second is observable but never resolved and never joins the module graph. The built graph therefore contains no split package and the program prints alpha from com.first.

    2. B. It fails at run time with `NoClassDefFoundError: com/shared/Alpha`, because the duplicate package makes `com.shared` ambiguous to the class loader.

      Wrong: this imagines the duplicate package is detected lazily at class load. Split packages are checked eagerly when the layer is constructed, and here there is no conflict at all because com.second is never resolved.

    3. C. It fails at startup with `LayerInstantiationException: Package com.shared in both module com.first and module com.second` — a split package anywhere on the module path is fatal.

      Wrong: this names the right exception for the wrong trigger. LayerInstantiationException only fires if com.second is actually resolved (for example via --add-modules); merely sitting on the module path is not enough.

    4. D. It prints `beta from com.second`, because when two modules on the module path contain the same package the one found later wins.

      Wrong: this invents a last-one-wins rule. JPMS has no such precedence; a genuine split-package conflict is an error, never a silent pick, and here com.second is never even resolved.

    Explanation

    The split-package rule is a constraint on the **module graph**, not on the module path. It says: no two modules *in the same layer* may contain the same package. Modules only enter that layer if they are **resolved**, and resolution starts from the root — here `com.app`, named by `-m` — and follows `requires` edges. com.app requires only com.first, so the resolved graph is `{com.app, com.first, java.base}`. com.second is *observable* (it sits on the module path, ready to be found) but nothing pulls it in, so it never joins the graph, and there is no conflict to report. The program runs and prints `alpha from com.first`. Why the others are wrong: `It fails at startup with `LayerInstantiationException`...` names the right exception for the wrong trigger — this is the single most instructive distractor here. That exception is exactly what you get **if com.second is resolved**: `java --module-path mods --add-modules com.second -m com.app/com.app.Main` dies before `main` with `Error occurred during initialization of boot layer` / a `java.lang.LayerInstantiationException` reporting `Package com.shared in both module ...` (the JVM names the two modules in an unspecified order). Merely *sitting on the module path* is not enough. `It prints `beta from com.second`...` invents a last-one-wins rule. There is no such precedence in JPMS; a genuine conflict is an error, never a silent pick. `It fails at run time with `NoClassDefFoundError`...` imagines the conflict is detected lazily, at class load. Split packages are checked eagerly, when the layer is constructed, precisely so that this class of ambiguity can never reach a class loader. Exam tip: hold *observable* and *resolved* apart — it is the distinction this whole question turns on, and it is the same one that makes `requires static` fail at run time. Also note the compile-time face of the rule: had com.app declared `requires com.second;` as well, javac would have refused it up front with `error: module com.app reads package com.shared from both com.first and com.second`.

  2. Question 2

    Which statement about the java.base module is correct?

    1. A. Every named module implicitly requires java.base, so writing requires java.base; is legal but redundantCorrect answer

      Per the JLS every module except java.base itself has an implicit requires java.base, so writing it explicitly is permitted but changes nothing — the dependence is always in effect.

    2. B. A module must declare requires java.base; or java.lang types are inaccessible

      Because the dependence is implicit, java.lang, java.util and the rest are readable without any declaration; compilation never fails for a missing requires java.base.

    3. C. java.base exports every package in the JDK

      java.base exports only its own foundational packages such as java.lang, java.util and java.io; other APIs live in other modules — java.sql, for example, is in the java.sql module.

    4. D. Only named modules can read java.base; classpath code cannot

      Code on the classpath runs in the unnamed module, which reads every observable module, java.base included.

    Explanation

    Every module other than java.base itself carries an implicit dependence on java.base, so its foundational packages are always readable and declaring the dependency by hand is redundant rather than required. java.base exports only its own core packages, not every JDK package, so other APIs live in separate modules. Its readability is not limited to named modules either — code on the classpath reads it through the unnamed module too. java.base is the one dependency you never need to declare.

  3. Question 3

    A module is declared like this: ```java module com.svc { exports com.svc.api; exports com.svc.util to com.svc.client; } ``` The source tree for `com.svc` contains **only** `com/svc/api/Service.java` — there is no `com/svc/util` directory at all. The module `com.svc.client` is not on the module path and is not part of this compilation. You compile: ``` javac --release 17 -d out src/com.svc/module-info.java src/com.svc/com/svc/api/Service.java ``` What does javac report?

    1. A. It fails with `package is empty or does not exist: com.svc.util`; the unknown target module `com.svc.client` produces only a warning.Correct answer

      Correct: an exported package must exist and be non-empty, so the missing com.svc.util is a hard error, whereas a qualified export's target module need not be observable at compile time, so the unknown com.svc.client is only a warning.

    2. B. It fails with two errors, one for `com.svc.util` and one for `com.svc.client`; javac requires both the package and every target module to exist.

      Wrong: this over-corrects by promoting the target-module diagnostic to an error. The missing package is an error, but the unobservable friend module is only a warning.

    3. C. It fails with `module not found: com.svc.client` — a qualified export must name a module that is observable at compile time.

      Wrong: this picks the wrong line as fatal. With the com.svc.util package actually present, that same declaration compiles with only a 'module not found' warning, so the missing friend module cannot be the error.

    4. D. It compiles, emitting one warning per suspicious line — both problems are deferred until the module graph is built at run time.

      Wrong: this treats the missing package as lenient too. Exporting a package with no types is an error because there is nothing to export, so the compilation fails.

    Explanation

    The two halves of `exports com.svc.util to com.svc.client;` are checked with different severities, and the question is whether you know which is which. The **package** being exported must actually exist in the module and must contain at least one type. `com/svc/util` does not exist, so javac hard-fails: ``` module-info.java:3: error: package is empty or does not exist: com.svc.util exports com.svc.util to com.svc.client; ^ ``` The **target** of a qualified export is only advisory at compile time — the friend module may perfectly well be compiled later, or shipped separately — so an unobservable target is a warning, not an error: ``` module-info.java:3: warning: [module] module not found: com.svc.client ``` One error and one warning; javac exits non-zero because of the error. Why the others are wrong: `It fails with `module not found: com.svc.client`...` picks the wrong line as fatal. Verified: with the `com/svc/util` package actually present, that very same declaration compiles with exit code 0 and *only* the `module not found` warning — so the missing friend module cannot be the error. `It compiles, emitting one warning per suspicious line...` treats the missing package as lenient too. It is not: exporting a package with no types is an error, because there is nothing to export. `It fails with two errors...` over-corrects, promoting the warning to an error. Both diagnostics are emitted, but only one of them is an error. Exam tip: `exports p to m;` — the package `p` must exist and be non-empty (error if not); the module `m` need not be anywhere in sight (warning at worst). The same asymmetry catches people with `opens p to m;`. Contrast with `requires m;`, where a missing `m` *is* a hard error (`module not found: m`) — `requires` is about what you read, and you cannot read what is not there.

  4. Question 4

    Which statement about an automatic module is correct?

    1. A. Code on the classpath is treated as an automatic module

      Classpath code joins the unnamed module, not an automatic module; it is placement on the module path that makes a plain JAR automatic.

    2. B. A plain JAR placed on the module path becomes an automatic module that requires and reads all other modules and exports all its packagesCorrect answer

      A plain JAR with no module-info.class on the module path becomes an automatic module: it reads every other module (including the unnamed module), exports all of its packages, and takes a name from the JAR file name or the Automatic-Module-Name manifest entry.

    3. C. Automatic modules must contain a module-info.class

      A module-info.class is exactly what an automatic module lacks; adding one would turn it into an explicit module.

    4. D. An automatic module cannot be required by an explicit module

      Being requirable by name from explicit modules is the whole point of automatic modules as a migration bridge.

    Explanation

    A plain JAR with no module descriptor, when placed on the module path, is promoted to an automatic module: it reads every other resolved module, exports all of its packages, and receives a name derived from the JAR file name or its Automatic-Module-Name manifest entry. The very same JAR on the classpath would instead join the unnamed module, which has no name and cannot be required. Location, not content, decides which migration path applies — and because an automatic module is named, explicit modules can require it as a migration bridge.

  5. Question 5

    Module com.report.impl supplies the implementation com.report.impl.PdfWriter for the service interface com.report.api.Writer (declared in module com.report.api). Which directive belongs in com.report.impl's module-info.java?

    1. A. provides com.report.api.Writer with com.report.impl.PdfWriter;Correct answer

      The provides directive names the service type first and the implementation after with — provides Service with Implementation — registering PdfWriter so ServiceLoader can instantiate it for consumers of Writer.

    2. B. provides com.report.impl.PdfWriter with com.report.api.Writer;

      The operands are reversed; the service interface must precede with and the implementation class must follow it.

    3. C. uses com.report.api.Writer;

      uses marks a consumer that will call ServiceLoader.load(Writer.class); the provider side declares provides, not uses.

    4. D. exports com.report.impl to com.report.api;

      A qualified export exposes a package's types at compile time to one module; it does not register a service implementation, and a provider need not even export its implementation package.

    Explanation

    A service provider registers its implementation with a directive that names the service interface first and the concrete class after the with keyword, so ServiceLoader can instantiate the implementation for any consumer of the interface. The order is fixed — interface before with, implementation after — and this registration is separate from consuming a service or from exposing a package to another module. Notably the provider needs neither a consuming directive nor an export of its implementation package; the service registry connects provider and consumer.

  6. Question 6

    Which statement about the uses directive in module-info.java is correct?

    1. A. uses com.report.api.Writer declares that this module may look up Writer implementations through ServiceLoader at run timeCorrect answer

      uses names a service type this module consumes, telling the module system it may call ServiceLoader.load(Writer.class) so providers can be located at run time even though no requires edge points at them.

    2. B. uses must list every provider module so they can be resolved at startup

      The consumer never names provider modules; providers are discovered dynamically among the resolved modules, and finding none is a legal empty result.

    3. C. uses grants deep reflective access to the named type's package

      Reflective access is the opens directive's territory; uses is purely about service lookup.

    4. D. One uses directive is required for each provider implementation class

      uses is declared once per service interface however many implementations exist; provides is the directive that names implementation classes.

    Explanation

    The uses directive declares which service type a module intends to consume, authorizing it to call ServiceLoader.load on that type and letting providers be discovered at run time without any requires edge pointing at them. It is declared once per service interface, not per provider, and providers are found dynamically among the resolved modules rather than listed by the consumer. It is about service lookup only, not reflective access, and a named module that calls ServiceLoader without the matching declaration fails with ServiceConfigurationError.

  7. Question 7

    A developer splits a utility library into two modules, each of which needs a type from the other: ```java // src/com.a/module-info.java module com.a { requires com.b; exports com.a; } // src/com.b/module-info.java module com.b { requires com.a; exports com.b; } ``` Each module contains one public class (`com.a.Alpha`, `com.b.Beta`), and neither class actually references the other module's class yet. Both modules are compiled in a single invocation: ``` javac --release 17 --module-source-path src -d mods $(find src -name '*.java') ``` What happens?

    1. A. Both modules compile, because a cycle is rejected only when one of the two sides uses `requires transitive`.

      Wrong: this invents a special case. requires transitive changes who else can read the module; it has no bearing on whether the graph may contain a loop. Plain mutual requires is already an error.

    2. B. Both modules compile. Readability cycles between modules are legal — each module simply reads the other.

      Wrong: this is a flat denial of the rule. The module dependence graph must be acyclic, and mutual requires between two modules is precisely what is banned.

    3. C. javac fails, reporting `cyclic dependence involving com.b` — the module dependence graph must be acyclic.Correct answer

      Correct: the module dependence graph must be acyclic, and compiling both declarations together lets javac see the cycle and reject it with 'cyclic dependence involving com.b' regardless of whether the classes reference each other.

    4. D. Both modules compile. The cycle is caught later, at launch, when `java` throws `LayerInstantiationException` while building the boot layer.

      Wrong: this moves the check to run time, but the code never compiles. LayerInstantiationException is real, but it comes from a split package, not from a requires cycle.

    Explanation

    The module dependence graph is required to be **acyclic**, and javac enforces that as soon as it can see both declarations. Compiling the two modules together over `--module-source-path` gives javac exactly that view, and it reports the cycle from both ends: ``` src/com.a/module-info.java:2: error: cyclic dependence involving com.b requires com.b; src/com.b/module-info.java:2: error: cyclic dependence involving com.a requires com.a; 2 errors ``` Note that nothing depends on whether the *classes* reference each other — the cycle is in the `requires` directives alone, so it is a pure declaration error and no class file is produced. Why the others are wrong: `Both modules compile. Readability cycles between modules are legal...` is the flat denial of the rule. Readability itself is not symmetric-by-default, and mutual `requires` is precisely what is banned. `Both modules compile. The cycle is caught later, at launch...` moves the check to run time. Resolution at run time would indeed have to reject a cycle, but it never gets the chance: the code cannot be compiled in the first place. (`LayerInstantiationException` is real, but it is what you get from a *split package*, not from a cycle.) `...a cycle is rejected only when one of the two sides uses `requires transitive`.` invents a special case. `transitive` changes who *else* can read the module; it has no bearing on whether the graph may contain a loop. Plain `requires` on both sides is already an error. Exam tip: if you see two module declarations that `requires` each other, stop — it is a compile error, full stop, no matter what the classes inside do. The idiomatic fix is to extract the shared types into a third module that both of them require, or to invert one direction with a service (`uses`/`provides`), which creates no `requires` edge at all.

  8. Question 8

    Two modules are compiled into the directory `mods`. ```java // module com.opt module com.opt { exports com.opt; } package com.opt; public class Helper { public static String greet() { return "helper present"; } } ``` ```java // module com.app module com.app { requires static com.opt; } package com.app; import com.opt.Helper; public class Main { public static void main(String[] args) { System.out.println(Helper.greet()); } } ``` Both modules compile without error, and `mods` contains both of them. You then run: ``` java --module-path mods -m com.app/com.app.Main ``` What is the result?

    1. A. It fails at run time with `java.lang.NoClassDefFoundError: com/opt/Helper`, because a `requires static` dependence is not resolved at run time.Correct answer

      requires static is mandatory at compile time but optional at run time, so resolution ignores the static edge and com.opt is never added to the graph; main starts and the missing class throws NoClassDefFoundError.

    2. B. It fails at startup with `java.lang.module.FindException: Module com.opt not found`, before `main` is entered.

      Assumes an unresolved optional dependence is a resolution failure; that is the point of static - no FindException is thrown, the JVM starts and fails only when the class is loaded.

    3. C. It prints `helper present`, because com.opt is sitting on the module path and is therefore part of the module graph.

      Assumes being on the module path is the same as being in the module graph; observability is not resolution, so com.opt is never resolved and the call fails (--add-modules would fix it).

    4. D. com.app fails to compile: `requires static` is legal only in a module that also declares a `uses` directive.

      Invents a rule; requires static stands alone and has nothing to do with uses/services, so com.app compiles without a uses directive.

    Explanation

    `requires static` declares a dependence that is **mandatory at compile time but optional at run time**. That is why com.app compiles cleanly against `com.opt.Helper`. At run time, however, resolution starts from the root module (com.app, named by `-m`) and follows only the ordinary `requires` edges — it deliberately ignores `requires static` edges. So com.opt, although *observable* on the module path, is never *resolved* into the module graph. `main` starts, reaches the call to `Helper.greet()`, and the class simply is not there: `java.lang.NoClassDefFoundError: com/opt/Helper`, caused by `java.lang.ClassNotFoundException: com.opt.Helper`. Why the others are wrong: `It prints `helper present`...` encodes the most common misconception here — that being on the module path is the same as being in the module graph. Observability is a precondition for resolution, not resolution itself. (Adding `--add-modules com.opt` makes com.opt a root, and then the program does print `helper present`.) ``requires static` is legal only in a module that also declares a `uses` directive.` is invented; `requires static` stands alone and has nothing to do with services. The declaration compiles. `...FindException: Module com.opt not found...` assumes an unresolved optional dependence is a resolution failure. It is not — that is the whole point of `static`. A `FindException` is what you would get from a plain `requires com.opt` when com.opt is genuinely absent; here the failure is deferred to the moment the class is actually loaded. Exam tip: `requires static` = "compile against it, tolerate its absence at run time", and the code must be written to tolerate that absence (guard the call, catch `NoClassDefFoundError`, or reach it only reflectively). The trap runs both ways: the module being *present* on the module path does not resurrect it — only `--add-modules`, or another resolved module with a non-static `requires` on it, will.

Practise all 18 Java Platform Module System (JPMS) questions

OCP Java SE 17 has the full set, inside timed mock exams that mirror real exam conditions — every question with a worked explanation.

Open OCP Java SE 17

Other topics in this pack