Full Stack • Java • System Design • Cloud • AI Engineering

Java2026-06-08

Java 17 Features - Complete Guide

Comprehensive guide to Java 17 LTS features including Records, Sealed Classes, Pattern Matching, Text Blocks, Switch Expressions, and more with diagrams, data flows, and real-world examples.

Java 17 Features - Complete Guide

Java 17, released in September 2021, is a Long-Term Support (LTS) release that brings significant improvements and modern features to the Java ecosystem. It's widely adopted in enterprise applications and is the recommended version for production use.

Why Java 17 Matters

  • LTS Release: Long-term support until 2029
  • Modern Features: Records, sealed classes, pattern matching
  • Performance: Improved garbage collection and JIT compilation
  • Security: Enhanced security features and updates
  • Enterprise Ready: Stable and production-ready

Java Version Timeline

Java 8 (2014)  ─── LTS ───┐
                           │
Java 11 (2018) ─── LTS ───┤
                           ├─── Enterprise Adoption
Java 17 (2021) ─── LTS ───┤
                           │
Java 21 (2023) ─── LTS ───┘

Table of Contents

  1. Records
  2. Sealed Classes
  3. Pattern Matching
  4. Text Blocks
  5. Switch Expressions
  6. Helpful NullPointerExceptions
  7. Stream API Enhancements
  8. New Methods
  9. Real-World Examples

Records

Records are a special kind of class designed to hold immutable data. They provide a compact syntax for declaring data carrier classes.

Traditional Class vs Record

// Traditional Class (Verbose)
public class PersonOld {
    private final String name;
    private final int age;
    
    public PersonOld(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 o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonOld person = (PersonOld) o;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @Override
    public String toString() {
        return "PersonOld{name='" + name + "', age=" + age + "}";
    }
}

// Record (Concise) - Automatically generates:
// - Constructor
// - Getters (name(), age())
// - equals(), hashCode(), toString()
public record Person(String name, int age) {}

Record Features

// Basic record
public record User(String name, String email, int age) {}

// Usage
User user = new User("Venu", "[email protected]", 30);
System.out.println(user.name());   // Venu
System.out.println(user.email());  // [email protected]
System.out.println(user);          // User[name=Venu, [email protected], age=30]

// Records are immutable
// user.name = "John"; // Compilation error - no setters

// Equality based on values
User user2 = new User("Venu", "[email protected]", 30);
System.out.println(user.equals(user2)); // true

Custom Record Methods

public record Employee(String name, String department, double salary) {
    
    // Compact constructor - validation
    public Employee {
        if (salary < 0) {
            throw new IllegalArgumentException("Salary cannot be negative");
        }
        // Automatically assigns parameters to fields
    }
    
    // Custom methods
    public double annualSalary() {
        return salary * 12;
    }
    
    public boolean isHighEarner() {
        return salary > 100000;
    }
    
    // Static methods
    public static Employee createIntern(String name) {
        return new Employee(name, "Intern", 50000);
    }
}

// Usage
Employee emp = new Employee("Venu", "Engineering", 150000);
System.out.println("Annual: $" + emp.annualSalary());
System.out.println("High earner: " + emp.isHighEarner());

Employee intern = Employee.createIntern("John");

Record with Interfaces

public interface Identifiable {
    String getId();
}

public record Product(String id, String name, double price) 
    implements Identifiable {
    
    @Override
    public String getId() {
        return id;
    }
    
    public String getDisplayName() {
        return name + " ($" + price + ")";
    }
}

// Usage
Product product = new Product("P001", "Laptop", 1200.00);
System.out.println(product.getId());           // P001
System.out.println(product.getDisplayName());  // Laptop ($1200.0)

Nested Records

public record Address(String street, String city, String zipCode) {}

public record Customer(
    String name,
    String email,
    Address address
) {
    public String getFullAddress() {
        return address.street() + ", " + 
               address.city() + " " + 
               address.zipCode();
    }
}

// Usage
Address addr = new Address("123 Main St", "New York", "10001");
Customer customer = new Customer("Venu", "[email protected]", addr);
System.out.println(customer.getFullAddress());

Record Data Flow

Traditional Class:
┌─────────────────────────────────────┐
│ Write boilerplate code:             │
│ - Fields                             │
│ - Constructor                        │
│ - Getters                            │
│ - equals(), hashCode(), toString()   │
└─────────────────────────────────────┘
         │
         ▼
    ~50 lines

Record:
┌─────────────────────────────────────┐
│ public record Person(String name,   │
│                      int age) {}    │
└─────────────────────────────────────┘
         │
         ▼
     1 line

Sealed Classes

Sealed classes restrict which classes can extend or implement them, providing better control over inheritance hierarchies.

Sealed Class Syntax

// Sealed class - explicitly permits subclasses
public sealed class Shape 
    permits Circle, Rectangle, Triangle {
    
    private final String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    public String getColor() {
        return color;
    }
    
    public abstract double area();
}

// Permitted subclasses must be:
// - final (cannot be extended further)
// - sealed (permits specific subclasses)
// - non-sealed (open for extension)

public final class Circle extends Shape {
    private final double radius;
    
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

public final class Rectangle extends Shape {
    private final double width;
    private final double height;
    
    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double area() {
        return width * height;
    }
}

public non-sealed class Triangle extends Shape {
    private final double base;
    private final double height;
    
    public Triangle(String color, double base, double height) {
        super(color);
        this.base = base;
        this.height = height;
    }
    
    @Override
    public double area() {
        return 0.5 * base * height;
    }
}

// Triangle is non-sealed, so it can be extended
public class EquilateralTriangle extends Triangle {
    public EquilateralTriangle(String color, double side) {
        super(color, side, side * Math.sqrt(3) / 2);
    }
}

Sealed Interface

public sealed interface Payment 
    permits CreditCardPayment, DebitCardPayment, CashPayment {
    
    double getAmount();
    String getPaymentMethod();
}

public final class CreditCardPayment implements Payment {
    private final double amount;
    private final String cardNumber;
    
    public CreditCardPayment(double amount, String cardNumber) {
        this.amount = amount;
        this.cardNumber = cardNumber;
    }
    
    @Override
    public double getAmount() { return amount; }
    
    @Override
    public String getPaymentMethod() { return "Credit Card"; }
}

public final class DebitCardPayment implements Payment {
    private final double amount;
    private final String cardNumber;
    
    public DebitCardPayment(double amount, String cardNumber) {
        this.amount = amount;
        this.cardNumber = cardNumber;
    }
    
    @Override
    public double getAmount() { return amount; }
    
    @Override
    public String getPaymentMethod() { return "Debit Card"; }
}

public final class CashPayment implements Payment {
    private final double amount;
    
    public CashPayment(double amount) {
        this.amount = amount;
    }
    
    @Override
    public double getAmount() { return amount; }
    
