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
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.
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!
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 Setpeople = 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); } }
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"; }
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(); }; } }
Łą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 dane | Potrzebujesz mutowalne obiekty |
Chcesz immutable obiekty | Potrzebujesz dziedziczenia |
Potrzebujesz tylko podstawowe metody | Masz skomplikowaną logikę biznesową |
Tworzysz DTO, Value Objects | Implementujesz wzorce projektowe |
Pracujesz z API responses | Potrzebujesz custom serialization |
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' }
Tak! Records mogą implementować interfejsy, ale nie mogą dziedziczyć po innych klasach (oprócz domyślnego dziedziczenia po Record).
Nie. Wszystkie pola w Record muszą być zdefiniowane w nagłówku. Możesz dodać static pola lub metody, ale nie instance fields.
Records mają specjalną obsługę serialization w Javie. Są automatycznie Serializable jeśli wszystkie komponenty są Serializable.
Nie. Jeśli zmienna jest null, pattern matching zwróci false. Zawsze sprawdzaj null osobno: if (obj != null && obj instanceof String str)
Tak, jeśli wszystkie ich komponenty są immutable. Ponieważ Records domyślnie tworzą final pola, są naturalnie thread-safe.
Records mają automatycznie wygenerowane equals(), więc testy są bardzo proste. Możesz porównywać obiekty bezpośrednio: assertEquals(expected, actual)
Przydatne zasoby:
- JEP 359: Records (Official)
- JEP 305: Pattern Matching for instanceof
- Oracle Java 14 Documentation
- Oracle Java Magazine – Records
🚀 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?