Java 14 – Record classes

Wydanie nowej wersji Javy zostało zaplanowane na 17 marca 2020. Wśród zapowiedzianych nowości znajdują się rekordy (records). Mają one służyć do przechowywania płytkiego, niemutowalnego stanu aplikacji. Co to oznacza w praktyce? Przede wszystkim mniej powtarzalnego kodu który musi wygenerować i utrzymać programista.

Funkcjonalność ta jest opisana jako Java Enhancement Proposal 359 (JEP359). W 14 wersji języka wejdzie ona jako preview. Oznacza to w pełni działająca funkcjonalność która w przyszłych wersjach może się zmienić lub nawet zostać całkowicie usunięta. Wszystko w zależności od informacji zwrotnej od programistów którzy zdecydują się jej używać.

Jeśli chcesz wypróbować sam przykłady, możesz pobrać developerską wersję JDK 14 ze strony https://jdk.java.net/14/. Pamiętaj tylko, że przy kompilacji i uruchomieniu musisz dodać flagę –enable-prewiev. Wszystkie przykłady które pokazuję są dostępne w repozytorium GitHuba pod adresem https://github.com/mloza/jvm14-records. Niestety IDE jeszcze nie dostarczają wsparcia (podobno będzie w Intellij 2020.1, w EAP na dzień dzisiejszy jeszcze nie działa) i będą sygnalizować błędy przy użyciu słowa kluczowego Record.

Pierwszy przykład

Zacznijmy od bardzo prostego przykładu klasy która będzie przetrzymywać kilka podstawowych pól.

public record Person(
String firstName,
String lastName,
int age) {}
view raw Person.java hosted with ❤ by GitHub

Całość zamknęła się w 4 linijkach. Porównajmy to do zwykłej klasy którą musielibyśmy stworzyć aby otrzymać taki sam rezultat.

public final class PersonOldWay {
private final String firstName;
private final String lastName;
private final int age;
public PersonOldWay(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String firstName() {
return firstName;
}
public String lastName() {
return lastName;
}
public int age() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PersonOldWay personOld = (PersonOldWay) o;
return age == personOld.age &&
Objects.equals(firstName, personOld.firstName) &&
Objects.equals(lastName, personOld.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("PersonOldWay{");
sb.append("firstName='").append(firstName).append('\'');
sb.append(", lastName='").append(lastName).append('\'');
sb.append(", age=").append(age);
sb.append('}');
return sb.toString();
}
}
view raw PersonOldWay.java hosted with ❤ by GitHub

Czyli to co otrzymujemy automatycznie to podstawowy konstruktor przypisujący wartości wszystkich pól, gettery do pól oraz metody equals, hashCode oraz to String. Całość zajmuje 52 linie, czyli 49 lini kodu różnicy. Co prawda większość z tego zostało wygenerowane automatycznie przez IDE, jednak utrzymanie tego jest uciążliwe. Jeśli chcemy dodać kolejne pole, musimy również zmodyfikować metody equals, toString, hashCode plus wygenerować getter. Używając rekordów musimy dodać tylko jedną linię z deklaracją pola.

Kilka rzeczy na które trzeba zwrócić uwagę:

  • rekord jest klasą finalną, czyli nie można po niej dziedziczyć,
  • pola rekordu są również finalne, nie można modyfikować ich zawartości (są niemutowalne),
  • rekordy nie mogą być abstrakcyjne,
  • domyślnie rekord dziedziczy po klasie java.lang.Record, tak samo jak wszystkie obiekty dziedziczą po Object, a enumy po Enum
  • rekordy mogą implementować interfejsy,
  • w przykładzie użyłem tylko typów prostych, ale można używać również typów złożonych,
  • rekordy i ich pola można adnotować jak w normalnych klasach.

Użycie klas rekordów

Klas rekordów używamy w ten sam sposób jak normalnych klas. Nowe instancje są tworzone z użyciem słowa kluczowego new. W konstruktorze musimy podać wszystkie parametry które zadeklarowaliśmy w klasie. Przykład poniżej.

public class Main {
public static void main(String[] args) {
Person p = new Person("Java", "Developer", 29);
System.out.println(p.firstName());
System.out.println(p.lastName());
System.out.println(p.age());
System.out.println(p);
}
}
view raw Main.java hosted with ❤ by GitHub

Po uruchomieniu programu otrzymamy następujący wynik.

 Java
 Developer
 29
 Person[firstName=Java, lastName=Developer, age=29]

Konstruktory

Możemy nadpisać domyślny konstruktor który przypisuje tylko wartości pól i dodać do niego na przykład walidację danych. Możemy też dodawać własne konstruktory, ale w pierwszej linii muszą one wywoływać podstawowy konstruktor.

public record Person (
String firstName,
String lastName,
int age) {
public Person {
if (age < 20) {
new IllegalArgumentException("You are too young!");
}
}
public Person() {
this("Java", "Developer", 21);
System.out.println("Default object has been created");
}
}

Metody i pola w rekordach

Nie możemy deklarować pól instancji w rekordach (poza tymi zadeklarowanymi w nagłówku). Możemy jedynie dodać pola statyczne. Możemy za to tworzyć metody operujące na stanie obiektu, zarówno statyczne jak i metody instancji.

public record Person (
String firstName,
String lastName,
int age) {
public static int x;
public static int inc() {
return x++;
}
public String getFullName() {
return firstName + " " + lastName;
}
}
view raw PersonMethods.java hosted with ❤ by GitHub

Lokalne rekordy

Ciekawą opcją jest też możliwość tworzenia rekordów lokalnie, na przykład w ciele metody.

public static void localRecord() {
String persons = """
Java,Developer,29
Python,Developer,25
John,Doe,65
""";
record LocalPerson(String firstName, String lastName, int age) {
LocalPerson(String[] data) {
this(data[0], data[1], Integer.parseInt(data[2]));
}
}
Arrays.stream(persons.split("\n"))
.map(i -> i.split(","))
.map(LocalPerson::new)
.forEach(System.out::println);
}
view raw LocalRecord.java hosted with ❤ by GitHub

Podsumowanie

Rekordy będą ciekawą nowością. Dobrym zastosowaniem dla nich będzie na przykład tworzenie modelu danych który przyjmuje lub zwraca aplikacja. Można by też pokusić się o użycie ich do opisu modelu bazy danych, jednak to że są niemutowalne może stworzyć tutaj dodatkowe problemy. Rozwiązania takie są już znane w innych językach opartych o JVM jak chociażby Kotlin czy Scala. Można powiedzieć, że w Javie pojawia się to dosyć późno. Aktualnie IntelliJ jeszcze nie wspiera tej funkcjonalności i wciąż sygnalizuje błędy przy próbie jej użycia, wsparcie ma się pojawić dopiero w wersji 2020.1.

Jest to tylko jedna z nowości przychodzących wraz z Javą 14. O pozostałych postaram się napisać więcej w kolejnych wpisach.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *