Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Modern Java: from 8 to 25

A new Java version comes out every six months, and every two years one of them gets long-term support — LTS. These are versions 8, 11, 17, 21 and 25. Many projects and online articles are still written in Java 8, and modern code looks noticeably different. This lesson gathers the main changes, with links to the lessons that cover them in detail.

Java 8: the foundation

Java 8 (2014) is the most important version in the language's history. It introduced:

All of this is an ordinary part of modern code, not "new stuff".

Java 9–11: less boilerplate

void main() {
    var names = List.of("Anna", "Oleg");
    IO.println(names);
    IO.println("ab".repeat(3));
}

Output:

[Anna, Oleg]
ababab

Java 12–17: new shapes for data

Java 18–21: pattern matching and virtual threads

void main() {
    List<String> queue = List.of("Anna", "Oleg", "Ira");
 
    IO.println(queue.getFirst());
    IO.println(queue.getLast());
    IO.println(queue.reversed());
}

Output:

Anna
Ira
[Ira, Oleg, Anna]

Before, the last element took list.get(list.size() - 1).

Java 22–25: easier to start

  • The unnamed variable _ for values you don't need (Java 22) — record patterns.
  • Compact source files and a main method without static and parameters, the IO class for input and output, automatic import of the java.base packages (Java 25) — the first program.
  • Code before the super(...) call in a constructor — for example, argument checks (Java 25).
  • Scoped values, ScopedValue — a replacement for ThreadLocal with virtual threads (Java 25) — scoped values.

Then and now

The same task — totalling orders per customer — in Java 8 style and in Java 25 style. First the old version:

import java.util.*;
 
public class Main {
    static class Order {
        private final String customer;
        private final int amount;
 
        Order(String customer, int amount) {
            this.customer = customer;
            this.amount = amount;
        }
 
        String getCustomer() { return customer; }
        int getAmount() { return amount; }
    }
 
    public static void main(String[] args) {
        List<Order> orders = Arrays.asList(new Order("Anna", 20), new Order("Oleg", 35), new Order("Anna", 15));
        Map<String, Integer> totals = new TreeMap<>();
        for (Order order : orders) {
            Integer current = totals.get(order.getCustomer());
            totals.put(order.getCustomer(), (current == null ? 0 : current) + order.getAmount());
        }
        System.out.println(totals);
    }
}

Output:

{Anna=35, Oleg=35}

Now the modern one — a record instead of a class, List.of, a stream with grouping and a compact source file:

record Order(String customer, int amount) {}
 
void main() {
    var orders = List.of(new Order("Anna", 20), new Order("Oleg", 35), new Order("Anna", 15));
 
    var totals = orders.stream()
            .collect(Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.summingInt(Order::amount)));
 
    IO.println(totals);
}

Output:

{Anna=35, Oleg=35}

Both versions run on Java 25, but the second is a third of the length and reads like a description of the task.

Common mistake

var doesn't make Java a dynamic language: the variable's type is inferred once from the initial value and never changes:

void main() {
    var count = 10;
    count = "ten";
}

Output:

Main.java:3: error: incompatible types: String cannot be converted to int
    count = "ten";
            ^
1 error
error: compilation failed

And remember that new features need a new version: if a project is built for Java 17, it has records, but not record patterns or compact source files. The version is set in the build settings, for example <maven.compiler.release>25</maven.compiler.release> in Maven.

Thanks for reading!

Donate to the site — we work hard for you.