C, PHP, VB, .NET

Дневникът на Филип Петров


* Record в Java

Публикувано на 11 май 2023 в раздел ПИК3 Java.

В практиката често се работи с непроменими (immutable) обекти. Те се описват чрез класове, в които всички член променливи са private и final, и има само и единствено get методи. Така след като е създаден, обектът не може да се променя. Въпреки, че в зависимост от употребата може да се спестят някои елементи от шаблона, който ще покажа по-долу, едно цялостно решение би включило освен get методи до променливите, също метод hashCode(), метод equals() и метод toString():

package recordsexample;

public class RecordsExample {

    public static void main(String[] args) {
        Person p1 = new Person("Ivan", "Ivanov", 20);
        System.out.println(p1.toString());
    }
}

final 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;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = result * 37 + (this.firstname == null ? 0 : this.firstname.hashCode());
        result = result * 37 + (this.lastname == null ? 0 : this.lastname.hashCode());
        result = result * 37 + this.age;
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Person)) {
            return false;
        }
        Person p = (Person) o;
        if (p.firstname == null || p.lastname == null) {
            return false;
        }
        return (this.firstname.equals(p.getFirstname()) && this.lastname.equals(p.getLastName()) && this.age == p.getAge());
    }

    @Override
    public String toString() {
        return "[firstname=" + this.firstname + ", lastname=" + this.lastname + ", age=" + this.age + "]";
    }

    public String getFirstname(){
        return this.firstname;
    }
    
    public String getLastName(){
        return this.lastname;
    }
    
    public int getAge(){
        return this.age;
    }
}

С JDK 16 на помощ в такива ситуации идва новият тип непроменими (immutable) класове Record. Целият описан по-горе код може да бъде заменен с (почти) еквивалентния следен:

package recordsexample;

public class RecordsExample {

    public static void main(String[] args) {
        Person p1 = new Person("Ivan", "Ivanov", 20);
        System.out.println(p1.toString());
    }
}

record Person(String firstname, String lastname, int age){};

Споменах „почти“, защото имплементираният от мен метод hashCode() е различен от този, който се използва в record класа. Друга съществена разлика е, че get методите в record не съдържат думата "get" в името си. Например ако искате да прочетете първото име на човека, трябва да извикате метод "firstname":

Person p1 = new Person("Ivan", "Ivanov", 20);
System.out.println(p1.firstname());

Това е направено нарочно, като най-вероятната причина е да се направи съществена отличителна разлика между record и JavaBean. Още една разлика е, че record е винаги final клас (не може да бъде наследяван), както и че той не може да наследява друг (позволено е обаче да имплентира интерфейси).  От последното следва, че record не може да е и абстрактен.

Удобно нововъведение е, че record дават възможност за добавяне на специален „компактен конструктор“, с който се добавя валидация на данните. Например ако искаме да подсигурим, че годините не са отрицателно число, можем да направим следното:

record Person(String firstname, String lastname, int age){
    public Person{
        if(age<0) throw new IllegalArgumentException("Age cannot be negative");
        if(firstname==null || lastname==null) throw new IllegalArgumentException("Names cannot be null");
    }
};

От тук насетне може да добавяте и нови методи или да предефинирате някои от съществуващите. Например можем да направим допълнителен конструктор, който приема годините по подразбиране със стойност 18, както и да добавим метод за връщане на пълното име на човека по следния начин:

record Person(String firstname, String lastname, int age){
    public Person{
        if(age<0) throw new IllegalArgumentException("Age cannot be negative");
        if(firstname==null || lastname==null) throw new IllegalArgumentException("Names cannot be null");
    }
    
    public Person(String firstname, String lastname){
        this(firstname, lastname, 18);
    }

    String fullName(){
        return this.firstname+" "+this.lastname;
    }
};

Може да се досетите, че е възможно да създадете свой get метод, който просто препраща към стандартно генерирания. Това по-скоро би било лоша практика и е по-добре да се избягва.

Накрая ще изредя и някои ограничения за клас record:

  • не може да наследява;
  • не може да бъде наследяван;
  • не може да се добавят допълнителни член променливи извън дефинираните в кръглите скоби (но е възможно да се дефинират статични променливи).

На практика record е създадено за удобство да не пишете голямо количество тривиален код. Възползвайте се когато е удачно. Единствено помнете, че той е само и единствено за непроменими (immuatable) обекти.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*