Connect with me
JDK 24 is Here! Game-Changing Features Every Java Developer Must Know
JDK 24 has arrived with powerful new features for Java developers. This release brings performance boosts, improved concurrency, class file instance main method and more. Dive in to explore how these updates can improve your Java code!

In this article, I’ve outlined the most important syntax and API updates introduced in JDK 24.
JDK 24 is a significant release because the JDK Enhancement Proposal(JEP) candidates included in this update have a higher chance of becoming part of JDK 25, which is scheduled for release in September 2025.
JDK 25 will be the next Long-Term Support (LTS) version after JDK 21 (the current LTS).
JDK 24 bring notable improvements to both performance and the syntactic constructs of the Java programming language. Let’s explore some of the most important updates.
⚠️ All the code examples provided here are preview features of JDK 24. To run them, you’ll need to enable preview mode using the --enable-preview
flag.
Here is a guide for symbols used in the articles:
🔗 — Link to JEP
⚠️ — Pay attention
❌ — Limitation
👉 — Reasoning
1. Simple Source Files and Instance Main methods
It’s finally here! Java now allows a void main()
method to print “Hello, World.”
Until JDK 21, the traditional “Hello, World” program looked like this.
public class MyFirstClass {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}
❌ One of the challenges faced by Java beginners is that this code can be overly verbose, making it difficult to understand, especially for those unfamiliar with Java’s syntax and constructs.
With JDK 24, the “Hello, World” program is much simpler and looks like this.
void main() {
println("Hello, World");
}
⚠️ This is not a new Java syntax — experienced Java developers will still have access to all the existing syntax and verbosity.
👉 This new syntax is possible by implicit declaration of classes and instance methods by JVM.
Additionally, the JVM now imports certain required packages automatically at runtime, reducing the need for manual namespace declarations when using utility methods like println()
.
The following program reads the user’s name from the console and prints a greeting message.
void main() {
var name = readln();
var message = "Hello, World and " + name;
println(message);
}
If we take a turn to actual/verbose Java code then this code will convert to as follows.
import java.io.*;
public class A {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String name = sc.nextLine();
String message = "Hello, World and " + name;
System.out.println(message);
sc.close();
}
}
This also improves coding simple Java Programs without reliance on heavy verbosity and declarations as is apparent from above code.
🔗 Simple Source File and Instance Main Method are introduced in JEP 495.
2. Primitive type matching in instanceof and Switch-Case
❌ Until JDK 21, the type matching required the variable that is being compared to be of referenced type instead of a primitive type.
👉 Beginning from JDK24 the type matching with instanceof
and in Switch-Case
will allow objects to match with primitive types and not only referenced types.
Given below is an example of type matching with instanceof
operator.
public class Test {
public static String identifyType(Object value) {
if (value instanceof int) {
return String.format("int: %d", value);
} else if (value instanceof String) {
return String.format("String: %s", value);
} else if (value instanceof Double) {
return String.format("Double: %f", value);
} else {
return "Unknown type";
}
}
This is an example for type matching in switch-case
.
String identifyType(Object value) {
return switch (value)
case int i -> String.format("int: %d", i); // primitive
case Double d -> String.format("Double: %f", d);
case String s -> String.format("String: %s", s);
default -> "Unknown type";
};
}
🔗 Primitive types in patterns, instanceof,
and switch
are introduced in JEP 488.
3. Flexible Constructor Bodies
👉 With this JEP, developers can include logic within the constructor body before initializing the superclass using super()
.
❌ Currently, in JDK 21, this is not possible — developers must use auxiliary methods within the superclass to execute logic after initialization.
The constructor body is now divided into two parts: Prologue and Epilogue.
public class A extends B{
public A() {
// Prologue
super();
// Epilogue
}
}
Thus, we can now define logic both before and after the super()
call, with the exception that the ⚠️ current instance being initialized cannot be accessed during the prologue.
Here is an example.
public class A extends B {
private static int multiplier = 10;
private final int childValue;
public A(int baseValue) {
// Prologue
int computedValue = baseValue * multiplier;
super(baseValue); // Initialize B
// Epilogue
this.childValue = computedValue;
}
}
🔗 Flexible Constructor Bodies are introduced in JEP 492.
4. Scoped Values
The ScopedValue API is an improvement over the ThreadLocal API for sharing immutable data across different threads (including virtual threads) and tasks.
👉 ScopedValues solved three key challenges faced by ThreadLocal:
- Mutability: In ThreadLocal, a thread can both access and modify the shared variable, leading to unintended side effects. ScopedValue, on the other hand, allows only immutable data to be shared.
- Unbound Lifetime: A ThreadLocal variable must be manually removed when no longer needed. If accessed by multiple threads, it can persist in memory longer than necessary. ScopedValue automatically cleans up once the original owning thread completes execution.
- Expensive Inheritance: In ThreadLocal, variables must be copied and recreated in memory for any child threads forked from a parent thread. ScopedValue allows memory to be efficiently shared between parent and child threads.
Below is a simple example demonstrating the use of the ScopedValue API.
static final ScopedValue<String> USERNAME = ScopedValue.newInstance();
void main(String[] args) {
ScopedValue.where(USERNAME, "Amrit").run(() -> {
System.out.println("Value inside main task: " + USERNAME.get());
performSubTask();
});
}
static void performSubTask() {
System.out.println("Value inside sub-task: " + USERNAME.get());
}
🔗 ScopedValues are introduced in JEP 487.
5. Module Import Declaration
👉 With module import declaration, users can now import a group of related packages directly, rather than importing them individually.
This reduces verbosity and eliminates inefficiencies caused by duplicate package imports.
For example, instead of the following declarations at the top of the file.
import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.stream.*;
Users can now just import all these packages grouped by module java.xml
as follows.
import module java.xml;
👉 There are other modules such as java.base
, java.desktop
, java.sql
etc. which group required packages for each usecase.
🔗 Module Import Declarations are introduced in JEP 494.
6. Structured Concurrency
StructuredTask API introduced in JDK24 groups together subtasks in concurrent programming, treating them as a single unit rather than separate, independent tasks at runtime.
❌ In traditional concurrent execution using ExecutorService API, subtasks have only a logical relationship with the parent task that started them. However, at runtime, they run independently, with no direct connection.
This lack of structure poses a challenge — if one subtask depends on another, and the dependent subtask fails, the first subtask should also fail. However, until the dependent task completes, there is no built-in mechanism to track its status or enforce failure propagation.
👉 With StructuredTask API developers can now have control over the entire task which can be composed of subtasks running in different task as a whole unit.
Thus, if one task fails, the failure is propagated to other dependent tasks and the task as a whole also abort.
Given below is an example explaining the contrast between ExecutorService API and StructuredTask API.
import java.util.concurrent.*;
Integer fun1() throws Exception {
Integer val = 1;
for (int i = 0; i < 10; i++) {
println(val);
val++;
Thread.sleep(1000);
if (val == 4) {
System.out.println("Fun 1 stopped at value 4");
throw new Exception();
}
}
return val;
}
Integer fun2() throws InterruptedException {
Integer val = 1;
for (int i = 0; i < 10; i++) {
println(val);
val++;
Thread.sleep(1000);
}
return val;
}
void handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Grouping using Scope
Supplier<Integer> x = scope.fork(() -> fun1());
Supplier<Integer> y = scope.fork(() -> fun2());
scope.join().throwIfFailed();
} catch (Exception e){
println("Both Fun 1 and Fun 2 is now stopped because Fun 1 failed");
}
}
void handleWithoutST() throws ExecutionException, InterruptedException {
ExecutorService exsc = Executors.newFixedThreadPool(3);
Future<Integer> x = exsc.submit(() -> fun1());
Future<Integer> y = exsc.submit(() -> fun2());
exsc.shutdown();
}
void main() throws ExecutionException, InterruptedException {
// handle(); // --> Stops with either of the sub-task fails
// handleWithoutST(); // --> Does not stop and keeps execution alive
}
🔗 Structured Task is introduced in JEP 499.
7. Ahead-of-time class loading and linking
Ahead-of-Time (AOT) Class Loading & Linking are designed to improve the startup time of Java applications.
It achieves this by caching the loaded and linked forms of classes during one run and reusing them in subsequent runs.
This enhancement also lays the foundation for future optimizations in both startup and warmup times.
👉 Ahead-of-time compilation has seen an increase of 40% performance improvement in Java Programs.
🔗 Ahead-of-Time (AOT) Class Loading & Linking is introduced in JEP 483.
More JDK 24 Updates
In this article I’ve summarized the top most updates which I believe are great for Java Developers and introduce them in their day-to-day coding.
🔗 However, there are more JEPs which are preview candidates in JDK 24 and can be looked into from the official release page of JDK 24 https://openjdk.org/projects/jdk/24/.