Java 14 – Records i Pattern Matching dla Początkujących

TL;DR: Java 14 wprowadza Records – kompaktowy sposób tworzenia klas przechowujących dane oraz Pattern Matching dla instanceof – eleganckie sprawdzanie typów bez castowania. Te funkcje drastycznie upraszczają kod i eliminują boilerplate.

Czy masz dość pisania dziesiątek linii kodu tylko po to, żeby stworzyć prostą klasę przechowującą dane? Java 14 wprowadza rewolucyjne zmiany, które sprawią, że Twój kod stanie się krótszy, czytelniejszy i mniej podatny na błędy.

Dlaczego Java 14 Records i Pattern Matching są ważne

W codziennej pracy programisty często tworzymy klasy które służą tylko do przechowywania danych – tzw. „data classes” lub „value objects”. Tradycyjnie wymagało to pisania konstruktorów, getterów, equals(), hashCode() i toString(). Java 14 eliminuje ten problem wprowadzając Records – automatycznie generowane klasy danych.

Co się nauczysz:

  • Jak tworzyć i używać Records w Java 14
  • Różnice między Records a tradycyjnymi klasami
  • Pattern Matching dla instanceof – nowoczesne sprawdzanie typów
  • Praktyczne zastosowania w prawdziwych projektach
  • Kiedy używać Records, a kiedy pozostać przy klasycznych klasach
Wymagania wstępne: Podstawowa znajomość Java (klasy, obiekty, dziedziczenie), doświadczenie z tworzeniem getterów i setterów, rozumienie koncepcji equals() i hashCode().

Czym są Records w Java 14

Records to nowy typ klasy w Javie, który automatycznie generuje całą infrastrukturę potrzebną do przechowywania danych. Zamiast pisać dziesiątki linii kodu, wystarczy jedna deklaracja.

Record – specjalny typ klasy w Java 14, który automatycznie implementuje konstruktor, gettery, equals(), hashCode() i toString() na podstawie zdefiniowanych pól.

Tradycyjny sposób vs Records

**Tradycyjna klasa (przed Java 14):**

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;
    
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public int getAge() {
        return age;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age &&
               Objects.equals(firstName, person.firstName) &&
               Objects.equals(lastName, person.lastName);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName, age);
    }
    
    @Override
    public String toString() {
        return "Person{" +
               "firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               ", age=" + age +
               '}';
    }
}
// 45+ linii kodu!

**Ta sama funkcjonalność z Records:**

public record Person(String firstName, String lastName, int age) {
    // To wszystko! Java automatycznie wygeneruje:
    // - konstruktor
    // - gettery (firstName(), lastName(), age())
    // - equals() i hashCode()
    // - toString()
}
// Tylko 1 linia kodu!
Pro tip: Records domyślnie tworzą final pola, co oznacza że obiekty są niemutowalne (immutable). To świetna praktyka bezpieczeństwa w programowaniu.

Używanie Records w praktyce

public class RecordExample {
    public static void main(String[] args) {
        // Tworzenie obiektu Record
        Person person = new Person("Jan", "Kowalski", 30);
        
        // Automatyczne gettery (bez "get" prefix!)
        System.out.println("Imię: " + person.firstName());
        System.out.println("Nazwisko: " + person.lastName());
        System.out.println("Wiek: " + person.age());
        
        // Automatyczny toString()
        System.out.println(person);
        // Output: Person[firstName=Jan, lastName=Kowalski, age=30]
        
        // Automatyczny equals()
        Person person2 = new Person("Jan", "Kowalski", 30);
        System.out.println(person.equals(person2)); // true
        
        // Użycie w kolekcjach
        Set people = Set.of(person, person2);
        System.out.println(people.size()); // 1 (bo są równe!)
    }
}

Records z dodatkowymi metodami

Records mogą mieć również własne metody i walidację:

public record Employee(String name, String department, int salary) {
    
