Java 24 was launched in JavaOne 2025 conference on 18th March 2025. 2025 is a landmark year since it marks 30 years since the first release of Java. I started my journey with Java 1.5, and it's amazing to see how Java has evolved over the years. We had to wait for years for new features in Java earlier, but now we get new features every 6 months. This rapid release cycle has made Java more competitive and modern. Now let us explore the key features included in Java 24 release.
Feature Classifications in Java 24
Before diving into the specific features, it's helpful to understand how Java classifies new features:
Final Features: Fully implemented, tested, and officially part of the Java SE Platform, ready for production use.
Preview Features: Complete but not yet permanent features available for developer feedback, requiring the
--enable-preview
flag to use.Incubator Features: Experimental APIs distributed in separate modules (prefixed with "jdk.incubator.") that may evolve significantly based on feedback.
Experimental Features: Early, potentially unstable versions of (mostly) VM-level features that often require specific flags to enable.
What it is: This feature extends pattern matching (which has been introduced in multiple previous versions) to work with primitive types in both instanceof
and switch
expressions, making code more concise and type-safe.
Why it matters: Simplifies code that needs to match primitive types, reducing boilerplate and improving readability.
You can learn about all pattern matching features in Java from this blog - Clean code with Pattern Matching in Java.
Example:
public void processValue(Object obj) { switch (obj) { case int i -> System.out.println("Integer: " + i); case long l -> System.out.println("Long: " + l); case String s -> System.out.println("String: " + s); default -> System.out.println("Unknown: " + obj); } }
What it is: Allows statements to appear before an explicit constructor invocation (super(...)
or this(...)
), making constructors more flexible.
Why it matters: Enables cleaner initialization logic and reduces the need for repetitive code in constructors.
Example:
class UserProfile extends BaseProfile{ private final SecurityLevel securityLevel; public UserProfile(String rawUsername, String rawEmail) { // Sanitize input before calling superclass constructor var sanitizedUsername = sanitize(rawUsername); var sanitizedEmail = sanitize(rawEmail); this.securityLevel = determineSecurityLevel(sanitizedEmail); super(sanitizedUsername, sanitizedEmail); } private static String sanitize(String input) { return input.trim().toLowerCase(); } private static SecurityLevel determineSecurityLevel(String email) { return email.endsWith(".gov") ? SecurityLevel.HIGH : email.endsWith(".org") ? SecurityLevel.MEDIUM : SecurityLevel.STANDARD; } }
What it is: Allows developers to import all the packages exported by a module with a single declaration, rather than importing each package individually.
Why it matters: Simplifies working with modular libraries by reducing import statement verbosity, especially when using multiple packages from the same module.
Syntax: import module.*;
For example import module java.base
has the same effect as 54 on-demand package imports, one for each of the packages exported by the java.base module. It is as if the source file contains import java.io.* and import java.util.* and so on.
Example:
import module java.base; void main() throws IOException { var names = List.of("Alice", "Bob", "Charlie"); Path filePath = Paths.get("test.txt"); Files.write(filePath, names); } // No need to import java.nio.file.Paths, java.util.List, java.nio.file.Files
What it is: Simplifies Java program structure for beginners by eliminating the need for explicit class declarations and static main methods in simple programs.
Why it matters: Lowers the barrier to entry for learning Java, allowing beginners to write code without immediately needing to understand concepts like classes and static methods.
Example:
// Traditional Java program public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } // Now, with simple source files and instance main methods void main() { System.out.println("Hello, World!"); }
What it is: Preloads and links classes at build time rather than at runtime.
Why it matters: Improve startup time by making the classes of an application instantly available, in a loaded and linked state, when the HotSpot Java Virtual Machine starts. Achieve this by monitoring the application during one run and storing the loaded and linked forms of all classes in a cache for use in subsequent runs. Lay a foundation for future improvements to both startup and warmup time.
What it is: Reduces the size of object headers in the HotSpot JVM from between 96 and 128 bits down to 64 bits on 64-bit architectures.
Why it matters: This will reduce heap size, improve deployment density, and increase data locality.
What it is: Virtual threads had a limitation that if a blocking call is executed in a synchronized
block, platform thread is not released. Solution for this problem was to use other locking mechanisms like ReentrantLock
instead of synchronized
block. This JEP removes this limitation.
Why it matters: Enhances the scalability of applications using virtual threads with synchronization, improving throughput for concurrent workloads. Many libraries and frameworks use synchronized
blocks, and now you don't have to wait for them to be updated to use virtual threads.
This blog from Netflix discusses the issue they faced after migrating to virtual threads which is caused by the pinning in synchronized block - Netflix Engineering Blog - Java 21 Virtual Threads - Dude, Where’s My Lock?.
What it is: Extends the Stream API with custom intermediate operations via the gather
method. You can create a custom gatherer to perform complex operations on stream elements. Some common gatherers are available in Gatherers
class.
Why it matters: Enables more complex and flexible data transformations within streams without breaking the stream pipeline.
You can learn more about Stream API from this Java Stream Gatherers.
Example:
var employeePairListWithInbuiltGatherer = employees.stream() .filter(employee -> employee.department().equals("Engineering")) .map(Employee::name) .gather(Gatherers.windowFixed(2)) // Inbuilt gatherer .toList(); var fixedWindowGatherer = getFixedWindowGatherer(2); // Custom gatherer to group elements in fixed window var employeePairList = employees.stream() .filter(employee -> employee.department().equals("Engineering")) .map(Employee::name) .gather(fixedWindowGatherer) .toList(); // Custom gatherer to group elements in fixed window private static <T> Gatherer<T,List<T>,List<T>> getFixedWindowGatherer(int limit) { Supplier<List<T>> initializer = ArrayList::new; Gatherer.Integrator<List<T>,T,List<T>> integrator = (state, element, downstream) -> { state.add(element); if(state.size() == limit){ var group = List.copyOf(state); downstream.push(group); state.clear(); } return true; }; BiConsumer<List<T>, Gatherer.Downstream<? super List<T>>> finisher = (state,downStream) ->{ if(!state.isEmpty()){ downStream.push(List.copyOf(state)); } }; return Gatherer.ofSequential(initializer,integrator,finisher); }
What it is: An API for structured concurrency that treats groups of related tasks running in different threads as a single unit of work. This approach streamlines error handling and cancellation while managing the lifecycle of concurrent operations cohesively.
Why it matters: Simplifies concurrent programming by eliminating common risks like thread leaks and cancellation delays. It improves reliability and observability of concurrent code, which is particularly valuable when working with large numbers of virtual threads. This API helps developers manage complexity when breaking tasks into multiple concurrent subtasks.
Example:
public String fetchUserData(long userId) throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // Fork tasks to run concurrently Subtask<UserProfile> profileTask = scope.fork(() -> userService.fetchProfile(userId)); Subtask<List<Order>> ordersTask = scope.fork(() -> orderService.fetchOrders(userId)); Subtask<CreditRating> creditTask = scope.fork(() -> creditService.fetchCreditRating(userId)); // Wait for all tasks to complete (or one to fail) scope.join(); // If any task failed, propagate the exception scope.throwIfFailed(); // All tasks completed successfully - combine results return generateUserReport( profileTask.get(), ordersTask.get(), creditTask.get() ); } }
You can learn more about Structured Concurrency from this Concurrency in Spring and Java blog post.
What it is: A safer alternative to thread-local variables for sharing data within a thread.
Why it matters: Provides a more structured approach to context propagation, especially with virtual threads.
You can learn more about Scoped Values from this Scoped Values Guide.
In the below code we are using Scoped Values to set the user context for each task.
Example:
void main() { List<String> users = List.of("Alice", "Bob", "Charlie", "David", "Eve"); try(ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()){ for (String user : users) { executor.submit(() -> { ScopedValue.runWhere(UserContext.userScopedVal, user, () -> { var stocks = new StockRepository().getStockSymbols(); }); }); } } } class StockRepository{ Map<String, List<String>> userStocks = Map.of( "Alice", List.of("AAPL", "GOOGL", "AMZN"), "Bob", List.of("MSFT", "TSLA"), "Charlie", List.of("AAPL", "AMZN"), "David", List.of("GOOGL", "MSFT"), "Eve", List.of("TSLA") ); public List<String> getStockSymbols(){ return userStocks.get(UserContext.userScopedVal.get()); } } class UserContext { public static final ScopedValue<String> userScopedVal = ScopedValue.newInstance(); }
What it is: A new API for parsing, generating, and transforming Java class files.
Why it matters: Simplifies building tools that work with bytecode, like bytecode analyzers, code generators, and JVM language implementations. The Java ecosystem has many libraries for parsing and generating class files, each with different design goals, strengths, and weaknesses. Frameworks that process class files generally bundle a class-file library such as ASM, BCEL, or Javassist. Because the class-file format can evolve every six months due to 6 month release cycle, frameworks are more frequently encountering class files that are newer than the class-file library that they bundle. To solve this problem, the Class-File API provides a standard library for reading, writing, and transforming class files.
What it is: A new API for expressing vector computations that reliably compile to optimal vector hardware instructions.
Why it matters: Dramatically improves performance for computationally intensive applications by leveraging SIMD (Single Instruction, Multiple Data) hardware capabilities.
Example:
// Define a preferred species whose shape is optimal for the current architecture static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED; void main(String[] args) { float[] a = new float[1024]; float[] b = new float[1024]; float[] c = new float[1024]; for (int i = 0; i < a.length; i++) { a[i] = i; b[i] = i * 2; } // Compute using Vector API vectorComputation(a, b, c); } void vectorComputation(float[] a, float[] b, float[] c) { int i = 0; int upperBound = SPECIES.loopBound(a.length); // Process vectors in chunks determined by species length for (; i < upperBound; i += SPECIES.length()) { var va = FloatVector.fromArray(SPECIES, a, i); var vb = FloatVector.fromArray(SPECIES, b, i); // Perform vector operations: (a² + b²) * -1 var vc = va.mul(va) .add(vb.mul(vb)) .neg(); vc.intoArray(c, i); } // Handle remaining elements with scalar computation for (; i < a.length; i++) { c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; } }
This example demonstrates how the Vector API can significantly accelerate computation-intensive operations by leveraging SIMD (Single Instruction, Multiple Data) capabilities of modern CPUs. The code calculates (a² + b²) * -1
for each element in the arrays.
The key improvements over scalar code include:
When executed on supported hardware, this code can achieve several times the performance of the equivalent scalar computation, especially for large arrays and compute-intensive operations.
What it is: A new API for Key Derivation Functions (KDFs), which are cryptographic algorithms for deriving additional keys from a secret key and other data.
Why it matters: Provides standardized, secure methods for deriving cryptographic keys, making it easier to implement secure key management in applications.
What it is: Implements ML-KEM, a quantum-resistant key encapsulation mechanism based on module lattices, standardized by NIST in FIPS 203.
Why it matters: Prepares Java applications for the post-quantum era by providing cryptographic algorithms resistant to attacks by quantum computers, which could break existing public key cryptography.
What it is: Implements ML-DSA, a quantum-resistant digital signature algorithm based on module lattices, standardized by NIST in FIPS 204.
Why it matters: Future-proofs Java applications against quantum computing attacks that could compromise current signature algorithms like RSA and ECDSA.
You can find the source code for all the examples in this article on CodeWiz Java24.
JDK 24 represents a significant step forward for the Java platform, bringing enhancements across multiple fronts including language features, runtime performance, libraries, and security. These improvements make Java more powerful, efficient, and secure for developers building everything from microservices to high-performance computing applications.
The features showcased in this article demonstrate Java's continued evolution to meet modern development needs while maintaining its commitment to backward compatibility and performance.
To stay updated with the latest updates in Java and Spring, follow us on YouTube, LinkedIn, and Medium.
This blog introduces Stream Gatherers, a new feature in Java 22, which allows developers to add custom intermediate operations to stream processing. It explains how to create and use Stream Gatherers to enhance data transformation capabilities in Java streams.
Explore the top 5 features released from Java 21 to Java 23, including Virtual Threads, Pattern Matching with Records and Sealed Classes, Structured Concurrency, Scoped Values, and Stream Gatherers. Learn how these features can enhance your Java applications.
Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.