Sitemap
Berat Yesbek

Berat Yesbek

Computer Engineer, Software Engineer at Kramp https://www.linkedin.com/in/beratyesbek/

Evolution of Java ~ Java 25

14 min readOct 8, 2025

--

History of Java, Evolution of Java and Java 25

Hello everyone! As you are aware, Java 25 was released on September 16. In this article, we’ll explore new features of Java 25 and the evolution of Java. Let’s refresh our coffees and get started ☕ 📰😊.

As you know, the Java programming language was first developed in the early 1990s at Sun Microsystems by James Gosling. Initially, this language was designed for embedded systems such as smart televisions and set-top boxes. However, with the rise of the internet in 1995, it became a general-purpose programming language.

Throughout Java’s evolution, the language has been enriched with new features; particularly LTS versions such as Java 8, Java 11, and Java 17 have brought the most transformative changes. These innovations, such as lambda expressions, the module system, and pattern matching, enable us to design our applications in a more modular, robust, readable, and high-performance manner, thereby optimising our development processes.

Java 25 is now ushering in a new revolution with the innovations it offers in threading and concurrency. Behind this revolution lie powerful features that make artificial intelligence (AI), big data processing and many other features more accessible and efficient.

Before jumping into real use cases for Java 25 features, let me list all Java features which was introduced so far.

Press enter or click to view image in full size
The Evolution of Java Versions

The Evolution of Java Versions

JDK 1.0 (January 1996)

The first version of Java included the core language features (java.lang package, basic types), the graphical user interface (AWT), applets, and I/O and network libraries. This version laid the foundation for Java.

JDK 1.1 (February 1997)

Nested classes, JavaBeans, JDBC (database connectivity), and RMI (remote method invocation) were added. These features made Java more suitable for enterprise applications.

J2SE 1.2 (December 1998)

It was renamed Java 2. The Collections Framework and Swing (advanced GUI) were introduced. The platform tripled in size, with 59 packages and 1,520 classes added.

J2SE 1.3 (May 2000)

The HotSpot JVM and performance improvements were introduced. JNDI (naming and directory service) became standard. Windows 95 support ended here.

J2SE 1.4 (February 2002)

Assertions, regular expressions, and the NIO (New I/O) package were added. This made I/O operations more efficient.

Java SE 5 (September 2004)

Generics (type safety), annotations, enums, autoboxing/unboxing, enhanced for loops, and varargs were introduced. Language semantics were modernised, and multithreaded programming was improved.

Java SE 6 (December 2006)

Scripting support (JSR 223), web services with JAX-WS, JDBC 4.0, and pluggable annotations were added. Performance optimisations and GUI improvements were made.

Java SE 7 (July 2011)

Try-with-resources, the diamond operator (type inference), multiple exception catching, and NIO.2 (file system API) were introduced. Minor improvements such as binary literals and underscores in numeric literals were added.

Java SE 8 (March 2014, LTS)

Lambda expressions, the Stream API, default methods, and the Date/Time API (JSR 310) were introduced. This release brought functional programming to Java and created a major paradigm shift.

Java SE 9 (September 2017)

The module system (Project Jigsaw), JShell (interactive shell), and multi-release JAR files were added. The platform became modular.

Java SE 10 (March 2018)

Local variable type inference (var keyword) and parallel full GC in the G1 garbage collector were introduced.

Java SE 11 (September 2018, LTS)

HTTP Client standardised, Epsilon GC (low-latency GC) added. Java EE and CORBA modules removed.

Java SE 12 (March 2019)

Switch statements (preview) and Shenandoah GC (experimental) introduced.

Java SE 13 (September 2019)

Text blocks (preview) and dynamic CDS archives were added.

Java SE 14 (March 2020)

Records (preview), pattern matching for instanceof (preview), and helper null pointer exceptions were introduced.

Java SE 15 (September 2020)

Changes such as sealed classes (preview), private classes, Text Blocks, and the removal of CMS GC were made.

Java SE 16 (March 2021)

Pattern matching for instanceof (second preview), records (finalised), and ZGC support for macOS were added.

Java SE 17 (September 2021, LTS)

Sealed classes were finalised, pattern matching for switch (preview) and the foreign memory access API (incubating) were introduced.

Java SE 18 (March 2022)

UTF-8 has been made the default, code snippets in Javadoc and switch pattern matching (preview) have been added.

Java SE 19 (September 2022)

Record patterns (preview), foreign function and memory API (preview), and structured concurrency (incubating) have been introduced.

Java SE 20 (March 2023)

Scoped values (incubating) and other preview/incubator features added.