    // Compact constructor - walidacja parametrów  
    public Employee {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        if (salary < 0) {
            throw new IllegalArgumentException("Salary cannot be negative");
        }
        // Automatyczne przypisanie do pól
    }
    
    // Dodatkowe metody
    public boolean isHighEarner() {
        return salary > 100000;
    }
    
    public String getFormattedSalary() {
        return String.format("$%,d", salary);
    }
    
    public Employee withRaise(int raise) {
        return new Employee(name, department, salary + raise);
    }
}
Compact constructor to skrócona forma konstruktora w Records. Nie musisz wypisywać wszystkich parametrów – Java automatycznie przypiszę je do odpowiednich pól po wykonaniu Twojej logiki walidacyjnej.

Pattern Matching dla instanceof

Druga ważna nowość w Java 14 to Pattern Matching dla instanceof. Eliminuje potrzebę ręcznego castowania po sprawdzeniu typu.

Tradycyjny sposób

// Stary sposób - przed Java 14
public String processShape(Object shape) {
    if (shape instanceof Rectangle) {
        Rectangle rect = (Rectangle) shape; // Ręczne castowanie
        return "Rectangle with area: " + (rect.getWidth() * rect.getHeight());
    } else if (shape instanceof Circle) {
        Circle circle = (Circle) shape; // Znowu castowanie
        return "Circle with area: " + (Math.PI * circle.getRadius() * circle.getRadius());
    }
    return "Unknown shape";
}

Z Pattern Matching (Java 14)

// Nowy sposób - Java 14 Pattern Matching
public String processShape(Object shape) {
    if (shape instanceof Rectangle rect) {
        // 'rect' jest automatycznie dostępne jako Rectangle!
        return "Rectangle with area: " + (rect.getWidth() * rect.getHeight());
    } else if (shape instanceof Circle circle) {
        // 'circle' jest automatycznie dostępne jako Circle!  
        return "Circle with area: " + (Math.PI * circle.getRadius() * circle.getRadius());
    }
    return "Unknown shape";
}
Pattern Matching to jak inteligentny asystent – gdy sprawdzasz „czy to jest Rectangle”, automatycznie przygotowuje Ci zmienną typu Rectangle do użycia, eliminując nudne i podatne na błędy castowanie.

Praktyczne zastosowania Pattern Matching

public class JsonProcessor {
    
    public void processValue(Object value) {
        if (value instanceof String str && !str.isEmpty()) {
            System.out.println("Processing non-empty string: " + str.toUpperCase());
        } else if (value instanceof Integer num && num > 0) {
            System.out.println("Processing positive number: " + num * 2);
        } else if (value instanceof List list && !list.isEmpty()) {
            System.out.println("Processing list with " + list.size() + " elements");
        }
    }
    
    // Można łączyć z warunkami dodatkowymi!
    public String formatData(Object data) {
        return switch (data) {
            case null -> "No data";
            case String s when s.length() > 10 -> "Long string: " + s.substring(0, 10) + "...";
            case String s -> "Short string: " + s;
            case Integer i when i < 0 -> "Negative: " + Math.abs(i);
            case Integer i -> "Positive: " + i;
            default -> "Unknown type: " + data.getClass().getSimpleName();
        };
    }
}
Uwaga: Switch expressions z pattern matching to jeszcze preview feature w Java 14. Pełna implementacja pojawi się w późniejszych wersjach. Na razie używaj głównie z if-else.

Łączenie Records z Pattern Matching

Records i Pattern Matching świetnie współpracują ze sobą:

// Definicja Records dla różnych typów zdarzeń
public record UserLoginEvent(String username, LocalDateTime timestamp) {}
public record UserLogoutEvent(String username, LocalDateTime timestamp) {}
public record OrderPlacedEvent(String orderId, BigDecimal amount, String customerId) {}

public class EventProcessor {
    
