skip to content

When you declare a toolchain with languageVersion 21, what bytecode level does compilation target, and how does this interact with sourceCompatibility/targetCompatibility and the release flag?

level: middleimportance: should knowfreq 45%

answer

  1. toolchain picks JDK, release picks bytecode
  2. bare toolchain 21 => Java 21 bytecode
  3. options.release restricts the API too
  4. release preferred over source/target
  5. build with 21, target 17

basics

~20 s

By default a JDK-21 toolchain compiles to Java 21 bytecode. If you still want older bytecode, set sourceCompatibility/targetCompatibility (or release) lower than the toolchain version. The toolchain picks the JDK; source/target/release set the bytecode level.

solid answer

~50 s

Declaring `languageVersion = JavaLanguageVersion.of(21)` makes Gradle compile, by default, to **Java 21 bytecode** using the JDK 21 compiler. The toolchain and the bytecode level are separate concerns: - The **toolchain** chooses *which JDK* (compiler + runtime). - **`sourceCompatibility`/`targetCompatibility`** (or the preferred **`options.release`**) choose the *bytecode language level*. If you declare a JDK 21 toolchain but need to ship Java 17 bytecode (e.g. consumers on JDK 17), set the target lower: ```kotlin java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } tasks.withType<JavaCompile>().configureEach { options.release = 17 } ``` `options.release` is preferred over source/target because it also restricts the **API** to that version (preventing accidental use of newer JDK methods). When you don't set any of these, Gradle infers the bytecode level from the toolchain's language version. So a bare JDK 21 toolchain == Java 21 bytecode.

code

kotlin · 6 lines
kotlin
java {
    toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
tasks.withType<JavaCompile>().configureEach {
    options.release = 17 // emit 17 bytecode, restrict API to 17
}

go deeper

for a junior

Know that a bare JDK 21 toolchain produces Java 21 bytecode by default.

for a middle

Separate the two dials and show how to ship 17 bytecode from a 21 toolchain using options.release.

for a senior

Explain why release beats target (API restriction) and the runtime NoSuchMethodError risk it prevents.

for a principal

Define an org standard: modern build toolchain + release-pinned bytecode targets so libraries stay consumable by older runtimes.

## Two separate dials It is essential to separate **which JDK compiles** from **what bytecode comes out**: 1. **Toolchain `languageVersion`** — selects the JDK installation (compiler `javac` + runtime). 2. **Bytecode level** — set by `sourceCompatibility`, `targetCompatibility`, or `options.release`. ## Default behaviour When you declare only a toolchain: ```kotlin java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } ``` Gradle infers `source`/`target` from the toolchain — so output is **Java 21 bytecode**, compiled with JDK 21's `javac`. You don't need to set anything else for the common case of 'compile with and target the same version'. ## Targeting older bytecode with a newer JDK A frequent requirement: build *with* a modern JDK (better compiler, security patches, faster CI agents) but emit bytecode an older runtime can load. There are two mechanisms: ### sourceCompatibility / targetCompatibility ```kotlin java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } ``` This sets `-source 17 -target 17`. Problem: it does **not** stop you calling APIs introduced after 17. The code might compile against JDK 21's class library and then `NoSuchMethodError` at runtime on JDK 17. ### options.release (preferred) ```kotlin tasks.withType<JavaCompile>().configureEach { options.release = 17 } ``` `--release 17` does three things at once: sets source level, sets target bytecode, **and** restricts the visible API to the Java 17 standard library. That last part is the safety win — it prevents accidental newer-API usage. Since Gradle 6.6+, `options.release` is a lazy `Property<Int>` and is the recommended way to cross-compile. ## Precedence - If `options.release` is set, it governs and source/target are effectively subsumed. - If only source/target are set, those are used. - If none are set, the toolchain's language version determines the level. ## Why this matters in interviews Candidates often say 'I set the toolchain to 21 so my bytecode is 21' — true by default — but then can't explain how to ship 17 bytecode from a 21 build. The crisp answer is: keep the JDK-21 toolchain, set `options.release = 17`. Using `release` over `target` is the senior signal because it closes the accidental-newer-API gap. ## Putting it together ```kotlin java { toolchain { languageVersion = JavaLanguageVersion.of(21) } // build WITH JDK 21 } tasks.withType<JavaCompile>().configureEach { options.release = 17 // emit Java 17 bytecode + restrict API to 17 } ``` Result: JDK 21's toolchain compiles, but the artifact runs on any JDK 17+ and can't accidentally depend on post-17 APIs.

  • Why prefer options.release over targetCompatibility?
    release (--release N) sets source, target AND restricts the visible standard-library API to version N, preventing accidental use of newer methods that would NoSuchMethodError on the older runtime. target only sets bytecode level.
  • If I set neither release nor source/target with a JDK 21 toolchain, what bytecode is produced?
    Java 21 bytecode — Gradle infers the level from the toolchain's language version.

saying these in an interview costs you the question

  • Claiming you must lower the toolchain version to emit older bytecode.
  • Treating targetCompatibility and options.release as equivalent (release also restricts the API).
  • Saying a toolchain alone can't produce older bytecode.

context