Java SE 21 (September 2023, LTS)

Record patterns, pattern matching for switch, virtual threads (finalise), string templates (preview), and unnamed classes/main methods (preview) were introduced.

Java SE 22 (March 2024)

Foreign function and memory API, unnamed variables/patterns, and structured concurrency (preview) were added.

Java SE 23 (September 2024)

Changes such as Markdown documentation comments, pattern matching for primitive types (preview), and the removal of string templates were made.

Java SE 24 (March 2025)

Generational Shenandoah (experimental), compact object headers (experimental), and pattern matching for primitive types (second preview) were added.

Java 25 Features

Press enter or click to view image in full size
Java (Generated by Gemini)

1. Compact Source Files & Instance Main Methods (JEP 512)

We can write small Java programs (scripts, teaching, prototyping) without having to declare a class.

Reduces the learning curve by offering out-of-class files and instance main methods for beginner-level developers.

Imagine you are working as a tutor on an education platform to teach Java to new developers. Compact Source Files enable students to write code quickly.

Get Berat Yesbek’s stories in your inbox

Join Medium for free to get updates from this writer.

Before

package com.beratyesbek;

import java.util.List;

public class Main{
public static void main(String[] args) {
System.out.println("Hello, Java 25!");
}
}

After

void main() {
IO.println("Hello, Java 25!");
}

2. Flexible Constructor Bodies (JEP 513)

Java allows us to make calls before super() and this(), making our code more flexible and secure.

Imagine we need to build a modern account system in Java 25 featuring both Personal and Enterprise accounts. The key challenge is that although both use an Account ID, the validation rules for that ID must be completely different based on the account type. We aim to use Java 25 features to handle this type-specific validation in a more flexible, secure, and type-safe way. Flexible Constructor bodies enable us to manage it securely before moving to the parent class.

After Java 25

package com.beratyesbek.jep513;

abstract class Account {
protected String accountId;

public Account(String accountId) {
this.accountId = accountId;
}
}

public class PersonalAccount extends Account {

public PersonalAccount(String accountId) {
if (!accountId.matches("[0-9]+")) {
throw new IllegalArgumentException("Account ID must contain only digits.");
}
super(accountId);
}
}

Before Java 25

package com.beratyesbek.preversion.jep513;


abstract class Account {

protected String accountId;

public Account(String accountId) {
this.accountId = accountId;
}

}

public class PersonalAccount extends Account {

public PersonalAccount(String accountId) {
super(accountId);
if (!accountId.matches("[0-9]+")) {
throw new IllegalArgumentException("Account ID must contain only digits.");
}
}

}

3. Primitive Types in Patterns (JEP 507) (Preview)

Pattern matching (in instanceof, switch) now works not only for reference types but also for primitive types like int, double Etc.

This feature enhances instanceof, switch statements, and other pattern-matching constructs to work with primitive types. Previously, pattern matching was only available for reference types (such as classes or records). JEP 507 extends this capability to primitive types, enabling code that is shorter, more readable, and safer.

After Java 25

package com.beratyesbek.jep507;

public class OrderProcessor {
public double calculateDiscount(int amount) {
return switch (amount) {
case int a when a >= 0 && a <= 100 -> 0.05;
case int a when a <= 500 -> 0.10;
case int a -> 0.15;
};
}
}

Before Java 25

package com.beratyesbek.preversion.jep507;

public class OrderProcessor {
public double calculateDiscount(int amount) {
if (amount >= 0 && amount <= 100) {
return 0.05;
} else if (amount <= 500) {
return 0.10;
} else {
return 0.15;
}
}
}
  • The switch statement directly processes the int value using pattern matching.
  • When conditions explicitly state the ranges, making the code more readable and declarative.
  • The risk of error is reduced because the ranges are clearly defined.
  • JEP 507 preserves runtime performance by performing optimisations at compile time. Switch statements are generally better optimised than if-else structures.

4. Module Import Declarations (JEP 511)

to import all the packages exported by that module, instead of having to import individual packages or classes.

This helps in modules/JPMS scenarios, as well as in compact source files, or for developers who want less boilerplate when using types from a module.

After Java 25

import module java.base; // SEE

void main() {
List<Long> list = List.of(1L, 2L, 3L, 4L, 5L);
long sum = list.stream().mapToLong(Long::longValue).sum();
BigDecimal bigDecimal = new BigDecimal("0");
IO.println(bigDecimal);
IO.println("sum = " + sum);

}

Before Java 25

package com.beratyesbek.preversion.jep511;