    public void handleEvent(Object event) {
        if (event instanceof UserLoginEvent login) {
            System.out.println("User " + login.username() + 
                             " logged in at " + login.timestamp());
            // Automatycznie dostępne pola z Record!
            
        } else if (event instanceof UserLogoutEvent logout) {
            System.out.println("User " + logout.username() + 
                             " logged out at " + logout.timestamp());
                             
        } else if (event instanceof OrderPlacedEvent order) {
            System.out.println("Order " + order.orderId() + 
                             " placed for " + order.amount() + 
                             " by customer " + order.customerId());
        }
    }
}

// Użycie
public class Main {
    public static void main(String[] args) {
        EventProcessor processor = new EventProcessor();
        
        // Tworzenie zdarzeń jako Records
        var loginEvent = new UserLoginEvent("john.doe", LocalDateTime.now());
        var orderEvent = new OrderPlacedEvent("ORD-001", 
                                            new BigDecimal("299.99"), 
                                            "CUST-123");
        
        processor.handleEvent(loginEvent);
        processor.handleEvent(orderEvent);
    }
}

Kiedy używać Records, a kiedy tradycyjnych klas

Użyj Records gdy…Użyj tradycyjnych klas gdy…
Przechowujesz tylko danePotrzebujesz mutowalne obiekty
Chcesz immutable obiektyPotrzebujesz dziedziczenia
Potrzebujesz tylko podstawowe metodyMasz skomplikowaną logikę biznesową
Tworzysz DTO, Value ObjectsImplementujesz wzorce projektowe
Pracujesz z API responsesPotrzebujesz custom serialization
Typowy błąd początkujących: Próba używania Records wszędzie. Records są świetne do przechowywania danych, ale nie zastąpią tradycyjnych klas w każdym przypadku.

Konfiguracja Java 14 w projekcie

Aby używać Records i Pattern Matching, potrzebujesz Java 14:



    14
    14
    14



    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.8.1
            
                14
                
                
                    --enable-preview
                
            
        
    

// Gradle - build.gradle
sourceCompatibility = '14'
targetCompatibility = '14'

compileJava {
    options.compilerArgs += '--enable-preview'
}

compileTestJava {
    options.compilerArgs += '--enable-preview'
}

test {
    jvmArgs += '--enable-preview'
}
Czy Records mogą implementować interfejsy?

Tak! Records mogą implementować interfejsy, ale nie mogą dziedziczyć po innych klasach (oprócz domyślnego dziedziczenia po Record).

Czy mogę dodać pole do Record, które nie jest w konstruktorze?

Nie. Wszystkie pola w Record muszą być zdefiniowane w nagłówku. Możesz dodać static pola lub metody, ale nie instance fields.

Jak działa serialization z Records?

Records mają specjalną obsługę serialization w Javie. Są automatycznie Serializable jeśli wszystkie komponenty są Serializable.

Czy Pattern Matching działa z null?

Nie. Jeśli zmienna jest null, pattern matching zwróci false. Zawsze sprawdzaj null osobno: if (obj != null && obj instanceof String str)

Czy Records są thread-safe?

Tak, jeśli wszystkie ich komponenty są immutable. Ponieważ Records domyślnie tworzą final pola, są naturalnie thread-safe.

Jak testować klasy używające Records?

Records mają automatycznie wygenerowane equals(), więc testy są bardzo proste. Możesz porównywać obiekty bezpośrednio: assertEquals(expected, actual)

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz system zarządzania biblioteką używając Records. Zdefiniuj Record dla Book (title, author, isbn, publicationYear) i Record dla BorrowingRecord (bookIsbn, borrowerName, borrowDate, returnDate). Napisz klasę LibraryService która używa Pattern Matching do przetwarzania różnych typów zdarzeń (BookBorrowed, BookReturned, BookAdded). Dodaj walidację w compact constructors.

Jakie pierwsze wrażenia masz po poznaniu Records i Pattern Matching? Widzisz miejsca w swoich projektach gdzie mogłyby uprościć kod?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry