Your AI can write Java 25 right with SonarQube
好的,我现在需要帮用户总结一篇文章的内容,控制在100个字以内,而且不需要特定的开头。首先,我得仔细阅读这篇文章,理解它的主要观点。 文章主要讨论了AI在生成Java 25代码时遇到的问题。Java 25引入了一些新特性,比如ScopedValue、模块导入和灵活的构造函数。AI工具虽然能生成看起来正确的代码,但由于训练数据的问题,常常会犯一些严重的错误。例如,ScopedValue的绑定在传统线程池中不起作用,或者模块导入导致类型歧义。 接着,文章提到了SonarQube如何通过新增的规则来检测这些AI生成的错误代码。这些规则帮助开发者识别潜在的问题,比如丢弃ScopedValue的绑定、构造函数中的验证逻辑位置不当等。 我需要将这些关键点浓缩到100字以内。首先指出AI生成Java 25代码的问题,然后提到SonarQube的作用。确保语言简洁明了,不遗漏主要信息。 最后检查字数和表达是否准确,确保总结清晰传达文章的核心内容。 </think> 文章探讨了AI工具在生成Java 25代码时可能引入的错误和陷阱,并介绍了SonarQube如何通过新增规则检测这些问题。重点包括ScopedValue绑定传播失败、模块导入导致类型歧义以及构造函数中字段初始化顺序不当等问题。 2026-3-18 15:0:0 Author: securityboulevard.com(查看原文) 阅读量:2 收藏

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.

The training data limitation

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.

More capable models, more severe bugs

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: the ThreadLocal trap

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: the prologue illusion

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.

Validation after super

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().

The uninitialized field read

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.

Prologue guardrails

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: the ambiguity time bomb

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.

The common thread

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.

Every Java 25 rule at a glance

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/


文章来源: https://securityboulevard.com/2026/03/your-ai-can-write-java-25-right-with-sonarqube/
如有侵权请联系:admin#unsafe.sh