en un clic
java-21-to-25
// Upgrade Java projects from JDK 21 to JDK 25 covering Flexible Constructor Bodies, Stream Gatherers, Class-File API, Scoped Values, Security Manager removal
// Upgrade Java projects from JDK 21 to JDK 25 covering Flexible Constructor Bodies, Stream Gatherers, Class-File API, Scoped Values, Security Manager removal
Assess a Java project's modernization posture — Java version, framework versions, dependency health, vulnerabilities, and migration opportunities
Deploy Java applications to Azure services including App Service, Container Apps, AKS, and Spring Apps
Upgrade Java projects from JDK 11 to JDK 17 covering Records, Sealed Classes, Pattern Matching for instanceof, Switch Expressions, Text Blocks
Upgrade Java projects from JDK 17 to JDK 21 covering Virtual Threads, Pattern Matching for switch, Record Patterns, Sequenced Collections
Upgrade Java projects from JDK 8 to JDK 11 covering module system, removed Java EE modules (JAXB, JAX-WS, CORBA), var keyword, HTTP Client, collection factory methods
Migrate Java application services and dependencies to Azure equivalents using predefined migration tasks
| name | java-21-to-25 |
| description | Upgrade Java projects from JDK 21 to JDK 25 covering Flexible Constructor Bodies, Stream Gatherers, Class-File API, Scoped Values, Security Manager removal |
This guide provides comprehensive GitHub Copilot instructions for upgrading Java projects from JDK 21 to JDK 25, covering new language features, API changes, deprecations, and migration patterns based on 39 JEPs delivered in Java 22, 23, 24, and 25.
Migration Pattern: Remove all Security Manager usage
// These calls now throw UnsupportedOperationException in Java 24+:
// System.setSecurityManager(sm);
// System.getSecurityManager();
// Old: Security Manager checks
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new FilePermission("/tmp/data", "read"));
}
// New: Use application-level security, file system permissions,
// or container-based isolation. Remove all SecurityManager references.
// There is no migration path — SecurityManager is gone.
.policy files and -Djava.security.policy JVM flagsSystem.setSecurityManager() and System.getSecurityManager() callsjava.security.manager system property is ignoredMigration Pattern: Replace Unsafe with VarHandle or Foreign Memory API
// Old: sun.misc.Unsafe for field access (deprecated, warns at runtime in 24+)
Unsafe unsafe = /* obtain Unsafe */;
long offset = unsafe.objectFieldOffset(MyClass.class.getDeclaredField("count"));
unsafe.getInt(obj, offset);
unsafe.putInt(obj, offset, 42);
unsafe.compareAndSwapInt(obj, offset, 0, 1);
// New: VarHandle API (Java 9+) — for on-heap field access
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class AtomicCounter {
private volatile int count;
private static final VarHandle COUNT;
static {
try {
COUNT = MethodHandles.lookup()
.findVarHandle(AtomicCounter.class, "count", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public int get() { return (int) COUNT.get(this); }
public void set(int val) { COUNT.set(this, val); }
public boolean cas(int expected, int newVal) {
return COUNT.compareAndSet(this, expected, newVal);
}
}
// New: Foreign Memory API (Java 22+) — for off-heap memory access
import java.lang.foreign.*;
// Old: unsafe.allocateMemory / unsafe.getInt / unsafe.freeMemory
// New:
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(ValueLayout.JAVA_INT, 10);
segment.set(ValueLayout.JAVA_INT, 0, 42);
int value = segment.get(ValueLayout.JAVA_INT, 0);
}
Migration Pattern: Declare native access and migrate to FFM API
# JNI usage now produces warnings at runtime in Java 24+.
# Suppress warnings by explicitly granting native access:
# For classpath-based applications
java --enable-native-access=ALL-UNNAMED -jar myapp.jar
# For modular applications
java --enable-native-access=com.example.mymodule -jar myapp.jar
// In module-info.java, no special declaration is needed for JNI itself,
// but you must pass --enable-native-access on the command line.
// Preferred: Migrate from JNI to Foreign Function & Memory API (JEP 454)
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
// Old: JNI native method
// public native long getProcessId();
// New: FFM API (Java 22+)
public long getProcessId() {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = linker.defaultLookup();
MethodHandle getpid = linker.downcallHandle(
lookup.find("getpid").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG)
);
try {
return (long) getpid.invoke();
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
Migration Pattern: Use _ for unused variables and patterns
// Old: Unused variable in try-with-resources, catch, or loop
try {
// ...
} catch (NumberFormatException ex) { // ex never used
System.out.println("Invalid number");
}
for (Order order : orders) { // order never used, just counting
total++;
}
// New: Unnamed variables (Java 22+)
try {
// ...
} catch (NumberFormatException _) {
System.out.println("Invalid number");
}
for (Order _ : orders) {
total++;
}
// In pattern matching — ignore components you don't need
if (obj instanceof Point(var x, _)) {
// only care about x coordinate
System.out.println("x = " + x);
}
switch (shape) {
case Circle(var radius, _) -> computeCircle(radius);
case Rectangle(var w, var h) -> computeRect(w, h);
default -> throw new IllegalArgumentException();
}
// In lambdas
map.forEach((_, value) -> process(value));
Migration Pattern: Simplified entry points and compact class declarations
// Old: Full class ceremony for simple programs (Java 21)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
// New: Instance main method (Java 25)
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
// New: Compact source file — implicit class, no class declaration needed
void main() {
System.out.println("Hello, World!");
}
// Instance main methods can use instance fields and methods
String greeting = "Hello";
void main() {
println(greeting + ", World!"); // println is available in implicit classes
}
// Priority order for main method selection:
// 1. static void main(String[]) — traditional
// 2. static void main() — no-args static
// 3. void main(String[]) — instance with args
// 4. void main() — instance no-args
Migration Pattern: Statements before super() or this()
// Old: Validation before super() required factory methods or workarounds
public class PositiveInteger extends Number {
private final int value;
// Old: Had to validate in a static helper
public PositiveInteger(int value) {
super(); // must be first statement in Java 21
if (value <= 0) throw new IllegalArgumentException("Must be positive");
this.value = value;
}
}
// New: Statements allowed before super() (Java 25)
public class PositiveInteger extends Number {
private final int value;
public PositiveInteger(int value) {
// Validation BEFORE calling super
if (value <= 0) {
throw new IllegalArgumentException("Must be positive: " + value);
}
this.value = value; // field assignment before super() is allowed
super();
}
}
// Useful for preparing arguments for super constructor
public class NamedLogger extends AbstractLogger {
public NamedLogger(String rawName) {
// Compute and validate before delegating
var normalized = rawName.strip().toLowerCase();
if (normalized.isEmpty()) {
throw new IllegalArgumentException("Name cannot be blank");
}
super(normalized); // pass computed value to super
}
}
Migration Pattern: Import all packages exported by a module
// Old: Multiple individual imports
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.function.Function;
// New: Module import declaration (Java 25)
import module java.base; // imports all packages exported by java.base
// Brings in java.util.*, java.io.*, java.nio.*, java.time.*, java.lang.*, etc.
// without wildcard-importing each package individually
// Works with any module
import module java.sql; // imports java.sql.* and javax.sql.*
import module java.net.http; // imports java.net.http.*
// Especially useful for reducing boilerplate in scripts and small programs
import module java.base;
void main() {
var list = List.of(1, 2, 3);
var now = Instant.now();
var path = Path.of("/tmp/test.txt");
}
Migration Pattern: Replace JNI with the standard FFM API
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
// Call a native C function (e.g., strlen)
public long strlen(String s) {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle strlenHandle = linker.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateFrom(s);
return (long) strlenHandle.invoke(cString);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
// Allocate and use off-heap memory safely
try (Arena arena = Arena.ofConfined()) {
// Allocate a struct-like segment
MemorySegment point = arena.allocate(
MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
)
);
point.set(ValueLayout.JAVA_INT, 0, 10); // x = 10
point.set(ValueLayout.JAVA_INT, 4, 20); // y = 20
}
// Memory is automatically freed when arena is closed
Migration Pattern: Replace ThreadLocal with ScopedValue
// Old: ThreadLocal (mutable, easy to leak, problematic with virtual threads)
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
public void handleRequest(User user) {
CURRENT_USER.set(user);
try {
processRequest();
} finally {
CURRENT_USER.remove(); // must clean up manually
}
}
public void processRequest() {
User user = CURRENT_USER.get();
// ...
}
// New: ScopedValue (Java 25) — immutable, bounded lifetime, virtual-thread-friendly
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public void handleRequest(User user) {
ScopedValue.runWhere(CURRENT_USER, user, () -> {
processRequest(); // CURRENT_USER is bound within this scope
});
// No cleanup needed — binding ends automatically
}
public void processRequest() {
User user = CURRENT_USER.get(); // reads the bound value
// ...
}
// ScopedValue with return value
String result = ScopedValue.callWhere(CURRENT_USER, user, () -> {
return generateReport();
});
Migration Pattern: Custom intermediate stream operations
import java.util.stream.Gatherers;
// Sliding window — groups elements into overlapping windows
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.toList();
// [[1,2,3], [2,3,4], [3,4,5]]
// Fixed-size window (non-overlapping)
List<List<Integer>> chunks = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowFixed(2))
.toList();
// [[1,2], [3,4], [5]]
// fold — stateful reduction into a stream of accumulated results
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.fold(() -> 0, Integer::sum))
.toList();
// [15]
// scan — running accumulation (emits each intermediate result)
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
// [1, 3, 6, 10, 15]
// mapConcurrent — parallel mapping with bounded concurrency
List<String> results = urls.stream()
.gather(Gatherers.mapConcurrent(10, url -> fetch(url)))
.toList();
// Custom gatherer — emit elements with distinct consecutive values
Gatherer<String, ?, String> dedup = Gatherer.ofSequential(
() -> new Object() { String last = null; },
(state, element, downstream) -> {
if (!element.equals(state.last)) {
state.last = element;
return downstream.push(element);
}
return true;
}
);
Stream.of("a", "a", "b", "b", "b", "c", "a")
.gather(dedup)
.toList();
// ["a", "b", "c", "a"]
Migration Pattern: Replace ASM with standard Class-File API
import java.lang.classfile.*;
import java.lang.classfile.attribute.*;
// Old: ASM library for bytecode manipulation
// ClassReader reader = new ClassReader(classBytes);
// ClassWriter writer = new ClassWriter(reader, 0);
// ClassVisitor visitor = new MyClassVisitor(writer);
// reader.accept(visitor, 0);
// byte[] result = writer.toByteArray();
// New: Class-File API (Java 24+)
// Parse a class file
ClassModel classModel = ClassFile.of().parse(classBytes);
// Read class information
String className = classModel.thisClass().asInternalName();
for (MethodModel method : classModel.methods()) {
String name = method.methodName().stringValue();
String descriptor = method.methodType().stringValue();
}
// Transform a class — add logging to all methods
byte[] transformedBytes = ClassFile.of().transform(classModel,
ClassTransform.transformingMethods(
MethodTransform.transformingCode(
(builder, element) -> {
if (element instanceof CodeElement ce) {
builder.with(ce);
}
}
)
)
);
// Generate a new class from scratch
byte[] newClass = ClassFile.of().build(
ClassDesc.of("com.example", "Generated"),
classBuilder -> classBuilder
.withFlags(ClassFile.ACC_PUBLIC)
.withMethod("hello", MethodTypeDesc.of(ClassDesc.ofDescriptor("V")),
ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC,
methodBuilder -> methodBuilder.withCode(codeBuilder -> codeBuilder
.getstatic(ClassDesc.of("java.lang", "System"), "out",
ClassDesc.of("java.io", "PrintStream"))
.ldc("Hello from generated class!")
.invokevirtual(ClassDesc.of("java.io", "PrintStream"), "println",
MethodTypeDesc.of(ClassDesc.ofDescriptor("V"),
ClassDesc.of("java.lang", "String")))
.return_()
)
)
);
Migration Pattern: Convert HTML JavaDoc to Markdown
// Old: HTML-based JavaDoc
/**
* Returns the <b>absolute</b> value of an {@code int} value.
* <p>
* If the argument is not negative, the argument is returned.
* If the argument is negative, the negation of the argument is returned.
* <ul>
* <li>If the argument is {@code Integer.MIN_VALUE}, the result is negative.</li>
* <li>If the argument is zero, the result is zero.</li>
* </ul>
*
* @param a the argument whose absolute value is to be determined
* @return the absolute value of the argument
* @throws ArithmeticException if the argument is {@code Integer.MIN_VALUE}
* @see Math#abs(int)
*/
// New: Markdown JavaDoc (Java 23+)
/// Returns the **absolute** value of an `int` value.
///
/// If the argument is not negative, the argument is returned.
/// If the argument is negative, the negation of the argument is returned.
///
/// - If the argument is `Integer.MIN_VALUE`, the result is negative.
/// - If the argument is zero, the result is zero.
///
/// @param a the argument whose absolute value is to be determined
/// @return the absolute value of the argument
/// @throws ArithmeticException if the argument is `Integer.MIN_VALUE`
/// @see Math#abs(int)
// Markdown JavaDoc supports fenced code blocks
/// Example usage:
/// ```java
/// int result = abs(-42); // returns 42
/// ```
// Links use Markdown syntax with JavaDoc references
/// See [Math#abs(int)] for the standard library version.
/// Uses the algorithm described in [Integer#MIN_VALUE].
Migration Pattern: Run multi-file programs directly without compilation
# Java 11 allowed single-file execution: java HelloWorld.java
# Java 22 extends this to multi-file programs
# If Main.java references Helper.java in the same directory:
java Main.java
# The launcher automatically finds and compiles Helper.java
# Works with packages — files must follow package directory structure
java --source 25 com/example/Main.java
# Useful for scripts, prototypes, and educational contexts
Migration Pattern: synchronized blocks no longer pin virtual threads
// In Java 21, synchronized blocks pinned virtual threads to platform threads,
// reducing scalability. In Java 24, this is fixed.
// This code now scales properly with virtual threads — no changes needed:
public class SharedResource {
private final Object lock = new Object();
public void access() {
synchronized (lock) {
// In Java 21: virtual thread was PINNED here (bad for scalability)
// In Java 24: virtual thread can unmount during blocking (fixed!)
performBlockingIO();
}
}
}
// You no longer need to convert synchronized to ReentrantLock for virtual threads.
// However, ReentrantLock still offers more features (tryLock, fairness, etc.)
Migration Pattern: Create smaller custom runtime images
# jlink can now create run-time images without JMOD files,
# reducing JDK installation size by ~25%.
# Standard jlink usage remains the same:
jlink --module-path $JAVA_HOME/jmods \
--add-modules java.base,java.sql \
--output custom-runtime
# When JDK is built without JMODs (future distributions may do this):
# jlink uses the run-time image itself as source
jlink --add-modules java.base,java.sql \
--output custom-runtime
Migration Pattern: Use quantum-resistant cryptographic algorithms
// ML-KEM: Module-Lattice-Based Key Encapsulation Mechanism
import javax.crypto.KEM;
import java.security.KeyPairGenerator;
// Generate ML-KEM key pair
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM");
kpg.initialize(NamedParameterSpec.ML_KEM_768);
var keyPair = kpg.generateKeyPair();
// Encapsulate (sender side)
KEM kem = KEM.getInstance("ML-KEM");
KEM.Encapsulator enc = kem.newEncapsulator(keyPair.getPublic());
KEM.Encapsulated encapsulated = enc.encapsulate();
byte[] ciphertext = encapsulated.encapsulation();
SecretKey sharedSecret = encapsulated.key();
// Decapsulate (receiver side)
KEM.Decapsulator dec = kem.newDecapsulator(keyPair.getPrivate());
SecretKey receiverSecret = dec.decapsulate(ciphertext);
// ML-DSA: Module-Lattice-Based Digital Signature Algorithm
import java.security.Signature;
KeyPairGenerator sigKpg = KeyPairGenerator.getInstance("ML-DSA");
sigKpg.initialize(NamedParameterSpec.ML_DSA_65);
var sigKeyPair = sigKpg.generateKeyPair();
Signature sig = Signature.getInstance("ML-DSA");
sig.initSign(sigKeyPair.getPrivate());
sig.update(data);
byte[] signature = sig.sign();
// Verify
sig.initVerify(sigKeyPair.getPublic());
sig.update(data);
boolean valid = sig.verify(signature);
Migration Pattern: Standard API for deriving cryptographic keys
import javax.crypto.KDF;
// HKDF (HMAC-based Key Derivation Function)
KDF hkdf = KDF.getInstance("HKDF-SHA256");
// Extract-then-expand
SecretKey derived = hkdf.deriveKey("AES",
KDF.HKDFParameterSpec.ofExtract()
.addIKM(inputKeyMaterial)
.addSalt(salt)
.thenExpand(info, 32) // 32 bytes for AES-256
);
Migration Pattern: Reduced object memory overhead (no code changes needed)
# Compact object headers reduce object header size from 96-128 bits to 64 bits.
# This is now a product feature in Java 25.
# Enabled by default — no flags needed.
# To explicitly disable (not recommended):
# -XX:-UseCompactObjectHeaders
Migration Pattern: Dramatically improve startup time with AOT caching
# Java 25 simplifies AOT cache creation with a single flag:
# Step 1: Training run — creates the AOT cache automatically
java -XX:AOTCacheOutput=app.aot -cp myapp.jar com.example.Main
# Step 2: Production run — use the AOT cache for fast startup
java -XX:AOTCache=app.aot -cp myapp.jar com.example.Main
# The AOT cache stores:
# - Pre-loaded and pre-linked classes (JEP 483)
# - Method execution profiles for immediate JIT compilation (JEP 515)
# - Resolved constant pool entries
# For Spring Boot / Quarkus / Micronaut applications, this can cut
# startup time significantly without any code changes.
# ZGC is now generational-only. Remove any non-generational flags:
# Old (Java 21): Explicitly choose generational mode
-XX:+UseZGC -XX:+ZGenerational
# New (Java 23+): Generational is default and only mode
-XX:+UseZGC
# -XX:-ZGenerational is no longer accepted in Java 24+
# Non-generational ZGC flags to REMOVE:
# -XX:-ZGenerational (error in 24+)
# -XX:+ZUncommit (renamed/removed)
# G1 GC now supports region pinning for JNI critical sections.
# This eliminates GC pauses caused by JNI GetPrimitiveArrayCritical.
# No configuration needed — automatic improvement.
# Internal G1 optimization — reduces C2 JIT compilation overhead.
# No configuration needed — automatic improvement.
# Shenandoah's generational mode is now a product feature.
-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
# JFR is significantly enhanced in Java 25:
# Cooperative sampling — more stable stack sampling at safepoints
# (Enabled by default, no configuration needed)
# Method timing & tracing — measure time spent in specific methods
# Configure via JFR event settings
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>25</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<!-- If using JNI in tests -->
<argLine>--enable-native-access=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
tasks.withType<JavaCompile> {
options.release.set(25)
}
tasks.withType<Test> {
useJUnitPlatform()
// If using JNI in tests
jvmArgs("--enable-native-access=ALL-UNNAMED")
}
Update build system
Address breaking changes
--enable-native-access for JNI usage (JEP 472)-XX:-ZGenerational flags (JEP 490)Fix sun.misc.Unsafe usage (JEP 471/498)
Update JNI code (JEP 472)
Language features
_ (JEP 456)API features
Startup optimization
GC tuning
Security updates
Use Unnamed Variables Aggressively
Adopt Scoped Values over ThreadLocal
Embrace Stream Gatherers
gather()windowSliding, windowFixed, fold, scan, mapConcurrentPlan for Post-Quantum Cryptography
Set Up AOT Caching for Production
Migrate Off sun.misc.Unsafe Now
This comprehensive guide enables GitHub Copilot to provide contextually appropriate suggestions when upgrading Java 21 projects to Java 25, focusing on language enhancements, API improvements, security updates, and modern Java development practices.