Claude Code correction guide. Updated January 2026.
- Claude suggests old thread pools — use virtual threads (Java 21+)
- Claude uses
sun.misc.Unsafe— migrate to VarHandle/FFM API - Claude forgets String Templates were removed in Java 23
- Claude returns null — use
Optional<T>for absence
| Tool | Use | NOT |
|---|---|---|
java 21+ LTS / java 25 |
Virtual threads, records | Java 17 or older |
maven/gradle with wrapper |
Build system | Global installs |
spotbugs + error-prone |
Static analysis | Just checkstyle |
junit 5 + testcontainers |
Testing | JUnit 4 |
jlink |
Custom runtime images | Fat JARs |
// Virtual threads (Java 21+)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
futures.forEach(f -> executor.submit(f));
}
// Pattern matching for switch (Java 21+)
String result = switch (obj) {
case String s -> s.toUpperCase();
case Integer i -> String.valueOf(i * 2);
case null -> "null";
default -> "unknown";
};
// Records for data (Java 16+)
record User(String name, String email) {}
// Scoped values (preview) instead of ThreadLocal
ScopedValue.runWhere(USER, currentUser, () -> process());- Returning
null— useOptional<T> catch (Exception e)— catch specific exceptions- Platform threads for I/O — use virtual threads
sun.misc.Unsafe— use VarHandle APIsynchronizedeverywhere — usejava.util.concurrent
- Java 24: Socket.connect() now closes socket on failure
- Java 23: String Templates removed, sun.misc.Unsafe warnings
- Java 21 LTS: Virtual threads stable, pattern matching finalized
- Java 26 (Mar 2026): Applet API fully removed
- With Spring: Virtual threads require
spring.threads.virtual.enabled=true
- Java deserialization (CWE-502):
ObjectInputStream.readObject()on attacker bytes is the classic Java RCE — a crafted object graph runs code through "gadget chains" in whatever is on the classpath (Commons-Collections, etc.) duringreadObject/readResolve, before you ever see the value. This is the impact pattern of CWE-502 "Deserialization of Untrusted Data": untrusted input → arbitrary object construction → code execution. Never native-deserialize data you did not produce. If you must, install aObjectInputFilter(Java 9+; process-wide defaultjdk.serialFilter) to allow-list classes and cap depth/refs. Prefer a data format with no code semantics (JSON via Jackson withactivateDefaultTypingOFF — polymorphic typing reintroduces the same gadget risk).
// Allow-list filter: reject everything except the exact classes you expect.
var filter = ObjectInputFilter.Config.createFilter(
"com.acme.dto.*;java.base/*;!*"); // last "!*" rejects all else
ois.setObjectInputFilter(filter);- Supply chain / log4shell class: a single transitive dependency (Log4j 2's
JNDI lookup, CVE-2021-44228) turned a log string into RCE across the ecosystem —
transitive reach, not your direct deps, is the attack surface. Enumerate the real
graph with
mvn dependency:tree/gradle dependencies, pin it with Maven dependency locking (mvn -Dlocking/dependencyManagementBOM) or GradledependencyLocking { lockAllConfigurations() }+ committed*.lockfile. - Audit tooling: run OWASP Dependency-Check (
dependency-check-maven/dependency-check-gradle, NVD-backed) or an OSV-backed scanner (osv-scanner, Google) in CI to fail the build on known-vulnerable coordinates. Do not assume a clean directpom.xmlmeans a clean tree. - Source: cwe.mitre.org (CWE-502), OWASP Deserialization Cheat Sheet, OWASP Dependency-Check. See References.
Virtual threads (JEP 444, finalized in Java 21) make thread-per-request cheap — but the failure modes shift.
// FOOTGUN: pinning. On Java 21–23 a virtual thread that blocks INSIDE a
// synchronized block/method pins its carrier platform thread — the whole pool can
// starve under load. JEP 491 (Java 24) removes this pin for synchronized; on 21–23
// migrate hot paths to a ReentrantLock, which never pinned.
synchronized (lock) { io.blockingCall(); } // pins the carrier on 21–23
// SAFE on any LTS: an explicit lock releases the carrier while blocked.
lock.lock();
try { io.blockingCall(); } finally { lock.unlock(); }- Do NOT pool virtual threads. They are the unit of work, not a scarce resource —
use
Executors.newVirtualThreadPerTaskExecutor()(one per task) and never wrap them in a fixed pool; pooling reintroduces the platform-thread bottleneck you removed. -Djdk.tracePinnedThreads=fullsurfaces pinning stacks on 21–23 (deprecated once JEP 491 lands — pinning onsynchronizedis gone on 24+).ScopedValue(JEP 506, finalized) replacesThreadLocalfor virtual threads: immutable, bounded to a dynamic scope, no leak across a million threads. Native serialization frames and JNI can still pin; keep blocking native calls short.- Structured concurrency (
StructuredTaskScope) is still preview (JEP 505); it makes a fan-out cancel siblings on first failure. Guard preview APIs behind a version check — do not ship preview features to production without--enable-preview. - Source: openjdk.org/jeps/444, openjdk.org/jeps/491, openjdk.org/jeps/506. See References.
// Return Optional<T> for absence, never null — the caller can't forget to check.
Optional<User> findUser(String id); // not: User findUser(...) returning null
// try-with-resources closes in reverse order, even on exception — no leaked handles.
try (var in = Files.newInputStream(p); var out = Files.newOutputStream(q)) {
in.transferTo(out);
}
// NEVER swallow InterruptedException — restore the interrupt so callers can stop.
try {
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // re-assert; do not just log-and-continue
throw new CancellationException();
}- Checked vs unchecked: use checked exceptions for recoverable, caller-actionable
failures; unchecked (
RuntimeException) for programming errors. Do notcatch (Exception e)broadly — it also swallows unchecked bugs andInterruptedException. - Never
return/continueinside afinallyblock — it silently discards a pending exception or return value.
- JPMS (Java Platform Module System, finalized in Java 9): strong encapsulation
means reflective access into JDK internals fails with
InaccessibleObjectException/IllegalAccessErrorunless the target module is opened. The fix is an explicit--add-opens java.base/java.lang=ALL-UNNAMED(orAdd-Opensin the JAR manifest) — NOT disabling the module system. On Java 16+ the JDK is strongly encapsulated by default; libraries that reflect intojava.base(older Mockito, some ORMs) break until opened. This is a migration cost, not a bug to route around with--add-openseverywhere (each open is a hole in encapsulation). - Records (finalized Java 16): immutable data carriers — but a record with a
mutable component (array,
List) is not deeply immutable; defensive-copy in the compact constructor.equals/hashCodeare component-wise, so arrays compare by identity (use a canonical constructor to convert toList). - Sealed classes (finalized Java 17) + pattern matching for switch
(finalized Java 21): an exhaustive
switchover a sealed hierarchy needs nodefault— but adding a permitted subtype turns a missing case into a compile error only if you omitdefault; a straydefaulthides the exhaustiveness check.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@Test
void parseRejectsEmpty() {
var ex = assertThrows(IllegalArgumentException.class, () -> Parser.parse(""));
assertTrue(ex.getMessage().contains("empty")); // assert error paths, not just happy
}
@ParameterizedTest
@CsvSource({"2, 4", "3, 9"})
void square(int in, int expected) { assertEquals(expected, square(in)); }- JUnit 5 (
org.junit.jupiter) — not JUnit 4's@RunWith. UseassertThrowsfor error paths,assertTimeoutPreemptivelyfor hangs,@Nestedfor grouping. - Testcontainers for real dependency integration (a throwaway Postgres/Kafka in Docker) instead of mocking the driver — tests the wire protocol, not a mock.
- Current LTS: Java 25, released 2025-09-22 (EOL 2031-09-30). Prefer it (or Java 21 LTS, released 2023-10-10, EOL 2029-12-31) for production; the six-month non-LTS releases (latest Java 26, released 2026-03-17) get only ~6 months of updates. [endoflife.date/eclipse-temurin + oracle-jdk, retrieved 2026-07-09]
- Virtual-thread pinning on
synchronizedis fixed in Java 24 (JEP 491) — code that pinned on 21–23 no longer starves carriers on 24+. Do not assume aReentrantLockrewrite is still needed once you are on 24/25. [openjdk.org/jeps/491, retrieved 2026-07-09] sun.misc.Unsafememory-access methods are deprecated for removal (JEP 471, Java 23) — migrate toVarHandle/ the Foreign Function & Memory API (java.lang.foreign, finalized JEP 454 in Java 22) before they are removed. Warnings here are future crashes. [openjdk.org/jeps/471, retrieved 2026-07-09]- String Templates were removed (not finalized) after preview — do not
generate
STR."..."template code; it will not compile on current JDKs.
- Java release status & LTS dates: https://endoflife.date/eclipse-temurin and https://endoflife.date/oracle-jdk
- CWE-502 (Deserialization of Untrusted Data): https://cwe.mitre.org/data/definitions/502.html
- OWASP Deserialization Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html
- OWASP Dependency-Check: https://owasp.org/www-project-dependency-check/
- JEP 444 (Virtual Threads, final in Java 21): https://openjdk.org/jeps/444
- JEP 491 (Synchronize Virtual Threads without Pinning, Java 24): https://openjdk.org/jeps/491
- JEP 506 (Scoped Values): https://openjdk.org/jeps/506
- JEP 505 (Structured Concurrency, preview): https://openjdk.org/jeps/505
- JEP 471 (deprecate sun.misc.Unsafe memory access): https://openjdk.org/jeps/471