    @Override
    public String getPaymentMethod() { return "Cash"; }
}

Sealed Class Hierarchy

┌─────────────────┐
│ Sealed Shape    │ (Permits: Circle, Rectangle, Triangle)
└────────┬────────┘
         │
    ┌────┴────┬────────────┬──────────┐
    │         │            │          │
    ▼         ▼            ▼          ▼
┌────────┐ ┌──────────┐ ┌─────────┐
│ Circle │ │Rectangle │ │Triangle │ (non-sealed)
│(final) │ │ (final)  │ │         │
└────────┘ └──────────┘ └────┬────┘
                              │
                              ▼
                    ┌──────────────────┐
                    │EquilateralTriangle│
                    └──────────────────┘

Benefits of Sealed Classes

// Exhaustive pattern matching (covered in next section)
public String describeShape(Shape shape) {
    return switch (shape) {
        case Circle c -> "Circle with radius " + c.getRadius();
        case Rectangle r -> "Rectangle " + r.getWidth() + "x" + r.getHeight();
        case Triangle t -> "Triangle with base " + t.getBase();
        // No default needed - compiler knows all possibilities
    };
}

Pattern Matching

Pattern matching simplifies type checking and casting.

Pattern Matching for instanceof

// Before Java 17 (Verbose)
public void processObjectOld(Object obj) {
    if (obj instanceof String) {
        String str = (String) obj;  // Explicit cast
        System.out.println("String length: " + str.length());
    } else if (obj instanceof Integer) {
        Integer num = (Integer) obj;  // Explicit cast
        System.out.println("Number: " + num);
    }
}

// Java 17 (Pattern Matching)
public void processObject(Object obj) {
    if (obj instanceof String str) {  // Pattern variable
        System.out.println("String length: " + str.length());
    } else if (obj instanceof Integer num) {  // Pattern variable
        System.out.println("Number: " + num);
    }
}

// With additional conditions
public void processWithCondition(Object obj) {
    if (obj instanceof String str && str.length() > 5) {
        System.out.println("Long string: " + str);
    }
}

Pattern Matching in Switch (Preview in 17)

// Enhanced switch with pattern matching
public String formatValue(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l -> String.format("long %d", l);
        case Double d -> String.format("double %f", d);
        case String s -> String.format("String %s", s);
        case null -> "null value";
        default -> obj.toString();
    };
}

// With guards (conditions)
public String classifyNumber(Object obj) {
    return switch (obj) {
        case Integer i when i > 0 -> "Positive integer";
        case Integer i when i < 0 -> "Negative integer";
        case Integer i -> "Zero";
        case Double d when d > 0.0 -> "Positive double";
        case Double d when d < 0.0 -> "Negative double";
        case Double d -> "Zero double";
        default -> "Not a number";
    };
}

Pattern Matching with Records

public record Point(int x, int y) {}

public String describePoint(Object obj) {
    return switch (obj) {
        case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
        case Point(int x, int y) when x == 0 -> "On Y-axis";
        case Point(int x, int y) when y == 0 -> "On X-axis";
        case Point(int x, int y) -> "Point at (" + x + ", " + y + ")";
        default -> "Not a point";
    };
}

// Usage
System.out.println(describePoint(new Point(0, 0)));  // Origin
System.out.println(describePoint(new Point(0, 5)));  // On Y-axis
System.out.println(describePoint(new Point(3, 4)));  // Point at (3, 4)

Pattern Matching Flow

Before Pattern Matching:
┌──────────────┐
│ instanceof   │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ Explicit Cast│
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  Use Variable│
└──────────────┘

With Pattern Matching:
┌──────────────────────┐
│ instanceof Type var  │
└──────────┬───────────┘
           │
           ▼
    ┌──────────────┐
    │ Use var      │
    │ (auto-cast)  │
    └──────────────┘

Text Blocks

Text blocks provide a cleaner way to write multi-line strings.

Text Block Syntax

// Before Java 17 (Concatenation)
String jsonOld = "{\n" +
                 "  \"name\": \"Venu\",\n" +
                 "  \"age\": 30,\n" +
                 "  \"email\": \"[email protected]\"\n" +
                 "}";

// Java 17 (Text Block)
String json = """
    {
      "name": "Venu",
      "age": 30,
      "email": "[email protected]"
    }
    """;

System.out.println(json);

Text Block Examples

public class TextBlockExamples {
    
    // SQL Query
    public static String getQuery() {
        return """
            SELECT e.name, e.salary, d.department_name
            FROM employees e
            INNER JOIN departments d ON e.dept_id = d.id
            WHERE e.salary > 100000
            ORDER BY e.salary DESC
            """;
    }
    
    // HTML
    public static String getHtml() {
        return """
            <!DOCTYPE html>
            <html>
            <head>
                <title>Welcome</title>
            </head>
            <body>
                <h1>Hello, World!</h1>
                <p>This is a text block example.</p>
            </body>
            </html>
            """;
    }
    
    // JSON
    public static String getJson(String name, int age) {
        return """
            {
              "name": "%s",
              "age": %d,
              "active": true
            }
            """.formatted(name, age);
    }
    
    // With escape sequences
    public static String getEscaped() {
        return """
            Line 1
            Line 2 with "quotes"
            Line 3 with \\ backslash
            """;
    }
    
    public static void main(String[] args) {
        System.out.println(getQuery());
        System.out.println(getJson("Venu", 30));
    }
}

Text Block Features

// Incidental whitespace is removed
String text1 = """
    Hello
    World
    """;  // No leading spaces in output

// Preserve trailing whitespace with \s
String text2 = """
    Line 1\s
    Line 2\s
    """;

// Line continuation with \
String text3 = """
    This is a very long line \
    that continues here
    """;  // Single line in output

// Interpolation with formatted()
String name = "Venu";
int age = 30;
String message = """
    Name: %s
    Age: %d
    """.formatted(name, age);

Switch Expressions

Switch expressions (introduced in Java 14, stable in 17) provide a more concise and safer way to write switch statements.

Traditional Switch vs Switch Expression

// Traditional switch statement
public String getDayTypeOld(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 = "Invalid day";
    }
    return result;
}

// Switch expression
public String getDayType(String day) {
    return switch (day) {
        case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" 
            -> "Weekday";
        case "SATURDAY", "SUNDAY" 
            -> "Weekend";
        default 
            -> "Invalid day";
    };
}

Switch Expression Examples

public class SwitchExpressionExamples {
    
    // Simple switch expression
    public static int getQuarter(int month) {
        return switch (month) {
            case 1, 2, 3 -> 1;
            case 4, 5, 6 -> 2;
            case 7, 8, 9 -> 3;
            case 10, 11, 12 -> 4;
            default -> throw new IllegalArgumentException("Invalid month");
        };
    }
    
    // With yield for complex logic
    public static String getGrade(int score) {
        return switch (score / 10) {
            case 10, 9 -> "A";
            case 8 -> "B";
            case 7 -> "C";
            case 6 -> "D";
            default -> {
                if (score < 0 || score > 100) {
                    yield "Invalid score";
                }
                yield "F";
            }
        };
    }
    
    // Enum switch
    enum Status { PENDING, APPROVED, REJECTED }
    
    public static String getStatusMessage(Status status) {
        return switch (status) {
            case PENDING -> "Waiting for approval";
            case APPROVED -> "Request approved";
            case REJECTED -> "Request rejected";
        };
    }
    
    // Type pattern matching in switch
    public static String formatObject(Object obj) {
        return switch (obj) {
            case Integer i -> "Integer: " + i;
            case String s -> "String: " + s;
            case Double d -> "Double: " + d;
            case null -> "null";
            default -> "Unknown type";
        };
    }
    
    public static void main(String[] args) {
        System.out.println("Q1: " + getQuarter(2));
        System.out.println("Grade: " + getGrade(85));
        System.out.println("Status: " + getStatusMessage(Status.APPROVED));
        System.out.println(formatObject(42));
    }
}

Switch Expression Flow

Traditional Switch:
┌─────────────┐
│   switch    │
└──────┬──────┘
       │
   ┌───┴───┬───────┬───────┐
   │       │       │       │
   ▼       ▼       ▼       ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│case1│ │case2│ │case3│ │deflt│
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
   │       │       │       │
   ▼       ▼       ▼       ▼
 break   break   break   break

Switch Expression:
┌─────────────┐
│   switch    │
└──────┬──────┘
       │
   ┌───┴───┬───────┬───────┐
   │       │       │       │
   ▼       ▼       ▼       ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│case1│ │case2│ │case3│ │deflt│
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
   └───────┴───────┴───────┘
              │
              ▼
         ┌─────────┐
         │ Result  │
         └─────────┘

Helpful NullPointerExceptions

Java 17 provides more detailed NullPointerException messages.

Enhanced NPE Messages

public class NullPointerExample {
    
    static class Person {
        String name;
        Address address;
    }
    
    static class Address {
        String street;
        City city;
    }
    
    static class City {
        String name;
    }
    
    public static void main(String[] args) {
        Person person = new Person();
        person.address = new Address();
        // person.address.city is null
        
        try {
            String cityName = person.address.city.name;
        } catch (NullPointerException e) {
            // Before Java 17:
            // NullPointerException
            
            // Java 17:
            // Cannot read field "name" because "person.address.city" is null
            System.out.println(e.getMessage());
        }
    }
}

Stream API Enhancements

Java 17 includes Stream API improvements from Java 9-16.

New Stream Methods

import java.util.*;
import java.util.stream.*;

