Skip to main content
Innovation|Innovation

Java Evolution: What Changed from Java 8 to Java 24 and Why It Matters

A comprehensive guide to Java's evolution from version 8 to 24 — covering lambdas, streams, records, sealed classes, virtual threads, pattern matching, and every major feature that changed how we write Java.

April 8, 20269 min read4 views0 comments
Share:

From Verbose to Elegant: A Decade of Java

Java 8 rewrote the rules in 2014 with lambdas and streams. Since then, every six months a new version ships. Most developers jumped from Java 8 to 11, then 17, now 21. Here is everything that changed — version by version — with the features you will actually use.

Java 8 (2014) — LTS — The Big Bang of Modern Java

Java 8 introduced functional programming to Java. This is the version most developers know deeply.

Lambda Expressions & Functional Interfaces

Replaced anonymous inner classes with concise syntax. Core interfaces: Predicate<T>, Function<T,R>, Consumer<T>, Supplier<T>.

// Lambda + Predicate composition
Predicate<Product> inStock = p -> p.getQuantity() > 0;
Predicate<Product> isExpensive = p -> p.getPrice() > 100.0;

List<Product> premiumAvailable = products.stream()
    .filter(inStock.and(isExpensive))
    .collect(Collectors.toList());

// All 4 core functional interfaces
Predicate<Employee> isSenior = e -> e.getYears() > 5;         // T -> boolean
Function<Employee, String> fullName = e -> e.getFirstName() + " " + e.getLastName(); // T -> R
Consumer<Employee> sendWelcome = e -> emailService.send(e);   // T -> void
Supplier<Employee> defaultEmp = () -> new Employee("Guest");  // () -> T

Stream API

Declarative data processing with lazy evaluation. Key operations: filter, map, flatMap, collect, reduce, groupingBy.

// Filter + Map + Collect
List<String> activeNames = employees.stream()
    .filter(e -> e.getStatus() == Status.ACTIVE)
    .map(Employee::getName)
    .sorted()
    .collect(Collectors.toList());

// GroupingBy + downstream counting
Map<Department, Long> headcount = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));

// FlatMap — flatten nested collections
List<String> allTags = articles.stream()
    .flatMap(a -> a.getTags().stream())
    .distinct()
    .collect(Collectors.toList());

// PartitioningBy — split into true/false groups
Map<Boolean, List<Order>> shipped = orders.stream()
    .collect(Collectors.partitioningBy(Order::isShipped));

Optional

Container for nullable values. Use for return types only, never for fields or parameters.

// Safe navigation through nested nullables
String cityName = Optional.ofNullable(customer)
    .map(Customer::getAddress)
    .map(Address::getCity)
    .orElse("UNKNOWN");

// orElseGet is lazy — supplier only runs if empty
Product fallback = products.stream()
    .filter(Product::isAvailable)
    .findFirst()
    .orElseGet(() -> new Product("Default", 0.0));

CompletableFuture

Non-blocking async operations with composition.

CompletableFuture<Double> priceCheck = CompletableFuture.supplyAsync(() -> fetchPrice(sku));
CompletableFuture<Integer> stockCheck = CompletableFuture.supplyAsync(() -> fetchStock(sku));

// Combine two async results
CompletableFuture<String> summary = priceCheck
    .thenCombine(stockCheck, (price, stock) ->
        "Price: $" + price + ", Stock: " + stock)
    .exceptionally(ex -> "Error: " + ex.getMessage()); // fallback on error

java.time API & Default Methods

Immutable, thread-safe date/time classes: LocalDate, LocalDateTime, ZonedDateTime, Instant, Duration. Interfaces gained default methods for backward-compatible evolution.

Java 9 (2017) — Modules & Collection Factories

Immutable Collection Factories

List<String> colors = List.of("red", "green", "blue");       // immutable
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);                // immutable
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87); // immutable

Optional & Stream Enhancements

