Skip to main content

Java New Features: Java 8 through Java 21+

A practical guide to the most impactful features introduced in modern Java versions — from lambdas and streams (Java 8) to virtual threads and pattern matching (Java 21).


1. Java 8 (LTS) — The Big Leap

Java 8 was a transformational release that introduced functional programming constructs to Java.

Lambda Expressions

Anonymous function syntax for implementing functional interfaces:

// Before: anonymous inner class
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};

// After: lambda
Comparator<String> comp = (a, b) -> a.length() - b.length();

Functional Interfaces

An interface with exactly one abstract method. Annotated with @FunctionalInterface:

InterfaceMethodUse Case
Function<T, R>R apply(T t)Transform: T → R
Predicate<T>boolean test(T t)Filter: T → boolean
Consumer<T>void accept(T t)Side-effect: T → void
Supplier<T>T get()Factory: () → T
UnaryOperator<T>T apply(T t)Transform: T → T
BiFunction<T, U, R>R apply(T t, U u)Transform: (T, U) → R

Stream API

Declarative pipeline for processing collections:

List<String> names = people.stream()
.filter(p -> p.getAge() > 18) // filter
.sorted(Comparator.comparing(Person::getName)) // sort
.map(Person::getName) // transform
.distinct() // remove duplicates
.limit(10) // take first 10
.collect(Collectors.toList()); // terminal operation

// Reduction
int totalAge = people.stream()
.mapToInt(Person::getAge)
.sum();

// Grouping
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity));

// Parallel stream (use with caution)
long count = list.parallelStream()
.filter(s -> s.length() > 5)
.count();

Optional

A container that may or may not hold a value. Eliminates explicit null checks:

// Creating
Optional<String> opt = Optional.of("value");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(mayBeNull);

// Using
String result = opt
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.orElse("default");

// Chaining
String city = getUser()
.flatMap(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");

Date-Time API (java.time)

Replaces the problematic java.util.Date and Calendar:

// Immutable, thread-safe
LocalDate date = LocalDate.of(2024, 3, 15);
LocalTime time = LocalTime.of(14, 30);
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

// Duration and Period
Duration duration = Duration.between(time1, time2);
Period period = Period.between(date1, date2);

// Formatting
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String formatted = dateTime.format(fmt);
LocalDateTime parsed = LocalDateTime.parse("2024-03-15 14:30", fmt);

Interface Default Methods

Interfaces can now have method implementations:

public interface Collection<E> {
// Abstract method
boolean add(E e);

// Default method — existing implementations don't break
default boolean isEmpty() {
return size() == 0;
}

// Static method
static <T> Collection<T> empty() {
return Collections.emptyList();
}
}

2. Java 9 — Modularity

Module System (JPMS)

Organizes code into modules with explicit dependencies:

// module-info.java
module com.myapp {
requires java.sql;
requires java.logging;
exports com.myapp.api; // visible to other modules
opens com.myapp.internal; // accessible via reflection
}

JShell (REPL)

Interactive Java shell for experimenting:

jshell> int x = 42;
x ==> 42

jshell> "Hello".chars().sum()
$1 ==> 500

Collection Factory Methods

List<String> list = List.of("a", "b", "c");        // immutable
Set<Integer> set = Set.of(1, 2, 3); // immutable
Map<String, Integer> map = Map.of("a", 1, "b", 2); // immutable

Other Notable Features

  • Optional.ifPresentOrElse(), Optional.stream()
  • Private interface methods
  • Stream.takeWhile(), Stream.dropWhile(), Stream.ofNullable()
  • G1 becomes the default garbage collector
  • Compact Strings (internal byte[] instead of char[] for Latin-1)

3. Java 10 — Local Variable Type Inference

var keyword

Lets the compiler infer local variable types:

// Explicit type
ArrayList<Map<String, List<Integer>>> data = new ArrayList<>();

// With var — much cleaner
var data = new ArrayList<Map<String, List<Integer>>>();

// Works in for loops
for (var entry : map.entrySet()) {
var key = entry.getKey();
var value = entry.getValue();
}

Rules:

  • Only for local variables with initializers
  • Not for method parameters, return types, or fields
  • Not for null or lambda: var x = null;var f = () -> {};

4. Java 11 (LTS) — HTTP Client & More

HttpClient API

Modern, asynchronous HTTP client replacing HttpURLConnection:

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.header("Accept", "application/json")
.GET()
.build();

// Synchronous
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

// Asynchronous
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

Other Features

  • var in lambda parameters: (var x, var y) -> x + y
  • String methods: isBlank(), strip(), lines(), repeat(int)
  • Files methods: Files.readString(path), Files.writeString(path, content)
  • ZGC (experimental) — ultra-low-latency garbage collector

5. Java 12–13 — Switch Expressions & Text Blocks

Switch Expressions (Preview → Standard in 14)

// Traditional switch — verbose, fall-through prone
switch (day) {
case MONDAY:
case FRIDAY:
System.out.println("Work hard");
break;
case SATURDAY:
case SUNDAY:
System.out.println("Rest");
break;
}

// Switch expression — concise, no fall-through
String activity = switch (day) {
case MONDAY, FRIDAY -> "Work hard";
case SATURDAY, SUNDAY -> "Rest";
default -> "Regular day";
};

Text Blocks (Preview → Standard in 15)

Multi-line string literals:

// Before: messy concatenation
String json = "{\n" +
" \"name\": \"John\",\n" +
" \"age\": 30\n" +
"}";

// After: text blocks
String json = """
{
"name": "John",
"age": 30
}
""";

6. Java 14–15 — Records & Sealed Classes

Records

Immutable data carriers with auto-generated equals(), hashCode(), toString(), and accessors:

// Before: verbose POJO
public class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) { /* ... */ }
@Override public int hashCode() { /* ... */ }
@Override public String toString() { /* ... */ }
}