import java.util.HashMap;
import java.util.List; // SEE
import java.math.BigDecimal;


public class Calculator {
static void main() {
List<Long> list = List.of(1L, 2L, 3L, 4L, 5L);
HashMap<Long, List<Long>> map = new HashMap<>();
BigDecimal bigDecimal = new BigDecimal("0");
long sum = list.stream().mapToLong(Long::longValue).sum();
IO.println("sum = " + sum);
}
}

5. Structured Concurrency (JEP 505) (Preview)

JEP 505 provides a preview API for structured concurrency in Java 25, designed to simplify concurrent programming. The core component is the StructuredTaskScope class in the java.util.concurrent package, which organises related tasks running in different threads into a single unit of work. This API improves error handling, cancellation, and observability in concurrent applications, particularly those using virtual threads (JEP 444), which was introduced with Java 21

Benefits

Simplified Error Management: If a sub-task fails, the others are automatically cancelled, preventing thread leaks and ensuring consistent error management. For example, in a server task that retrieves user and order data, if one fails, the other is cancelled, preventing unnecessary calculations.

Automatic Cancellation Propagation: If the main task is interrupted, all sub-tasks are cancelled, ensuring resources are cleanly released.

Clean Code Structure: The use of try-with-resources and StructuredTaskScope limits the lifetimes of sub tasks to lexical scope, making the code more readable and easier to maintain.

Advanced Observability: Thread dumps show the subtask hierarchy and reveal parent-child relationships, which facilitates debugging and tracing.

Flexible Completion Policies: The Joiner interface allows developers to define custom policies, such as collecting all successful results or returning the first successful result, catering to various use cases.

Compatibility with Virtual Threads: Structural concurrency works well with virtual threads, enabling the creation of scalable, easy-to-maintain server applications that process thousands or millions of requests concurrently.

Java 25 Example:

package com.beratyesbek.jep505;

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;

public class DashboardService {
public String fetchDashboardData(String userId) throws Exception {
System.out.println("Starting fetchDashboardData for " + userId);
try (var scope = StructuredTaskScope.open()) {
Subtask<String> profileTask = scope.fork(() -> fetchUserProfile(userId));
Subtask<String> transactionsTask = scope.fork(() -> fetchRecentTransactions(userId));
Subtask<String> notificationsTask = scope.fork(() -> fetchNotifications(userId));

System.out.println("All tasks forked; joining...");
scope.join();

System.out.println("All tasks completed successfully");
return "Dashboard data: Profile=" + profileTask.get() +
", Transactions=" + transactionsTask.get() +
", Notifications=" + notificationsTask.get();
} catch (StructuredTaskScope.FailedException e) {
throw new RuntimeException("Dashboard fetch failed: " + e.getMessage(), e);
}
}

private String fetchUserProfile(String userId) throws Exception {
System.out.println("Starting user profile fetch for " + userId);
Thread.sleep(800); // Simulate API call latency
System.out.println("Finished user profile fetch for " + userId);
return "User: John Doe, Age: 30";
}

private String fetchRecentTransactions(String userId) throws Exception {
System.out.println("Starting transactions fetch for " + userId);
Thread.sleep(1200); // Simulate longer database query
System.out.println("Finished transactions fetch for " + userId);
return "Last transaction: $100 purchase";
}

private String fetchNotifications(String userId) throws Exception {
System.out.println("Starting notifications fetch for " + userId);
Thread.sleep(300); // Simulate quick cache read
System.out.println("Finished notifications fetch for " + userId);
return "New message from support";
}

public static void main(String[] args) throws Exception {
DashboardService service = new DashboardService();
System.out.println(service.fetchDashboardData("USER123"));
}
}

6. JEP 502: Stable Values (Preview)

Stable Values (JEP 502), introduced as a preview API in Java 25, is designed to accelerate application startup and create a concurrency safe structure by enabling the deferred initialisation of immutable data. This prevents delays caused by eager initialisation in complex enterprise level classes, such as loggers, database connection pools, or configuration objects, while preserving JVM optimisations like constant folding.

An object is created on-demand, meaning that an object defined with StableValue.of() is initialised once upon first access via the orElseSet(Supplier) method and remains immutable thereafter. This enhances resource efficiency by preventing the creation of multiple objects across concurrent threads.

Java 25 Example:

package com.beratyesbek.jep502;

public class OrderService {

private final StableValue<PaymentService> paymentService = StableValue.of();

PaymentService getPaymentService() {
return paymentService.orElseSet(() -> new PaymentService());
}

public void processOrder() {
PaymentService ps = getPaymentService();
if (ps == null) {
throw new IllegalStateException("No PaymentService available in the current scope.");
}
ps.processPayment();
System.out.println("Processing order with " + ps);
}
}
---------------------------------
package com.beratyesbek.jep502;

