Java Programming Hub

Advanced Java development tutorials and guides

JavaModern FeaturesLambdaStreamsRecordsVirtual Threads

Modern Java Features: From Java 8 to Java 21 LTS Complete Guide

January 11, 202520 min readLanguage 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.