JavaModern FeaturesLambdaStreamsRecordsVirtual Threads
Modern Java Features: From Java 8 to Java 21 LTS Complete Guide
January 11, 2025•20 min read•Language Features
# Modern Java Features: From Java 8 to Java 21 LTS Complete Guide
Java has evolved significantly since Java 8, introducing numerous features that enhance developer productivity, performance, and code readability. This comprehensive guide covers the most important features from Java 8 through Java 21 LTS.
## Java 8: The Foundation of Modern Java
Java 8 introduced functional programming concepts that revolutionized how we write Java code.
### Lambda Expressions
Lambda expressions provide a concise way to represent functional interfaces:
```java
import java.util.*;
import java.util.function.*;
public class LambdaExamples {
public void basicLambdas() {
// Traditional anonymous class
Runnable oldWay = new Runnable() {
@Override
public void run() {
System.out.println("Old way");
}
};
// Lambda expression
Runnable newWay = () -> System.out.println("New way");
// Lambda with parameters
Comparator<String> comparator = (s1, s2) -> s1.compareToIgnoreCase(s2);
// Lambda with multiple statements
Consumer<String> processor = (String s) -> {
String processed = s.toUpperCase();
System.out.println("Processed: " + processed);
};
}
public void functionalInterfaces() {
// Predicate - boolean test
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isLong = s -> s.length() > 10;
Predicate<String> combined = isEmpty.or(isLong);
// Function - transformation
Function<String, Integer> stringLength = String::length;
Function<Integer, String> intToString = Object::toString;
Function<String, String> composed = stringLength.andThen(intToString);
// Supplier - factory
Supplier<List<String>> listSupplier = ArrayList::new;
// Consumer - side effects
Consumer<String> printer = System.out::println;
}
}
```
### Stream API
Streams provide a functional approach to processing collections:
```java
import java.util.stream.*;
public class StreamExamples {
public void basicStreamOperations() {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
// Filter and collect
List<String> longNames = names.stream()
.filter(name -> name.length() > 4)
.collect(Collectors.toList());
// Map and reduce
int totalLength = names.stream()
.mapToInt(String::length)
.sum();
// Find operations
Optional<String> firstLongName = names.stream()
.filter(name -> name.length() > 4)
.findFirst();
// Parallel processing
long count = names.parallelStream()
.filter(name -> name.startsWith("A"))
.count();
}
public void advancedStreamOperations() {
List<Person> people = Arrays.asList(
new Person("Alice", 30, "Engineering"),
new Person("Bob", 25, "Marketing"),
new Person("Charlie", 35, "Engineering"),
new Person("David", 28, "Sales")
);
// Grouping
Map<String, List<Person>> byDepartment = people.stream()
.collect(Collectors.groupingBy(Person::getDepartment));
// Partitioning
Map<Boolean, List<Person>> partitioned = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() > 30));
// Custom collector
String names = people.stream()
.map(Person::getName)
.collect(Collectors.joining(", ", "[", "]"));
// Flat mapping
List<String> allSkills = people.stream()
.flatMap(person -> person.getSkills().stream())
.distinct()
.collect(Collectors.toList());
}
}
```
### Optional
Optional helps eliminate null pointer exceptions:
```java
public class OptionalExamples {
public void basicOptional() {
Optional<String> optional = Optional.of("Hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(getString());
// Check if present
if (optional.isPresent()) {
System.out.println(optional.get());
}
// Functional approach
optional.ifPresent(System.out::println);
// Default values
String value = optional.orElse("Default");
String computed = optional.orElseGet(() -> computeDefault());
// Exception throwing
String required = optional.orElseThrow(() ->
new IllegalStateException("Value required"));
}
public void optionalChaining() {
Optional<Person> person = findPerson("Alice");
// Chaining operations
Optional<String> departmentName = person
.map(Person::getDepartment)
.map(Department::getName);
// Filtering
Optional<Person> engineer = person
.filter(p -> "Engineering".equals(p.getDepartment()));
// Flat mapping
Optional<String> managerName = person
.flatMap(Person::getManager)
.map(Person::getName);
}
}
```
## Java 9: Modularity and More
### Module System (Project Jigsaw)
```java
// module-info.java
module com.example.userservice {
requires java.base;
requires java.logging;
requires spring.core;
exports com.example.userservice.api;
exports com.example.userservice.model to spring.core;
provides com.example.userservice.spi.UserProvider
with com.example.userservice.impl.DatabaseUserProvider;
uses com.example.userservice.spi.NotificationService;
}
```
### Collection Factory Methods
```java
public class Java9Collections {
public void factoryMethods() {
// Immutable collections
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("x", "y", "z");
Map<String, Integer> map = Map.of(
"one", 1,
"two", 2,
"three", 3
);
// For larger maps
Map<String, Integer> largeMap = Map.ofEntries(
Map.entry("key1", 1),
Map.entry("key2", 2),
Map.entry("key3", 3)
);
}
}
```
### Enhanced Stream API
```java
public class Java9Streams {
public void newStreamMethods() {
// takeWhile - take elements while condition is true
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> lessThanFive = numbers.stream()
.takeWhile(n -> n < 5)
.collect(Collectors.toList()); // [1, 2, 3, 4]
// dropWhile - drop elements while condition is true
List<Integer> fiveAndAbove = numbers.stream()
.dropWhile(n -> n < 5)
.collect(Collectors.toList()); // [5, 6, 7, 8, 9, 10]
// iterate with condition
Stream.iterate(1, n -> n < 100, n -> n * 2)
.forEach(System.out::println); // 1, 2, 4, 8, 16, 32, 64
// ofNullable
Stream<String> stream = Stream.ofNullable(getString());
}
}
```
## Java 10-11: Local Variable Type Inference and HTTP Client
### Local Variable Type Inference (var)
```java
public class Java10Features {
public void varExamples() {
// Type inference for local variables
var message = "Hello World"; // String
var numbers = List.of(1, 2, 3, 4, 5); // List<Integer>
var map = Map.of("key", "value"); // Map<String, String>
// With complex types
var userService = new UserServiceImpl();
var result = userService.findUsersByDepartment("Engineering");
// In loops
for (var entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// With streams
var processedData = numbers.stream()
.filter(n -> n > 2)
.map(n -> n * 2)
.collect(Collectors.toList());
}
}
```
### HTTP Client (Java 11)
```java
import java.net.http.*;
import java.net.URI;
import java.time.Duration;
public class Java11HttpClient {
public void httpClientExamples() throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
// Synchronous GET request
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(30))
.build();
HttpResponse<String> response = client.send(getRequest,
HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
// Asynchronous POST request
String jsonBody = "{\"name\": \"John\", \"email\": \"john@example.com\"}";
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
CompletableFuture<HttpResponse<String>> futureResponse =
client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString());
futureResponse.thenAccept(resp -> {
System.out.println("Async response: " + resp.statusCode());
});
}
}
```
## Java 12-13: Switch Expressions and Text Blocks
### Switch Expressions (Preview in 12, Standard in 14)
```java
public class Java12SwitchExpressions {
public void traditionalSwitch(String day) {
String result;
switch (day) {
case "MONDAY":
case "TUESDAY":
case "WEDNESDAY":
case "THURSDAY":
case "FRIDAY":
result = "Weekday";
break;
case "SATURDAY":
case "SUNDAY":
result = "Weekend";
break;
default:
result = "Unknown";
break;
}
System.out.println(result);
}
public void modernSwitchExpression(String day) {
String result = switch (day) {
case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "Weekday";
case "SATURDAY", "SUNDAY" -> "Weekend";
default -> "Unknown";
};
System.out.println(result);
}
public void switchWithYield(int value) {
String result = switch (value) {
case 1, 2, 3 -> "Low";
case 4, 5, 6 -> "Medium";
case 7, 8, 9 -> "High";
default -> {
System.out.println("Processing special case: " + value);
yield "Special";
}
};
System.out.println(result);
}
}
```
### Text Blocks (Preview in 13, Standard in 15)
```java
public class Java13TextBlocks {
public void textBlockExamples() {
// Traditional string concatenation
String oldJson = "{
" +
" \"name\": \"John\",
" +
" \"age\": 30,
" +
" \"city\": \"New York\"
" +
"}";
// Text blocks
String newJson = """
{
"name": "John",
"age": 30,
"city": "New York"
}
""";
// SQL queries
String sql = """
SELECT u.name, u.email, d.name as department
FROM users u
JOIN departments d ON u.department_id = d.id
WHERE u.active = true
ORDER BY u.name
""";
// HTML templates
String html = """
<html>
<head>
<title>%s</title>
</head>
<body>
<h1>Welcome %s!</h1>
</body>
</html>
""".formatted("My App", "John");
}
}
```
## Java 14-16: Records and Pattern Matching
### Records (Preview in 14, Standard in 16)
```java
// Traditional class
public class TraditionalPerson {
private final String name;
private final int age;
public TraditionalPerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
TraditionalPerson person = (TraditionalPerson) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
// Record (much simpler)
public record Person(String name, int age) {
// Compact constructor for validation
public Person {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
// Additional methods
public boolean isAdult() {
return age >= 18;
}
// Static factory method
public static Person of(String name, int age) {
return new Person(name, age);
}
}
public class RecordExamples {
public void recordUsage() {
Person person = new Person("Alice", 30);
// Automatic getters
System.out.println(person.name()); // Alice
System.out.println(person.age()); // 30
// Automatic equals, hashCode, toString
Person another = new Person("Alice", 30);
System.out.println(person.equals(another)); // true
System.out.println(person.hashCode() == another.hashCode()); // true
System.out.println(person); // Person[name=Alice, age=30]
// Immutable
// person.name = "Bob"; // Compilation error
}
}
```
### Pattern Matching for instanceof (Preview in 14, Standard in 16)
```java
public class PatternMatchingExamples {
// Traditional instanceof
public void traditionalInstanceof(Object obj) {
if (obj instanceof String) {
String str = (String) obj;
System.out.println("String length: " + str.length());
}
}
// Pattern matching for instanceof
public void modernInstanceof(Object obj) {
if (obj instanceof String str) {
System.out.println("String length: " + str.length());
}
if (obj instanceof List<?> list && !list.isEmpty()) {
System.out.println("First element: " + list.get(0));
}
}
public String processValue(Object value) {
return switch (value) {
case String s -> "String: " + s.toUpperCase();
case Integer i -> "Integer: " + (i * 2);
case List<?> list -> "List with " + list.size() + " elements";
case null -> "Null value";
default -> "Unknown type: " + value.getClass().getSimpleName();
};
}
}
```
## Java 17-21: Sealed Classes and Virtual Threads
### Sealed Classes (Preview in 15, Standard in 17)
```java
// Sealed interface
public sealed interface Shape permits Circle, Rectangle, Triangle {
double area();
}
// Permitted implementations
public final class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public final class Rectangle implements Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public non-sealed class Triangle implements Shape {
private final double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
public class SealedClassExamples {
public void processShape(Shape shape) {
// Exhaustive pattern matching (no default needed)
String description = switch (shape) {
case Circle c -> "Circle with area " + c.area();
case Rectangle r -> "Rectangle with area " + r.area();
case Triangle t -> "Triangle with area " + t.area();
};
System.out.println(description);
}
}
```
### Virtual Threads (Preview in 19, Standard in 21)
```java
import java.util.concurrent.Executors;
public class VirtualThreadsExamples {
public void traditionalThreads() throws InterruptedException {
// Platform threads (expensive)
try (var executor = Executors.newFixedThreadPool(100)) {
for (int i = 0; i < 10000; i++) {
final int taskId = i;
executor.submit(() -> {
try {
Thread.sleep(1000); // Blocks platform thread
System.out.println("Task " + taskId + " completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
}
public void virtualThreads() throws InterruptedException {
// Virtual threads (lightweight)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
final int taskId = i;
executor.submit(() -> {
try {
Thread.sleep(1000); // Doesn't block carrier thread
System.out.println("Task " + taskId + " completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
}
public void virtualThreadBuilder() {
// Creating virtual threads directly
Thread virtualThread = Thread.ofVirtual()
.name("virtual-worker")
.start(() -> {
System.out.println("Running in virtual thread: " +
Thread.currentThread());
});
// Unstructured concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var task1 = scope.fork(() -> fetchUserData(1));
var task2 = scope.fork(() -> fetchUserData(2));
var task3 = scope.fork(() -> fetchUserData(3));
scope.join(); // Wait for all tasks
scope.throwIfFailed(); // Throw if any failed
// All tasks completed successfully
System.out.println("User 1: " + task1.get());
System.out.println("User 2: " + task2.get());
System.out.println("User 3: " + task3.get());
} catch (Exception e) {
e.printStackTrace();
}
}
private String fetchUserData(int userId) {
// Simulate I/O operation
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "User " + userId + " data";
}
}
```
## Modern Java Best Practices
### Combining Modern Features
```java
public class ModernJavaExample {
// Using records for data transfer
public record UserRequest(String name, String email, int age) {
public UserRequest {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name is required");
}
if (age < 0) {
throw new IllegalArgumentException("Age must be positive");
}
}
}
// Sealed interface for result types
public sealed interface Result<T> permits Success, Error {
record Success<T>(T value) implements Result<T> {}
record Error<T>(String message) implements Result<T> {}
}
public Result<String> processUser(UserRequest request) {
return switch (request.age()) {
case var age when age < 18 -> new Result.Error<>("User must be adult");
case var age when age > 120 -> new Result.Error<>("Invalid age");
default -> {
var processed = processUserData(request);
yield new Result.Success<>(processed);
}
};
}
private String processUserData(UserRequest request) {
var template = """
User Profile:
Name: %s
Email: %s
Age: %d
Status: %s
""";
var status = request.age() >= 65 ? "Senior" : "Regular";
return template.formatted(request.name(), request.email(),
request.age(), status);
}
// Using virtual threads for concurrent processing
public List<Result<String>> processUsers(List<UserRequest> requests) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = requests.stream()
.map(request -> executor.submit(() -> processUser(request)))
.toList();
return futures.stream()
.map(future -> {
try {
return future.get();
} catch (Exception e) {
return new Result.Error<String>("Processing failed: " +
e.getMessage());
}
})
.toList();
}
}
}
```
Modern Java features significantly improve code readability, maintainability, and performance. Adopting these features gradually while maintaining backward compatibility ensures smooth evolution of your codebase.