Walk me through the difference between a JavaCompile task's javaCompiler and a Test/JavaExec task's javaLauncher. When would you set each to different JDKs in the same build?
answer
- compilerFor → javaCompiler (JavaCompile)
- launcherFor → javaLauncher (Test/JavaExec)
- compiler ≠ target: options.release
- newer javac + release=17
- can't run 21 bytecode on 17 launcher
basics
~20 sjavaCompiler picks the JDK that compiles sources; javaLauncher picks the JDK that runs a process (tests or JavaExec). You set them differently to, e.g., compile bytecode for Java 17 but run tests on Java 21.
solid answer
~50 sThese are two distinct per-task tools from the same `javaToolchains` service. `JavaCompile.javaCompiler` (built via `compilerFor { }`) selects the `javac` that compiles your sources; the *target bytecode* is separately controlled by `options.release`. `Test.javaLauncher` and `JavaExec.javaLauncher` (built via `launcherFor { }`) select the JVM that *launches* the process. They are independent: you might compile with JDK 17 targeting `release = 17`, yet launch a test suite on a JDK 21 JVM to confirm your 17-compiled artifacts still run on 21. The reverse is also valid: compile sources that use newer language features with JDK 21 (`release = 21`) but you wouldn't be able to launch on 17 since 21 bytecode won't load. Setting these per task — rather than the whole project — keeps the override scoped and avoids forcing every task onto the same JDK.
code
kotlin · 12 linestasks.named<JavaCompile>("compileJava") {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(21)
}
options.release = 17
}
tasks.named<Test>("test") {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}
}go deeper
Know that javaCompiler compiles and javaLauncher runs; they can be different JDKs.
Add the release-vs-compiler distinction and a concrete forward-compat example.
Explain the full matrix (compiler JDK, release target, launcher JDK), the one-directional compatibility, and when each divergence is useful.
Tie to a CI matrix strategy across JDKs, governance of which JDKs are sanctioned, and cost/cache trade-offs of multi-JDK builds.
## Two tools, one service The `javaToolchains` service (`JavaToolchainService`) is the single source for per-task JDK tools. It hands out three provider types: | Factory | Returns | Used on task | Property | |---|---|---|---| | `compilerFor { }` | `Provider<JavaCompiler>` | `JavaCompile` | `javaCompiler` | | `launcherFor { }` | `Provider<JavaLauncher>` | `Test`, `JavaExec` | `javaLauncher` | | `javadocToolFor { }` | `Provider<JavadocTool>` | `Javadoc` | `javadocTool` | ## Compiler: who compiles vs. what it targets `javaCompiler` chooses *which JDK's javac* runs. That is separate from the **bytecode target**: ```kotlin tasks.named<JavaCompile>("compileJava") { javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(21) } options.release = 17 // emit Java 17 bytecode using JDK 21's javac } ``` Using a newer JDK's compiler with `options.release = 17` gives you the modern toolchain while producing artifacts that load on Java 17. `release` is the robust way to set the target — it also restricts the API surface to that version. ## Launcher: who runs the process `javaLauncher` chooses the JVM that actually executes a forked process. For `Test`, that is the test JVM; for `JavaExec`, the application/tool JVM. ```kotlin tasks.named<Test>("test") { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) } } ``` ## When to diverge - **Forward-compat validation:** compile/target 17, launch tests on 21 → proves 17 bytecode runs on the newer runtime. - **Newer compiler, older target:** use JDK 21 `javac` with `release = 17` to standardise the dev toolchain while shipping for older runtimes. - **Tooling tasks:** a `JavaExec` code-generator that needs JDK 21 features, while the main app compiles for 17. ## Pitfall You cannot launch on an older JDK than the bytecode target — Java 21 bytecode will throw `UnsupportedClassVersionError` on a 17 launcher. The compiler/launcher split only buys compatibility in the *forward* direction (older bytecode on newer JVM).
- If javaCompiler uses JDK 21 but you forget options.release, what bytecode is produced?By default JDK 21's javac targets Java 21 bytecode. Without release (or sourceCompatibility/targetCompatibility), you'll emit 21 bytecode, which then won't run on a 17 launcher. Always set release for cross-version targeting.
- Can you launch tests on a JDK older than the compiled bytecode target?No. Older JVMs reject newer class-file versions with UnsupportedClassVersionError. Compatibility via the split only works forward: older bytecode on a newer JVM.
saying these in an interview costs you the question
- Conflating javaCompiler (the compiling JDK) with options.release (the target version).
- Assuming you can run any bytecode on any launcher regardless of version.
- Saying javaLauncher affects compilation.