public class StreamEnhancements {
    public static void main(String[] args) {
        
        // 1. takeWhile() - Take elements while condition is true
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> taken = numbers.stream()
            .takeWhile(n -> n < 5)
            .collect(Collectors.toList());
        System.out.println("takeWhile: " + taken); // [1, 2, 3, 4]
        
        // 2. dropWhile() - Drop elements while condition is true
        List<Integer> dropped = numbers.stream()
            .dropWhile(n -> n < 5)
            .collect(Collectors.toList());
        System.out.println("dropWhile: " + dropped); // [5, 6, 7, 8, 9, 10]
        
        // 3. iterate() with predicate
        List<Integer> generated = Stream.iterate(0, n -> n < 10, n -> n + 2)
            .collect(Collectors.toList());
        System.out.println("iterate: " + generated); // [0, 2, 4, 6, 8]
        
        // 4. ofNullable() - Create stream from nullable value
        String value = null;
        long count = Stream.ofNullable(value).count();
        System.out.println("ofNullable count: " + count); // 0
        
        value = "Hello";
        Stream.ofNullable(value).forEach(System.out::println); // Hello
        
        // 5. toList() - Collect to unmodifiable list (Java 16+)
        List<String> names = Stream.of("Venu", "John", "Alice")
            .toList();  // Simpler than .collect(Collectors.toList())
        System.out.println("toList: " + names);
    }
}

New Methods

String Methods

public class StringMethods {
    public static void main(String[] args) {
        String text = "  Hello World  ";
        
        // isBlank() - Check if string is empty or whitespace
        System.out.println("   ".isBlank());  // true
        System.out.println("Hello".isBlank()); // false
        
        // strip() - Remove leading/trailing whitespace (Unicode-aware)
        System.out.println(text.strip());  // "Hello World"
        System.out.println(text.stripLeading());  // "Hello World  "
        System.out.println(text.stripTrailing()); // "  Hello World"
        
        // lines() - Stream of lines
        String multiline = "Line 1\nLine 2\nLine 3";
        multiline.lines().forEach(System.out::println);
        
        // repeat() - Repeat string
        System.out.println("Ha".repeat(3));  // "HaHaHa"
        
        // indent() - Add indentation
        String code = "public class Test {\n}";
        System.out.println(code.indent(4));
        
        // transform() - Apply function
        String result = "hello".transform(String::toUpperCase);
        System.out.println(result);  // "HELLO"
    }
}

Collection Factory Methods

import java.util.*;

public class CollectionFactories {
    public static void main(String[] args) {
        
        // Immutable List
        List<String> list = List.of("A", "B", "C");
        // list.add("D"); // UnsupportedOperationException
        
        // Immutable Set
        Set<Integer> set = Set.of(1, 2, 3, 4, 5);
        
        // Immutable Map
        Map<String, Integer> map = Map.of(
            "one", 1,
            "two", 2,
            "three", 3
        );
        
        // Map with entries (for more than 10 entries)
        Map<String, Integer> largeMap = Map.ofEntries(
            Map.entry("one", 1),
            Map.entry("two", 2),
            Map.entry("three", 3)
        );
        
        System.out.println("List: " + list);
        System.out.println("Set: " + set);
        System.out.println("Map: " + map);
    }
}

Real-World Examples

Example 1: REST API Response with Records

import java.time.LocalDateTime;
import java.util.*;

// API Response models using records
public record ApiResponse<T>(
    boolean success,
    String message,
    T data,
    LocalDateTime timestamp
) {
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(true, "Success", data, LocalDateTime.now());
    }
    
    public static <T> ApiResponse<T> error(String message) {
        return new ApiResponse<>(false, message, null, LocalDateTime.now());
    }
}

public record UserDTO(
    Long id,
    String name,
    String email,
    String role
) {}

public class ApiExample {
    public static void main(String[] args) {
        // Success response
        UserDTO user = new UserDTO(1L, "Venu", "[email protected]", "ADMIN");
        ApiResponse<UserDTO> response = ApiResponse.success(user);
        
        System.out.println("Success: " + response.success());
        System.out.println("User: " + response.data().name());
        
        // Error response
        ApiResponse<UserDTO> errorResponse = ApiResponse.error("User not found");
        System.out.println("Error: " + errorResponse.message());
    }
}

Example 2: Payment Processing with Sealed Classes

public sealed interface PaymentMethod 
    permits CreditCard, DebitCard, PayPal, BankTransfer {
    double processPayment(double amount);
}

public record CreditCard(String cardNumber, String cvv, String expiryDate) 
    implements PaymentMethod {
    
    @Override
    public double processPayment(double amount) {
        // Add 2.5% processing fee for credit cards
        double fee = amount * 0.025;
        System.out.println("Processing credit card payment: $" + amount);
        System.out.println("Fee: $" + fee);
        return amount + fee;
    }
}