// After: one line
public record Point(int x, int y) { }

// Usage
Point p = new Point(3, 4);
int x = p.x(); // accessor (not getX())
System.out.println(p); // Point[x=3, y=4]

Records can have:

  • Custom constructors (compact or canonical)
  • Instance methods
  • Static fields and methods
  • Implement interfaces

Records cannot: extend other classes, have mutable fields, be subclassed.

Sealed Classes (Preview → Standard in 17)

Restrict which classes can extend or implement a type:

public sealed interface Shape permits Circle, Rectangle, Triangle { }

public record Circle(double radius) implements Shape { }
public record Rectangle(double width, double height) implements Shape { }
public final class Triangle implements Shape { /* ... */ }

Why sealed classes?

  • Enables exhaustive pattern matching (compiler knows all subtypes)
  • Models closed type hierarchies (algebraic data types)

7. Java 16 — Pattern Matching for instanceof

Eliminates redundant casting:

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

// After — binding variable
if (obj instanceof String s) {
System.out.println(s.length()); // s is already cast
}

// Works with logical operators
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase());
}

8. Java 17 (LTS) — Sealed Classes Finalized

Java 17 is a Long-Term Support release. Key finalized features:

  • Sealed classes (from preview to standard)
  • Pattern matching for instanceof (standard)
  • Text blocks (standard)
  • Records (standard)
  • Strong encapsulation of JDK internals (cannot access internal APIs by default)

Migration Note

Java 17 is the recommended upgrade target from Java 8 or 11. Key breaking changes:

  • Strong encapsulation of sun.misc.* APIs
  • Removed SecurityManager deprecation
  • Removed RMI Activation
  • Need --add-opens for frameworks using deep reflection

9. Java 21 (LTS) — Virtual Threads & Pattern Matching

Virtual Threads (Finalized)

Lightweight threads managed by the JVM, enabling massive concurrency:

// Create a virtual thread
Thread.ofVirtual().start(() -> {
System.out.println("Running in virtual thread");
});

// Virtual thread executor — 1 virtual thread per task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit 100,000 tasks — each gets a virtual thread
List<Future<String>> futures = IntStream.range(0, 100_000)
.mapToObj(i -> executor.submit(() -> fetchUrl("https://example.com/" + i)))
.toList();
}

Pattern Matching for switch (Finalized)

// Type patterns + guards
String describe(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "positive integer: " + i;
case Integer i -> "non-positive integer: " + i;
case String s -> "string of length " + s.length();
case null -> "null!";
default -> "something else";
};
}

// Sealed class exhaustiveness
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> t.base() * t.height() / 2;
// no default needed — Shape is sealed, compiler knows all cases
};
}

Record Patterns (Finalized)

Deconstruct records in pattern matching:

record Point(int x, int y) { }

// Deconstruct directly
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
}

// Nested patterns in switch
switch (shape) {
case Circle(var radius) when radius > 10 -> "large circle";
case Circle(var radius) -> "small circle with radius " + radius;
default -> "not a circle";
}

Sequenced Collections

New interfaces for collections with defined encounter order:

// SequencedCollection
interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}

// Usage
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst(); // "a"
list.getLast(); // "c"
list.reversed(); // ["c", "b", "a"]

10. Feature Timeline Summary

VersionYearKey FeaturesLTS?
82014Lambdas, Streams, Optional, Date-Time API
92017Modules (JPMS), JShell, Collection factories
102018var local variable type inference
112018HttpClient, var in lambdas, ZGC (exp.)
122019Switch expressions (preview)
132019Text blocks (preview)
142020Records (preview), switch expressions (standard)
152020Sealed classes (preview), text blocks (standard)
162021Records (standard), pattern matching instanceof
172021Sealed classes (standard), strong encapsulation
182022UTF-8 default, simple web server
192022Virtual threads (preview), structured concurrency (incubator)
202023Virtual threads (2nd preview), scoped values (incubator)
212023Virtual threads, pattern matching switch, record patterns (all standard)
Java 8  →  Java 17 (LTS)  →  Java 21 (LTS)
  • Java 8 → 17: Biggest jump. Gain modules, records, sealed classes, text blocks, new GCs.
  • Java 17 → 21: Gain virtual threads, pattern matching for switch, record patterns.