Java 5

Generics

Java 5's biggest addition was generics -- type-safe parameterized classes like List that eliminate manual casting. It reshaped the entire Collections Framework. See the dedicated Generics subcategory for the full breakdown (bounds, wildcards, PECS, erasure).
 
List names = new ArrayList();  // pre-Java 5: raw type, unsafe cast needed
String first = (String) names.get(0);

List<String> typed = new ArrayList<>();  // Java 5+: type-safe, no cast
String second = typed.get(0);

Enhanced for-each Loop

The for-each loop simplified iterating over arrays and any Iterable, replacing manual index tracking or explicit Iterator calls. It became the default choice for read-only traversal, cutting out a common source of off-by-one bugs.
 
List<String> fruits = List.of("Apple", "Banana", "Orange");

for (int i = 0; i < fruits.size(); i++) {  // before: manual index
    System.out.println(fruits.get(i));
}

for (String fruit : fruits) {  // for-each: no index, no iterator calls
    System.out.println(fruit);
}

Enums and Annotations

Java 5 replaced int-constant hacks with real enum types -- type-safe constants that can carry fields and methods. It also introduced annotations, letting tools and frameworks read structured metadata via @, like @Override catching signature typos at compile time.
 
enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    boolean isWeekend() { // enums can carry methods, unlike int constants
        return this == SATURDAY || this == SUNDAY;
    }
}

class Dog {
    // compiler now verifies this actually overrides a superclass method
    @Override
    public String toString() {
        return "Dog";
    }
}

Java 8

Lambda Expressions and Functional Interfaces

Java 8's headline feature: lambda expressions, a concise way to pass behavior as a value via x -> x * 2 syntax, backed by java.util.function interfaces like Function`/`Predicate. See the dedicated Lambdas subcategory for syntax, method references, and composition.
 
// pre-Java 8: anonymous class, lots of ceremony
Runnable oldStyle = new Runnable() {
    public void run() { System.out.println("run"); }
};

// same thing, one line
Runnable newStyle = () -> System.out.println("run"); 

Stream API

The Stream API brought declarative, pipeline-based processing -- filter`/`map`/`collect instead of manual loops and mutable accumulators. See the dedicated Streams subcategory for creation, collectors, primitive streams, and parallel streams in depth.
 
List<String> names = List.of("Alice", "Bob", "Charlie");

List<String> result = names.stream()
        .filter(n -> n.length() > 3)   // keep long names
        .map(String::toUpperCase)      // transform each one
        .collect(Collectors.toList()); // declarative, no manual loop

Optional

`Optional}} wraps a value that might be absent, making that possibility explicit in a method's return type instead of relying on null. It nudges API design toward handling the "no value" case via orElse`/`ifPresent, rather than scattered null checks. Best used as a return type, not a field or parameter.
 
Optional<String> found = findUserById(1);
System.out.println(found.orElse("Unknown")); // safe default if absent

found.ifPresent(name -> System.out.println("Found: " + name));

static Optional<String> findUserById(int id) {
    return id == 1 ? Optional.of("Alice") : Optional.empty();
}

Java 9

JShell (REPL)

JShell added an interactive REPL to the JDK -- evaluate statements and expressions one at a time with no class or main method needed. Great for testing snippets or exploring an unfamiliar API without full compilation ceremony.
 
// Instead of creating a project, class, and main() method:
// typed interactively at the jshell> prompt, not compiled as a file

// 1. Testing date/time logic
jshell> import java.time.*

jshell> var tradeDate = LocalDate.of(2026, 7, 28) 
tradeDate ==> 2026-07-28 

jshell> tradeDate.plusDays(2) 
$5 ==> 2026-07-30 

// 2. Learning a new API
jshell> import java.net.http.*

jshell> var client = HttpClient.newHttpClient()
client ==> jdk.internal.net.http.HttpClientImpl@7e0b0338(1)

jshell> client.  // Now press TAB
authenticator() awaitTermination() close() connectTimeout() ...

Collection Factory Methods

`List.of,}} Set.of, and Map.of create small immutable collections in one line, replacing the old Arrays.asList + Collections.unmodifiableList dance. They reject null elements and throw UnsupportedOperationException on any mutation attempt -- a fail-fast guard instead of a silent bug.
 
List<String> colors = List.of("Red", "Green", "Blue");
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25);

try {
    colors.add("Yellow"); // throws: list is immutable
} catch (UnsupportedOperationException e) {
    System.out.println(
        "Cannot modify: " + e.getClass().getSimpleName()
    );
}

Java 11

Local-Variable Type Inference (var)