public record DebitCard(String cardNumber, String pin) 
    implements PaymentMethod {
    
    @Override
    public double processPayment(double amount) {
        // Add 1% processing fee for debit cards
        double fee = amount * 0.01;
        System.out.println("Processing debit card payment: $" + amount);
        System.out.println("Fee: $" + fee);
        return amount + fee;
    }
}

public record PayPal(String email) 
    implements PaymentMethod {
    
    @Override
    public double processPayment(double amount) {
        // Add 3% processing fee for PayPal
        double fee = amount * 0.03;
        System.out.println("Processing PayPal payment: $" + amount);
        System.out.println("Fee: $" + fee);
        return amount + fee;
    }
}

public record BankTransfer(String accountNumber, String routingNumber) 
    implements PaymentMethod {
    
    @Override
    public double processPayment(double amount) {
        // No fee for bank transfers
        System.out.println("Processing bank transfer: $" + amount);
        return amount;
    }
}

public class PaymentProcessor {
    
    public static String getPaymentDescription(PaymentMethod payment) {
        return switch (payment) {
            case CreditCard cc -> "Credit Card ending in " + 
                cc.cardNumber().substring(cc.cardNumber().length() - 4);
            case DebitCard dc -> "Debit Card ending in " + 
                dc.cardNumber().substring(dc.cardNumber().length() - 4);
            case PayPal pp -> "PayPal account: " + pp.email();
            case BankTransfer bt -> "Bank Transfer from account: " + bt.accountNumber();
        };
    }
    
    public static void main(String[] args) {
        List<PaymentMethod> payments = List.of(
            new CreditCard("1234567890123456", "123", "12/25"),
            new DebitCard("9876543210987654", "1234"),
            new PayPal("[email protected]"),
            new BankTransfer("123456789", "987654321")
        );
        
        double orderAmount = 100.00;
        
        for (PaymentMethod payment : payments) {
            System.out.println("\n" + getPaymentDescription(payment));
            double total = payment.processPayment(orderAmount);
            System.out.println("Total: $" + total);
        }
    }
}

Example 3: Data Processing Pipeline

import java.util.*;
import java.util.stream.*;

public record Transaction(
    String id,
    String customerId,
    double amount,
    String category,
    LocalDateTime timestamp
) {}

public class TransactionAnalyzer {
    
    public static void main(String[] args) {
        List<Transaction> transactions = List.of(
            new Transaction("T1", "C1", 100.0, "FOOD", LocalDateTime.now()),
            new Transaction("T2", "C2", 250.0, "ELECTRONICS", LocalDateTime.now()),
            new Transaction("T3", "C1", 50.0, "FOOD", LocalDateTime.now()),
            new Transaction("T4", "C3", 500.0, "ELECTRONICS", LocalDateTime.now()),
            new Transaction("T5", "C2", 75.0, "CLOTHING", LocalDateTime.now())
        );
        
        // Group by category and sum amounts
        Map<String, Double> categoryTotals = transactions.stream()
            .collect(Collectors.groupingBy(
                Transaction::category,
                Collectors.summingDouble(Transaction::amount)
            ));
        
        System.out.println("Category Totals:");
        categoryTotals.forEach((category, total) -> 
            System.out.println(category + ": $" + total));
        
        // Find high-value transactions
        List<Transaction> highValue = transactions.stream()
            .filter(t -> t.amount() > 200)
            .toList();
        
        System.out.println("\nHigh-value transactions:");
        highValue.forEach(t -> 
            System.out.println(t.id() + ": $" + t.amount()));
        
        // Customer spending
        Map<String, Double> customerSpending = transactions.stream()
            .collect(Collectors.groupingBy(
                Transaction::customerId,
                Collectors.summingDouble(Transaction::amount)
            ));
        
        System.out.println("\nCustomer Spending:");
        customerSpending.forEach((customer, total) -> 
            System.out.println(customer + ": $" + total));
    }
}

Migration from Java 8 to Java 17

Key Changes

// 1. Replace data classes with records
// Before
public class User {
    private final String name;
    private final String email;
    // constructor, getters, equals, hashCode, toString
}

// After
public record User(String name, String email) {}

// 2. Use pattern matching
// Before
if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.length());
}

// After
if (obj instanceof String str) {
    System.out.println(str.length());
}

// 3. Use text blocks for multi-line strings
// Before
String sql = "SELECT * FROM users\n" +
             "WHERE age > 18\n" +
             "ORDER BY name";

// After
String sql = """
    SELECT * FROM users
    WHERE age > 18
    ORDER BY name
    """;

// 4. Use switch expressions
// Before
String result;
switch (day) {
    case "MON": result = "Monday"; break;
    case "TUE": result = "Tuesday"; break;
    default: result = "Other";
}

// After
String result = switch (day) {
    case "MON" -> "Monday";
    case "TUE" -> "Tuesday";
    default -> "Other";
};

// 5. Use sealed classes for restricted hierarchies
public sealed class Result<T>
    permits Success, Error {
}

public final class Success<T> extends Result<T> {
    private final T value;
    // ...
}

public final class Error<T> extends Result<T> {
    private final String message;
    // ...
}

Best Practices

1. Use Records for DTOs

// ✅ Good - Immutable data transfer object
public record UserDTO(Long id, String name, String email) {}

// ❌ Avoid - Mutable fields in records
// Records should be immutable

2. Leverage Sealed Classes for Domain Modeling

// ✅ Good - Explicit type hierarchy
public sealed interface OrderStatus
    permits Pending, Confirmed, Shipped, Delivered, Cancelled {}

// Enables exhaustive pattern matching
public String getStatusMessage(OrderStatus status) {
    return switch (status) {
        case Pending p -> "Order is pending";
        case Confirmed c -> "Order confirmed";
        case Shipped s -> "Order shipped";
        case Delivered d -> "Order delivered";
        case Cancelled c -> "Order cancelled";
        // No default needed - compiler ensures all cases covered
    };
}

3. Use Text Blocks for Readability

// ✅ Good - Readable SQL
String query = """
    SELECT u.name, u.email, o.total
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE o.status = 'COMPLETED'
    """;

// ❌ Avoid - Hard to read
String query = "SELECT u.name, u.email, o.total " +
               "FROM users u " +
               "JOIN orders o ON u.id = o.user_id " +
               "WHERE o.status = 'COMPLETED'";

4. Pattern Matching for Cleaner Code

// ✅ Good - Pattern matching
public double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t -> 0.5 * t.base() * t.height();
    };
}

// ❌ Avoid - Traditional instanceof
public double calculateAreaOld(Shape shape) {
    if (shape instanceof Circle) {
        Circle c = (Circle) shape;
        return Math.PI * c.radius() * c.radius();
    } else if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle) shape;
        return r.width() * r.height();
    }
    // ...
}

Performance Improvements

Java 17 includes several performance enhancements:

1. Garbage Collection

  • G1GC improvements: Better pause time predictability
  • ZGC: Low-latency garbage collector
  • Shenandoah: Concurrent GC with minimal pause times

2. JIT Compilation

  • Improved C2 compiler optimizations
  • Better inlining decisions
  • Enhanced escape analysis

3. Startup Time

  • Faster class loading
  • Improved JVM startup
  • Better AOT compilation support

Summary

Java 17 LTS brings modern features to enterprise Java:

Records - Concise immutable data classes ✅ Sealed Classes - Controlled inheritance hierarchies ✅ Pattern Matching - Cleaner type checking and casting ✅ Text Blocks - Readable multi-line strings ✅ Switch Expressions - More powerful and safer switches ✅ Enhanced NPEs - Better debugging information ✅ Stream Enhancements - More powerful data processing ✅ New Methods - Improved String and Collection APIs

Key Benefits

  • Productivity: Less boilerplate code
  • Safety: Better type safety and null handling
  • Readability: More expressive syntax
  • Performance: Improved runtime performance
  • Maintainability: Cleaner, more maintainable code

Adoption Strategy

  1. Start with Records: Replace DTOs and value objects
  2. Use Text Blocks: For SQL, JSON, HTML, etc.
  3. Apply Pattern Matching: Simplify instanceof checks
  4. Leverage Sealed Classes: For domain modeling
  5. Adopt Switch Expressions: Replace traditional switches

Remember: Java 17 is the recommended LTS version for production applications. Its modern features make Java development more enjoyable and productive! 🚀