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();
String first = (String) names.get(0);
List<String> typed = new ArrayList<>();
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++) {
System.out.println(fruits.get(i));
}
for (String fruit : fruits) {
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() {
return this == SATURDAY || this == SUNDAY;
}
}
class Dog {
@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.
Runnable oldStyle = new Runnable() {
public void run() { System.out.println("run"); }
};
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)
.map(String::toUpperCase)
.collect(Collectors.toList());
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"));
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.
jshell> import java.time.*
jshell> var tradeDate = LocalDate.of(2026, 7, 28)
tradeDate ==> 2026-07-28
jshell> tradeDate.plusDays(2)
$5 ==> 2026-07-30
jshell> import java.net.http.*
jshell> var client = HttpClient.newHttpClient()
client ==> jdk.internal.net.http.HttpClientImpl@7e0b0338(1)
jshell> client.
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");
} 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>();
names.add("Alice");
for (var name : names) {
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 =
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());
System.out.println(" ".isBlank());
System.out.println("ab".repeat(3));
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() {
return Math.sqrt(x * x + y * y);
}
}
Point p = new Point(3, 4);
System.out.println(p);
System.out.println(p.x());
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) {
String s = (String) obj;
System.out.println(s.length());
}
if (obj instanceof String s) {
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));
}
}
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) {
case null -> "it's null";
case Integer i when i > 0 -> "positive: " + i;
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) {
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.
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();
});
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;
void main() {
List<String> names = List.of("Ann", "Bob");
names.forEach(System.out::println);
}