`var}} lets the compiler infer a local variable's type from its initializer -- introduced in Java 10, extended to lambda parameters in Java 11. It's not dynamic typing; the type is fixed at compile time, just the syntax is shorter. Best when the type is already obvious from the right-hand side.
 
var names = new ArrayList<String>(); // inferred as ArrayList<String>
names.add("Alice");

for (var name : names) { // inferred as String
    System.out.println(name);
}

New HTTP Client

`java.net.http.HttpClient}} finally gave Java a modern built-in HTTP client -- HTTP/2 by default, connection reuse, sync and async requests -- replacing the clunky HttpURLConnection. Async calls return CompletableFuture, integrating naturally with the rest of Java's concurrency utilities.
 
HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/status"))
        .GET()
        .build();

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

String and Collection API Additions

Java 11 added everyday String convenience methods: isBlank, strip (Unicode-aware trim`), {{lines() (splits into a line stream), and repeat(). These closed gaps developers had been working around with manual loops or Apache Commons for years.
 
String text = "  Hello World  ";

System.out.println(text.strip());    // "Hello World"
System.out.println("   ".isBlank()); // true
System.out.println("ab".repeat(3));  // "ababab"

Java 17

Records

Records (finalized in 16) are a compact way to declare an immutable data carrier -- record Point(int x, int y) auto-generates the constructor, accessors, equals, hashCode, and toString. They're implicitly final and can't extend a class, but can implement interfaces and add extra methods.
 
record Point(int x, int y) {
    double distanceFromOrigin() { // custom method alongside the generated ones
        return Math.sqrt(x * x + y * y);
    }
}

Point p = new Point(3, 4);
System.out.println(p);    // Point[x=3, y=4] -- toString generated for free
System.out.println(p.x()); // 3 -- accessor generated for free

Pattern Matching for instanceof

Pattern matching for instanceof (finalized in 16) removes the redundant cast after a type check -- bind a variable directly: if (obj instanceof String s). The compiler only treats s as assigned in branches where the check is true, keeping it safe while cutting boilerplate.
 
Object obj = "Hello";

if (obj instanceof String) {          // before: check, then separate cast
    String s = (String) obj;
    System.out.println(s.length());
}

if (obj instanceof String s) {        // pattern matching: cast is implicit
    System.out.println(s.length());
}

Java 21

Virtual Threads

Virtual threads (finalized in 21) are lightweight, JVM-managed threads that let apps run millions of concurrent tasks without exhausting OS resources, unlocking thread-per-request code at scale. See the dedicated Concurrency subcategory for the full picture.
 
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> Thread.sleep(100)); // cheap even at massive scale
    }
}

Pattern Matching for switch

Pattern matching for switch (finalized in 21) extends switch to match type patterns with guard clauses (`when`), replacing long if-else-instanceof chains. The compiler checks exhaustiveness against sealed hierarchies, and case null is finally allowed inside the switch instead of needing a separate check.
 
static String describe(Object obj) {
    return switch (obj) {
        // null handled inside switch now
        case null -> "it's null";
        case Integer i when i > 0 -> "positive: " + i; // guard clause
        case Integer i -> "non-positive: " + i;
        case String s -> "string of length " + s.length();
        default -> "something else";
    };
}

Record Patterns

Record patterns (also finalized in 21) deconstruct a record's components directly in a pattern -- case Point(int x, int y) -- instead of matching then calling accessors separately. These nest, so you can reach into records within records, pairing naturally with sealed interfaces and switch.
 
record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}

static int area(Object obj) {
    return switch (obj) {
        // deconstructs Rectangle AND both nested Points in one pattern
        case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) ->
                Math.abs((x2 - x1) * (y2 - y1));
        default -> 0;
    };
}

Java 25

Compact Source Files and Instance Main Methods

Compact source files (finalized in 25) drop the ceremony new Java learners hit first: no public class, no public static void main(String[] args). A file can start with a plain void main(), and java runs a .java file directly with no separate compile step.
 
// HelloWorld.java -- a complete, valid Java 25 program

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

Scoped Values

Scoped values (finalized in 25) share immutable data across a thread and the methods it calls, without passing it as a parameter. Unlike ThreadLocal, a scoped value is bound only for one call and auto-unbinds when it returns -- no leaks, and safe to share across structured concurrency tasks.
 
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();

ScopedValue.where(CURRENT_USER, "alice").run(() -> {
    handleRequest(); // CURRENT_USER.get() is visible here, no parameter needed
});

static void handleRequest() {
    System.out.println("User: " + CURRENT_USER.get());
}

Module Import Declarations

Module import declarations (finalized in 25) let a file import every package a module exports with one line -- import module java.base -- instead of several individual package imports. It only imports what the module actually exports, so encapsulation is unaffected.
 
import module java.base; // pulls in java.util, java.util.stream, etc. at once

void main() {
    // no explicit java.util.List import
    List<String> names = List.of("Ann", "Bob");
    names.forEach(System.out::println);
}