public class PaymentService {

public void processPayment() {
System.out.println("Processing payment...");
}
}
---------------------------------
package com.beratyesbek.jep502;

public class Main {

static void main() {
OrderService orderService = new OrderService();
orderService.processOrder();
}
}

Java 25 Concurrency Example:

package com.beratyesbek.jep502.concurrencyexample;

import java.util.concurrent.StructuredTaskScope.Subtask;
import module java.base;

public class UserDashboardService {
private final StableValue<UserContext> userContext = StableValue.of();

UserContext getUserContext() {
return userContext.orElseSet(() -> {
return new UserContext("USER123");
});
}

public String fetchDashboard(String userId) throws Exception {
System.out.println("Starting fetchDashboard for " + userId);
UserContext context = getUserContext();

try (var scope = StructuredTaskScope.open()) {
Subtask<String> profileTask = scope.fork(() -> fetchProfile(context));
Subtask<String> transactionsTask = scope.fork(() -> fetchTransactions(context));
Subtask<String> notificationsTask = scope.fork(() -> fetchNotifications(context));

System.out.println("All tasks forked; joining...");
scope.join();

System.out.println("All tasks completed successfully");
return "Dashboard: Profile=" + profileTask.get() +
", Transactions=" + transactionsTask.get() +
", Notifications=" + notificationsTask.get();
} catch (StructuredTaskScope.FailedException e) {
throw new RuntimeException("Dashboard fetch failed: " + e.getMessage(), e);
}
}

private String fetchProfile(UserContext context) throws Exception {
System.out.println("Profile task - Using context: " + context.userId + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(800);
return "Profile: " + context.userId + " details";
}

private String fetchTransactions(UserContext context) throws Exception {
System.out.println("Transactions task - Using context: " + context.userId + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(1200);
return "Transactions for " + context.userId;
}

private String fetchNotifications(UserContext context) throws Exception {
System.out.println("Notifications task - Using context: " + context.userId + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(300);
return "Notifications for " + context.userId;
}

// Immutable context
static class UserContext {
public final String userId;
UserContext(String userId) { this.userId = userId; }
}

public static void main(String[] args) throws Exception {
UserDashboardService service = new UserDashboardService();
System.out.println(service.fetchDashboard("USER123"));
}
}

7. Scoped Values (JEP 506)

It defines Scoped Values, a feature finalised in Java 25. This is a significant innovation that strengthens Java’s concurrency model. It is specifically designed to address the limitations of ThreadLocal variables.

Scoped Values is a mechanism that enables immutable data to be shared within a limited scope in a thread and its sub threads. Like ThreadLocal, which has been used since Java 1.2, it enables data sharing without parameters, but offers the following advantages:

  • Limited lifetime: Data is only bound for the duration of a specific code block (run() or call() invocation). This prevents memory leaks.
  • Immutability: Once bound, data cannot be modified, preventing data races and incorrect flows.
  • Efficient inheritance: Automatic and low-cost transfer to child threads, particularly compatible with virtual threads (JEP 444) and structured concurrency (JEP 505).

After Java 25:

package com.beratyesbek.jep506;

import java.util.concurrent.Callable;
import java.util.concurrent.StructuredTaskScope;

class OrderService {
private static final ScopedValue<Request> REQUEST_CONTEXT = ScopedValue.newInstance();

public void handleOrderRequest(String userId, Callable<Void> handler) throws Exception {
var context = new Request(userId, java.util.UUID.randomUUID().toString());
ScopedValue.where(REQUEST_CONTEXT, context).run(() -> {
try {
handler.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

public String processOrder() throws Exception {
try (var scope = StructuredTaskScope.open()) {
var inventoryTask = scope.fork(() -> checkInventory());
var paymentTask = scope.fork(() -> processPayment());

scope.join();
return "Order processed: Inventory=" + inventoryTask.get() + ", Payment=" + paymentTask.get();
}
}

private String checkInventory() throws Exception {
var ctx = REQUEST_CONTEXT.get();
System.out.println("Inventory check for user: " + ctx.userId + ", Tx: " + ctx.transactionId + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(500);
return "Available";
}

private String processPayment() throws Exception {
var ctx = REQUEST_CONTEXT.get();
System.out.println("Payment for user: " + ctx.userId + ", Tx: " + ctx.transactionId + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(800);
return "Approved";
}

public static void main(String[] args) throws Exception {
var service = new OrderService();
service.handleOrderRequest("user123", () -> {
try {
System.out.println(service.processOrder());
} catch (Exception e) {
e.printStackTrace();
}
return null;
});
}
}

// ---------------
package com.beratyesbek.jep506;

public final class Request {
public final String userId;
public final String transactionId;
public Request(String userId, String transactionId) {
this.userId = userId;
this.transactionId = transactionId;
}
}

Before Java 25:

package com.beratyesbek.preversion.jep506;

import java.util.concurrent.Callable;
import java.util.UUID;

class OrderService {
private static final ThreadLocal<Request> REQUEST_CONTEXT = new ThreadLocal<>();

public void handleOrderRequest(String userId, Callable<Void> handler) throws Exception {
var context = new Request(userId, UUID.randomUUID().toString());
REQUEST_CONTEXT.set(context);
try {
handler.call();
} finally {
REQUEST_CONTEXT.remove();
}
}

public String processOrder() throws Exception {
var ctx = REQUEST_CONTEXT.get();
if (ctx == null) {
throw new IllegalStateException("Context not set");
}

var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor();
try {
var inventoryFuture = executor.submit(() -> checkInventory(ctx));
var paymentFuture = executor.submit(() -> processPayment(ctx));

var inventoryResult = inventoryFuture.get();
var paymentResult = paymentFuture.get();
return "Order processed: Inventory=" + inventoryResult + ", Payment=" + paymentResult;
} finally {
executor.close();
}
}

private String checkInventory(Request request) throws Exception {
System.out.println("Inventory check for user: " + request.userId + ", Tx: " + request.transactionId + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(500);
return "Available";
}

private String processPayment(Request request) throws Exception {
System.out.println("Payment for user: " + request.userId + ", Tx: " + request.transactionId + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(800);
return "Approved";
}

public static void main(String[] args) throws Exception {
var service = new OrderService();
service.handleOrderRequest("user123", () -> {
try {
System.out.println(service.processOrder());
} catch (Exception e) {
e.printStackTrace();
}
return null;
});
}
}

// -------------------------

package com.beratyesbek.preversion.jep506;

public final class Request {
public final String userId;
public final String transactionId;
public Request(String userId, String transactionId) {
this.userId = userId;
this.transactionId = transactionId;
}
}

With the official release of Java 25 on September 16, 2025, the programming world was once again filled with excitement over the innovations offered by this established language. This release brings a series of features that push the boundaries of the language, libraries, performance mechanisms, and developer tools through 18 different JDK Enhancement Proposals (JEPs). For example, the fifth preview of structured concurrency, the finalised version of scoped values, primitive types in patterns, and the key derivation function API are innovations that directly address modern software development needs. However, due to time and focus constraints, this blog post only touches upon the most noteworthy concurrency and performance-focused features tools, such as flexible constructor bodies and the vector API, for instance, have the potential to transform daily productivity in enterprise applications. If you wish to learn more about the full scope of Java 25, I recommend consulting Oracle’s official documentation or the OpenJDK community’s resources; starting here, you can explore each of the 18 JEPs offered in this release.

This article also serves as the final stop on a journey summarising Java’s tremendous evolution since its inception in 1990 as the ‘Oak’ project at Sun Microsystems. Since those years, Java has continuously evolved, succeeding in offering programmers unique platform independence and a rich ecosystem: it embraced functional programming with lambda expressions and the stream API in Java 8, strengthened type safety with sealed classes in Java 14 and later, simplified data modelling with records in Java 16, revolutionised concurrency with virtual threads in Java 21, minimised performance bottlenecks with garbage collector (GC) improvements (e.g., generational Shenandoah) and innovations in JDK tools (such as JFR profiling), and increased flexibility with default and private methods in interfaces. In addition, countless small but effective updates — such as binary literals, text blocks, and pattern matching — have made the language more expressive and user-friendly with each passing day. Still, Java 25 stands out as the latest chapter in this evolution, particularly in the field of concurrency, where features like structured concurrency and scoped values make multi-thread management safer and more structured, while on the cryptography side, PEM encodings and the key derivation function API reinforce quantum-ready security standards, thus forming an indispensable foundation for cloud-based, high-scale, and security-focused applications.

Ultimately, this suite of features in Java 25 once again demonstrates how seamlessly the language adapts to today’s complex software ecosystem, from AI integration to big data processing, microservices architectures to edge computing. Despite being declared ‘dead’ numerous times over its 35-year journey, Java has consistently risen from the ashes as an innovative force.

See you in the next article..

GitHub:

References:

--