Modern Java: A Version Feature Guide

What changed in each release since Java 8, which old habits it replaces, and what to reach for in new code today.

Why this matters

A great deal of Java material still teaches patterns that were correct in 2010 and are not the best answer now. This note lists what changed and what it replaced, so older examples can be recognised for what they are.

Java 8: the turning point

// Lambdas and functional interfaces
Runnable task = () -> System.out.println("run");

// Streams
List<String> titles = notes.stream().filter(Note::published).map(Note::title).toList();

// Optional
Optional<Note> found = repository.findById(42);

// The modern date and time API
LocalDate today = LocalDate.now();

// Default and static interface methods
interface Validator { default Validator and(Validator other) { return null; } }
ReplacesWith
Anonymous classes for one methodLambdas
Loops that filter and collectStreams
Returning nullOptional
Date, Calendar, SimpleDateFormatjava.time

Java 9 to 11

// 9: immutable collection factories
List<String> roles = List.of("admin", "editor");
Map<String, Integer> limits = Map.of("free", 10, "paid", 100);

// 9: the module system, and private interface methods
// 9: takeWhile, dropWhile, Optional.stream, Stream.iterate with a condition

// 10: local variable type inference
var counts = new HashMap<String, Integer>();

// 11: String and Files convenience
"  text  ".strip();
"".isBlank();
"ab".repeat(3);
String content = Files.readString(path);

// 11: the standard HTTP client, and single file source launch

Java 12 to 17

// 14: switch expressions
String type = switch (day) {
    case 1, 7 -> "weekend";
    default   -> "weekday";
};

// 14: helpful NullPointerException messages, on by default from 15

// 15: text blocks
String query = """
        SELECT id, title
        FROM notes
        """;

// 16: pattern matching for instanceof
if (value instanceof String text && !text.isBlank()) { }

// 16: records
public record Money(String currency, long minorUnits) { }

// 16: Stream.toList()
List<String> titles = stream.toList();

// 17: sealed classes
public sealed interface Shape permits Circle, Square { }

Java 18 to 21

// 18: UTF-8 becomes the default charset everywhere

// 19 onwards: ExecutorService is AutoCloseable
try (var executor = Executors.newFixedThreadPool(4)) { }

// 21: virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handle(request));
}

// 21: pattern matching for switch, with guards
String result = switch (value) {
    case Integer i when i < 0 -> "negative";
    case Integer i            -> "number " + i;
    case String s             -> "text";
    default                   -> "other";
};

// 21: record patterns
if (shape instanceof Rectangle(double width, double height)) { }

// 21: sequenced collections
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
System.out.println(list.getFirst());
System.out.println(list.getLast());
System.out.println(list.reversed());

Java 22 to 25

// 25: compact source files and instance main methods
void main() {
    IO.println("A very small program.");
}

Java 25 is the current long term support release. Beyond the finalised features, several large efforts, structured concurrency and scoped values among them, have been moving through preview; check what is final in the version you target rather than assuming.

Old habit against modern equivalent

OldModern
new Date(), SimpleDateFormatLocalDate, DateTimeFormatter
Returning null for "not found"Optional
Anonymous class with one methodLambda or method reference
A class of getters, equals and hashCoderecord
if (x instanceof T) { T t = (T) x; }if (x instanceof T t)
A switch statement with breakA switch expression with arrows
Concatenated multi line stringsText blocks
Arrays.asList for a constantList.of
Collections.unmodifiableList at a boundaryList.copyOf
Vector, Hashtable, StackArrayList, HashMap, ArrayDeque
Manual close() in finallytry with resources
A thread pool for blocking I/OVirtual threads, on 21 and later
chain of instanceofSealed types with a pattern switch
new File(...)Path.of(...) and Files
Third party HTTP clients for simple callsjava.net.http.HttpClient

Still correct, and still worth knowing

  • Plain loops, when they read better than a stream.
  • synchronized, for simple mutual exclusion.
  • Interfaces and polymorphism, which pattern matching supplements rather than replaces.
  • Checked exceptions, used sparingly.
  • Arrays, for primitives and performance sensitive code.

Choosing a target version

VersionConsider it when
8Only a legacy constraint requires it
11An older system that cannot move yet
17Records and sealed types, widely supported
21Virtual threads and pattern matching for switch
25New projects, the current LTS

Target the newest long term support release your environment allows. The compatibility record is exceptionally strong, so upgrading is usually far less work than expected.

How to keep current

  • Read the release notes of each LTS rather than every intermediate version.
  • Check the version a tutorial targets before copying its patterns.
  • Let your editor suggest modern replacements, but understand each one before accepting it.
  • Prefer the standard library; several once essential third party utilities are now built in.

Practice

  1. Take an older class of yours and modernise it: records, var, text blocks and pattern matching.
  2. Replace every Date and SimpleDateFormat in a piece of code with java.time.
  3. Convert an instanceof chain into a sealed hierarchy with a pattern switch.
  4. Rewrite a fixed thread pool handling I/O using virtual threads and state the difference.
  5. List three features you would gain by moving a Java 8 project to the current LTS.

Conclusion

Java has changed considerably since 8, and most of the change removes ceremony rather than adding complexity. Target the newest LTS you can, learn the replacements above, and treat older examples as history rather than instruction.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Reflection in Java

Reflection inspects and manipulates classes at runtime. It powers most frameworks and should be rare in application code.

Read more
Java

Dynamic Proxies in Java

A dynamic proxy implements an interface at runtime and routes every call through one handler, which is how cross cutting behaviour is added.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.