// Optional: ifPresentOrElse, or(), stream()
opt.ifPresentOrElse(v -> process(v), () -> logMissing());
Optional<String> result = opt.or(() -> Optional.of("default"));

// Stream: takeWhile, dropWhile
List<Integer> prefix = Stream.of(1, 2, 3, 10, 4)
    .takeWhile(x -> x < 5)  // [1, 2, 3] — stops at first false
    .toList();

Private Interface Methods

Helper methods in interfaces for code reuse across default methods.

Java 10 (2018) — var Keyword

// Local variable type inference
var products = new ArrayList<Product>();    // inferred as ArrayList<Product>
var stream = products.stream();              // inferred as Stream<Product>

// Works in for loops
for (var product : products) {
    System.out.println(product.getName());
}

// Unmodifiable copies
var snapshot = List.copyOf(mutableList);  // immutable copy

Constraints: var works only for local variables — not fields, method parameters, or return types. Cannot initialize with null.

Java 11 (2018) — LTS — First LTS After Java 8

// String enhancements
"  hello  ".isBlank();        // false
"  hello  ".strip();          // "hello" (Unicode-aware trim)
"line1\nline2".lines();       // Stream<String>
"ha".repeat(3);               // "hahaha"

// Optional.isEmpty() — opposite of isPresent()
Optional.empty().isEmpty();   // true

// var in lambda parameters (useful for adding annotations)
list.stream()
    .map((@NonNull var s) -> s.toUpperCase())
    .toList();

// Predicate.not() — cleaner negation
list.stream().filter(Predicate.not(String::isBlank)).toList();

Java 12–13 (2019) — Switch Expressions & Text Blocks

// Switch expressions — arrow syntax, no fall-through, yields values
String label = switch (priority) {
    case HIGH, CRITICAL -> "URGENT";
    case MEDIUM         -> "REVIEW";
    case LOW            -> "BACKLOG";
};

// Text blocks — multi-line strings
String query = """
        SELECT id, name, department
        FROM employees
        WHERE status = 'ACTIVE'
        ORDER BY hire_date
        """;

// String.indent() and String.transform()
String result = input.transform(String::strip)
                     .transform(String::toUpperCase);

Java 14 (2020) — Records & Pattern Matching

Records — Immutable Data Classes

Auto-generates constructor, accessors, equals(), hashCode(), toString().

// One line replaces 50+ lines of boilerplate
public record OrderSummary(String orderId, String customerName,
                           double total, Status status) {}

// Compact constructor for validation
public record Price(double value, String currency) {
    public Price {
        if (value < 0) throw new IllegalArgumentException("Negative price");
        currency = currency.toUpperCase();
    }
}

Pattern Matching for instanceof

// Before: cast manually
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// After: binding variable in one step
if (obj instanceof String s && s.length() > 5) {
    System.out.println(s.toUpperCase());
}

Java 15 (2020) — Sealed Classes Preview & ZGC Production

Text blocks finalized. ZGC garbage collector moved to production-ready. Sealed classes introduced in preview. Nashorn JavaScript engine removed.

// ZGC — now production-ready, no experimental flag needed
// java -XX:+UseZGC MyApp

// Hidden classes — used internally by frameworks like Spring/Hibernate
// for runtime-generated proxy classes

Java 16 (2021) — Stream.toList() & Records Finalized

// Stream.toList() — cleaner than Collectors.toList()
List<String> names = employees.stream()
    .map(Employee::getName)
    .toList();  // returns unmodifiable list

// mapMulti — alternative to flatMap for conditional one-to-many
Stream.of(1, 2, 3, 4).<String>mapMulti((num, consumer) -> {
    if (num % 2 == 0) {
        consumer.accept(num + " is even");
        consumer.accept(num + " doubled = " + (num * 2));
    }
}).toList();

Java 17 (2021) — LTS — Sealed Classes & Pattern Switch

The current enterprise standard. Sealed classes and pattern matching for switch finalized.

Sealed Classes — Controlled Inheritance

public sealed interface PaymentResult
    permits Success, Failure, Pending {
}
public record Success(String txnId, double amount) implements PaymentResult {}
public record Failure(String error, int code) implements PaymentResult {}
public record Pending(String txnId) implements PaymentResult {}

// Exhaustive switch — compiler ensures all cases covered
String describe(PaymentResult result) {
    return switch (result) {
        case Success s  -> "Paid $" + s.amount();
        case Failure f  -> "Failed: " + f.error();
        case Pending p  -> "Pending: " + p.txnId();
    };
}

Algebraic Data Types (ADTs)

Sealed interface + records = sum types from functional programming. The compiler guarantees exhaustiveness — no default case needed.

Java 18 (2022) — UTF-8 Default & Simple Web Server

// UTF-8 is now the default charset on ALL platforms
// No more "works on my machine" charset bugs
System.out.println(Charset.defaultCharset()); // "UTF-8" everywhere

// Built-in simple web server for testing/prototyping
// Command line: jwebserver -p 9000 -d /path/to/files
var server = SimpleFileServer.createFileServer(
    new InetSocketAddress(8080),
    Path.of("/www"),
    OutputLevel.INFO
);
server.start();

// @snippet in Javadoc replaces <pre>{@code ...}</pre>

Java 19 (2022) — Virtual Threads & Structured Concurrency Preview

First preview of Project Loom — the biggest concurrency change since Java 5.

// Virtual threads: lightweight (~KB vs ~MB for platform threads)
Thread.ofVirtual().start(() -> {
    System.out.println("Running in virtual thread");
});

// Handle 100,000 concurrent tasks — impossible with platform threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
}

// Structured concurrency — fan-out pattern
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<User>    user  = scope.fork(() -> fetchUser());
    Subtask<Account> acct  = scope.fork(() -> fetchAccount());
    scope.join().throwIfFailed();
    return new Profile(user.get(), acct.get());
}

Java 20 (2023) — Refinement Release

All 7 JEPs are previews/incubators. Key addition: Scoped Values — a lightweight, immutable replacement for ThreadLocal designed for virtual threads.

// ScopedValue — replaces ThreadLocal for virtual threads
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();

void handleRequest(String username) {
    ScopedValue.runWhere(CURRENT_USER, username, () -> {
        processRequest(); // all called methods can read CURRENT_USER.get()
    });
}
// Why better? Immutable, auto-inherited by child virtual threads, bounded lifetime

Java 21 (2023) — LTS — Virtual Threads, Record Patterns, Sequenced Collections

The latest LTS. Virtual threads, pattern matching for switch, and record patterns all finalized.

Virtual Threads (Finalized)

// M:N scheduling — millions of virtual threads on few OS threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> {
            // Each task gets its own virtual thread
            return callExternalService();
        });
    }
}

Record Patterns — Destructuring

record Point(int x, int y) {}
record Line(Point start, Point end) {}

// Nested destructuring in switch
String describe(Object obj) {
    return switch (obj) {
        case Line(Point(var x1, var y1), Point(var x2, var y2))
            -> "Line from (%d,%d) to (%d,%d)".formatted(x1, y1, x2, y2);
        case Point(var x, var y) -> "Point at (%d,%d)".formatted(x, y);
        default -> "Unknown";
    };
}

Sequenced Collections

// New methods on ordered collections
List<String> list = List.of("a", "b", "c");
list.getFirst();   // "a"
list.getLast();     // "c"
list.reversed();   // ["c", "b", "a"]

// Also works on LinkedHashSet, LinkedHashMap, Deque

Java 22 (2024) — Unnamed Variables & FFM API

Unnamed Variables (_)

// Intentionally unused variable — no more "unused variable" warnings
try (var _ = ScopedContext.acquire()) { /* don't need the ref */ }
for (var _ : collection) { count++; }
map.forEach((_, value) -> process(value));
try { riskyOp(); } catch (Exception _) { log("failed"); }

// In pattern matching
if (obj instanceof Point(var x, _)) { /* only care about x */ }

Stream Gatherers (Preview)

// Sliding window
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 batches
List<List<Integer>> batches = Stream.of(1,2,3,4,5,6,7)
    .gather(Gatherers.windowFixed(3)).toList();
// [[1,2,3], [4,5,6], [7]]

Foreign Function & Memory API (Finalized)

Safe replacement for JNI. Call native C functions directly from Java with memory safety guarantees.

Java 23 (2024) — Markdown Javadoc & ZGC Default

Primitive Types in Patterns (Preview)

String describe(int statusCode) {
    return switch (statusCode) {
        case 200 -> "OK";
        case 404 -> "Not Found";
        case int i when i >= 500 -> "Server Error: " + i;
        case int i -> "Other: " + i;
    };
}

Markdown Documentation Comments

/// Returns the **full name** of the user.
///
/// This method combines:
/// - First name
/// - Last name
///
/// @param user the user object
/// @return the concatenated full name
public String getFullName(User user) { ... }

Module Import Declarations (Preview)

// One import for an entire module
import module java.base;  // imports java.util, java.io, java.nio, etc.

Note: String Templates (previewed in 21–22) were withdrawn in Java 23 due to design issues.

Java 24 (2025) — Stream Gatherers Finalized, AOT, Virtual Thread Unpinning

The largest release ever with 24 JEPs.

Stream Gatherers (Finalized)

// Built-in: windowFixed, windowSliding, fold, scan, mapConcurrent
List<Response> responses = urls.stream()
    .gather(Gatherers.mapConcurrent(5, this::httpGet))
    .toList();

// Custom gatherer: distinct by key
static <T, K> Gatherer<T, ?, T> distinctBy(Function<T, K> keyExtractor) {
    return Gatherer.ofSequential(HashSet::new,
        (seen, element, downstream) -> {
            if (seen.add(keyExtractor.apply(element)))
                return downstream.push(element);
            return true;
        });
}
// Usage: people.stream().gather(distinctBy(Person::lastName)).toList();

Ahead-of-Time Class Loading (Project Leyden)

# Record → Build cache → Launch with up to 42% faster startup
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar Main
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar
java -XX:AOTCache=app.aot -cp app.jar Main

Virtual Thread Unpinning

Virtual threads no longer get pinned to carrier threads in synchronized blocks. This was the biggest limitation of virtual threads in Java 21–23.

// Java 21–23: synchronized + blocking I/O PINNED the carrier thread
// Java 24: virtual thread unmounts — carrier is free for other work
synchronized (sharedResource) {
    return fetchFromDatabase(); // no longer pins! Major perf improvement
}

Class-File API (Finalized)

Standard API for parsing and generating .class files. Replaces third-party libraries like ASM.

Other Notable Changes

Security Manager permanently disabled. Compact object headers (experimental, 10–20% heap reduction). Generational Shenandoah GC (experimental).

LTS Roadmap & Which Version to Target

VersionYearTypeHeadline Feature
Java 82014LTSLambdas, Streams, Optional
Java 112018LTSString enhancements, var in lambda, HttpClient
Java 172021LTSSealed classes, pattern matching, records finalized
Java 212023LTSVirtual threads, record patterns, sequenced collections
Java 25Sep 2025LTSNext LTS — expected to finalize structured concurrency, scoped values

Recommendation: For new projects, target Java 21 (current LTS). For enterprises still on Java 8 or 11, plan migration to 21. Wait for Java 25 (September 2025) if you need the next LTS.

Key Takeaways

Java 8→11: Functional foundations — lambdas, streams, var, string helpers.

Java 12→17: Boilerplate elimination — records, sealed classes, switch expressions, text blocks, pattern matching.

Java 18→24: Concurrency revolution — virtual threads, structured concurrency, scoped values, stream gatherers, AOT compilation.

Java is no longer the verbose language it was. Modern Java (17+) is concise, expressive, and performs better than ever.


Comments


Login to join the conversation.

Loading comments…

More from Innovation