Your AI coding assistant writes a ScopedValue handler for a request pipeline. It binds a user context, forks the work to an ExecutorService, and reads the value on the worker thread. The code compiles. It looks like well constructed, modern Java. In production, the worker thread throws NoSuchElementException because scoped value bindings don't propagate to traditional thread pools. The assistant used the concurrency pattern it learned from 20 years of Java training data. That pattern is wrong for ScopedValue.
Java 25 is the first LTS-track release since JDK 21, and it finalizes features that have been in preview for years. AI coding tools generate fluent Java 25 code, but fluency and correctness are different things. SonarQube includes new rules that catch the exact failure modes AI coding tools introduce when they write for these features.
ScopedValue was in preview across JDK 21 through 24. Each version shipped a different API surface. In the previews, ScopedValue.orElse(null) was legal. In the final Java 25 API, passing null to orElse throws NullPointerException. An LLM trained on preview-era code generates calls that match a preview API but break against the final one.
This pattern repeats with every language release that finalizes preview features. The model's training data contains thousands of examples of the preview API, a handful of blog posts about the final API, and zero production codebases using the final version (because it just shipped). Research on LLM code generation confirms various forms of hallucination in generated code, including API misuse (Zhang et al., 2024).
Java 25 is not uniquely dangerous. It's a case study of a structural problem. Every finalized preview feature creates a window where AI tools are confidently wrong, and that window stays open until enough post-release code enters the training pipeline. For enterprise teams jumping from JDK 17 or 21 to 25, every finalized feature hits at once.
You might expect newer, more capable models to handle this better. Sonar's data says otherwise.
In Sonar's 2026 Developer Survey of 1,100+ developers, 42% of committed code is now AI-generated or AI-assisted, up from 6% in 2023. The volume is staggering. And the quality gap is widening in a direction most teams don't expect.
Sonar tested leading models across 4,400+ identical Java tasks. The results show a consistent and counterintuitive pattern: newly updated versions of LLMs improve pass rates, however the remaining bugs are more severe and harder to find. When Claude upgraded from 3.7 Sonnet to Sonnet 4, the pass rate improved 6.3%, but bugs were 93% more likely to be BLOCKER severity. Opus 4.6 increased vulnerability density 55%.
A systematic survey of bugs in AI-generated code documents the same pattern: models routinely produce code that is syntactically valid but semantically incorrect, with functional bugs that don't surface at compile time (Gao et al., 2025). More fluent code passes more tests, but the failures that remain are subtler and more expensive to find.
61% of developers in the Developer Survey cite AI code that "looks correct but isn't reliable" as a top concern. Yet only 48% of developers verify AI-generated code before committing. When developers ranked the most critical skill for the AI era, the top answer was "reviewing and validating AI-generated code for quality and security" (47%). The code verification gap is real, and it widens when the code looks right.
JEP 506 finalizes scoped values as a structured alternative to ThreadLocal. You declare a ScopedValue<T>, bind it through a Carrier, and any code executing within that carrier's .run() or .call() scope can read the value. When the scope ends, the binding disappears.
The API is simple. The trap is in how AI tools reach for the wrong concurrency primitive around it.
Every LLM has seen thousands of ThreadLocal + ExecutorService patterns. When asked to share context across concurrent tasks, they reproduce that pattern with scoped values bolted on. The result compiles, but the worker thread has no binding:
static final ScopedValue REQUEST_ID = ScopedValue.newInstance();
void handleRequest(String id) {
ScopedValue.where(REQUEST_ID, id).run(() -> {
executor.submit(() -> {
String reqId = REQUEST_ID.get(); // NoSuchElementException
processAsync(reqId);
});
});
}
Scoped value bindings propagate to child threads created via StructuredTaskScope, not to threads borrowed from an ExecutorService pool. The fix requires replacing the concurrency model, not just the variable type:
void handleRequest(String id) throws Exception {
ScopedValue.where(REQUEST_ID, id).call(() -> {
try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
scope.fork(() -> {
String reqId = REQUEST_ID.get(); // Inherited binding
processAsync(reqId);
return null;
});
scope.join();
}
return null;
});
}
Note: StructuredTaskScope is a preview API in Java 25 (JEP 505) and requires --enable-preview to compile.
A second failure mode is even quieter. ScopedValue.where(KEY, value) returns a Carrier object. If you don't chain it with .run() or .call(), the binding never activates. The line executes, does nothing, and the next .get() call throws. SonarQube rule S8432 catches exactly this: a .where() call whose Carrier result is discarded.
A related rule, S8465, catches a different ScopedValue misuse: creating an anonymous instance directly inside .where(). Without a stable reference, the key is unreachable because no code can call .get() on it.
// Noncompliant: Carrier discarded, binding never activates
ScopedValue.where(THEME, "DARK");
// Compliant: Carrier consumed, binding active during run()
ScopedValue.where(THEME, "DARK").run(this::renderUI);
And then there's the preview API residue. LLMs trained on JDK 21-24 code generate orElse(null) as a safe fallback when a scoped value might not be bound. In the preview API, that was legal. In the final Java 25 API, orElse requires a non-null default. Note that no rule catches this today. It's a runtime failure, not a structural pattern the static analyzer can flag:
// Preview-era pattern: throws NullPointerException in Java 25
User getUser() {
return CURRENT_USER.orElse(null);
}
// Compliant: non-null default or explicit isBound() guard
User getUser() {
return CURRENT_USER.orElse(User.anonymous());
}
All three failure modes share a root cause: the AI generates code shaped like ThreadLocal usage, where .set() modifies thread state in place, any thread pool inherits the value, and null is a valid sentinel. Scoped values work differently at every level, and the training data hasn't caught up.
JEP 513 allows statements before super() or this() in a constructor. Before Java 25, the explicit constructor invocation had to be the first statement. The code before super() is called the prologue, and it operates in an "early construction context" with restrictions that look nothing like a normal code block.
The most universal AI failure mode with JEP 513 is also the simplest. Every model generates validation after super() because pre-25 Java required it:
public SmallCoffee(int water, int milk, String topping) {
super(water, milk); // Noncompliant: constructing before validation
int totalVolume = water + milk;
if (totalVolume > MAX_CUP_VOLUME) {
throw new IllegalArgumentException();
}
}
The superclass allocates resources, initializes state, and possibly triggers side effects, all before you check whether the arguments are even valid. Java 25 lets you validate first:
public SmallCoffee(int water, int milk, String topping) {
int totalVolume = water + milk;
if (totalVolume > MAX_CUP_VOLUME) {
throw new IllegalArgumentException();
}
super(water, milk); // Compliant: validation before construction
}
SonarQube rule S8433 flags constructors where validation logic appears after super().
S8433 catches a performance and correctness issue. Rule S8447 catches a bug, and it's the most severe rule in the Java 25 set: type BUG, severity CRITICAL, reliability impact HIGH.
The scenario: a superclass constructor calls an overridable method. A subclass overrides that method and reads its own field. Because super() runs before the subclass field assignment, the field holds its default value (0 for int, null for objects, and false for boolean).
class Super {
Super() { foo(); }
void foo() { System.out.println("Base logic"); }
}
class Sub extends Super {
final int x;
Sub(int x) {
super();
this.x = x; // Noncompliant: x is 0 when foo() runs during Super()
}
@Override
void foo() {
System.out.println(x); // Prints 0, not the expected value
}
}
Read that code carefully. The field is final. The value is assigned. The constructor looks correct. But super() calls foo() before the assignment executes, so foo() reads the default value. A bug like this passes every human review because nothing looks wrong.
Java 25's flexible constructor bodies fix it by allowing the field initialization to move before super():
Sub(int x) {
this.x = x; // Initialize before super()
super(); // foo() now sees the correct value
}
The alternative fix: mark the superclass method final or private so subclasses can't override it in a way that observes uninitialized state.
The prologue enables better formed constructors, but it's not a dumping ground. Rule S8444 flags constructors with more than five statements (by default) before super() and recommends extracting complex logic into static helper methods. Static, because instance methods aren't accessible in the early construction context.
Together, S8433, S8444, and S8447 form a trio: validate before super() (S8433), don't overdo it (S8444), and initialize fields before super() when they're read during superclass construction (S8447).
JEP 511 introduces module import declarations. A single import module java.base; imports every public top-level type from every package exported by that module. It's a convenience feature that collapses dozens of import lines into one.
The danger is in what happens when you import more than one module.
import module java.base; // exports java.util.List, java.util.Date
import module java.desktop; // exports java.awt.List
import module java.sql; // exports java.sql.Date
public class OrderService {
List orders; // Compile error: ambiguous
Date createdAt; // Compile error: ambiguous
}
The error appears at the usage site, not at the import. An LLM generating this code sees no problem with the imports. Module imports have the lowest precedence in Java's shadowing hierarchy, below both single-type imports and on-demand package imports. To resolve ambiguity, you add a specific import:
import module java.base;
import module java.sql;
import java.util.List; // Disambiguates
import java.sql.Date; // Disambiguates
A subtler problem: import module java.se; imports the entire Java SE platform, but only in an explicit module that already requires java.se. In a typical classpath project (the unnamed module), this import fails to compile because java.se is not in the default set of root modules. An LLM won't know the difference. In a modular project where it does compile, the import creates a different risk: every new type added in a future JDK release can introduce an ambiguity that breaks compilation. The import that worked on day one grows more fragile with every Java version.
One more trap: import module cannot import from the unnamed module, which holds classpath jars that aren't explicitly modularized. And even for modularized libraries, LLMs guess the module name from Maven coordinates rather than the actual JPMS name. An LLM will generate import module com.google.guava; when the real module name is com.google.common, producing a compile error with no clear signal about what went wrong.
LLMs also make wrong precedence assumptions. If you write both import module java.base; and import java.awt.*;, the package import's java.awt.List shadows the module import's java.util.List. An LLM generating both imports likely intended java.util.List, but the shadowing rules say otherwise.
SonarQube rule S8445 enforces that module imports come first and that regular and static imports are properly grouped. The rule makes the shadowing hierarchy visible in the source file: module imports (broadest, lowest precedence) at the top, with regular and static imports each grouped below. When ambiguity exists, the structure makes it obvious where to add a disambiguation import.
All three JEPs produce the same shape of failure: code that is syntactically valid but semantically broken, where the error surfaces at runtime or at a usage site far from the root cause. The Carrier looks like it does something. The prologue looks like a normal code block. The module import looks correctly coded.
LLMs generate code token by token without running a type-checker, a semantic model, or a data-flow analysis between tokens. In contrast, SonarQube's static code analysis does. When a model generates ScopedValue.where(KEY, value) without .run(), the code compiles because the Carrier return type is valid. SonarQube flags it because the Carrier is discarded, meaning the intended effect never happens. When a model puts field initialization after super() in a class where the superclass calls an overridden method, the code compiles because the assignment is syntactically legal in the epilogue. SonarQube flags it because the field is read before it's written.
As models get more fluent, the bugs they introduce get subtler. The training data cliff compounds this: every language release that finalizes preview features creates a new batch of patterns where AI tools are confidently generating code for an API that no longer exists. The same dynamic that makes AI coding assistants useful for boilerplate (deep pattern knowledge of established APIs) makes them unreliable for new language features (shallow or stale knowledge of recently changed APIs).
Static analysis closes this gap because it doesn't depend on training data. SonarQube’s rules encode the final API contracts, the prologue restrictions, and the shadowing hierarchy as they actually are, not as they were in a preview six months ago. When the next JDK release finalizes more preview features, new rules will follow.
SonarQube includes rules across four JEPs for these features, released in two batches. JEPs 506, 513, and 511 are covered above. JEP 512 (Compact Source Files and Instance Main Methods) adds two smaller ergonomics rules:
| Rule | JEP | Type | Severity | What it catches |
| S8432 | 506 (Scoped Values) | Code Smell | Major | ScopedValue.where() result discarded |
| S8433 | 513 (Flexible Constructors) | Code Smell | Major | Validation logic after super() |
| S8444 | 513 (Flexible Constructors) | Code Smell | Major | Excessive logic in constructor prologue |
| S8445 | 511 (Module Imports) | Code Smell | Minor | Module imports not first; regular/static imports not grouped |
| S8447 | 513 (Flexible Constructors) | Bug | Critical | Field read before initialization via super() call chain |
| S8446 | 512 (Compact Source Files) | Code Smell | Major | Multiple main methods causing shadowing |
| S8450 | 512 (Compact Source Files) | Code Smell | Minor | BufferedReader boilerplate replaceable with IO.readln() |
| S8465 | 506 (Scoped Values) | Bug | Major | Anonymous ScopedValue created inside .where() – key unreachable |
The first four rules (S8432, S8433, S8444, S8445) shipped in sonar-java 8.24.0. S8446, S8447, S8450, and S8465 followed in 8.25.0. All are available now in SonarQube Cloud and in SonarQube Server starting with the 2026.2 release.
These rules exist because the gap between AI fluency and programming language correctness is structural and it widens with every release. Java 26 is already on the horizon with its own set of finalized preview features. The cycle repeats.
Run your AI-generated Java 25 code through SonarQube. These rules catch the patterns that AI gets wrong and reviewers miss. Analysis is free for public projects on SonarQube Cloud. If you're using SonarQube for IDE, the rules flag these patterns as you write rather than after you push.
*** This is a Security Bloggers Network syndicated blog from Blog RSS feed authored by Killian Carlsen-Phelan. Read the original post at: https://www.sonarsource.com/blog/ai-can-write-java-25-right-with-sonarqube/