«1. Введение

В этой статье мы покажем, как работать с библиотекой Immutables.

Библиотека состоит из аннотаций и обработчиков аннотаций для создания и работы с сериализуемыми и настраиваемыми неизменяемыми объектами.

2. Зависимости Maven

Чтобы использовать Immutables в вашем проекте, вам нужно добавить следующую зависимость в раздел зависимостей вашего файла pom.xml:

<dependency>
    <groupId>org.immutables</groupId>
    <artifactId>value</artifactId>
    <version>2.2.10</version>
    <scope>provided</scope>
</dependency>

Поскольку этот артефакт не требуется во время время выполнения, поэтому рекомендуется указать предоставленную область.

Новейшую версию библиотеки можно найти здесь.

3. Неизменяемые объекты

Библиотека генерирует неизменяемые объекты из абстрактных типов: Интерфейс, Класс, Аннотация.

Ключом к достижению этого является правильное использование аннотации @Value.Immutable. Он создает неизменяемую версию аннотированного типа и добавляет к его имени ключевое слово Immutable.

Если мы попытаемся сгенерировать неизменяемую версию класса с именем «X», будет сгенерирован класс с именем «ImmutableX». Сгенерированные классы не являются рекурсивно-неизменяемыми, поэтому следует помнить об этом.

И небольшое примечание: поскольку Immutables использует обработку аннотаций, вам нужно не забыть включить обработку аннотаций в вашей среде IDE.

3.1. Использование @Value.Immutable с абстрактными классами и интерфейсами

Давайте создадим простой абстрактный класс Person, состоящий из двух абстрактных методов доступа, представляющих создаваемые поля, а затем аннотируем класс аннотацией @Value.Immutable: ~~ ~

@Value.Immutable
public abstract class Person {

    abstract String getName();
    abstract Integer getAge();

}

После обработки аннотаций мы можем найти готовый к использованию, недавно сгенерированный класс ImmutablePerson в каталоге target/generated-sources:

@Generated({"Immutables.generator", "Person"})
public final class ImmutablePerson extends Person {

    private final String name;
    private final Integer age;

    private ImmutablePerson(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    String getName() {
        return name;
    }

    @Override
    Integer getAge() {
        return age;
    }

    // toString, hashcode, equals, copyOf and Builder omitted

}

Сгенерированный класс поставляется с реализованными toString, hashcode, equals методы и пошаговый конструктор ImmutablePerson.Builder. Обратите внимание, что сгенерированный конструктор имеет закрытый доступ.

Чтобы создать экземпляр класса ImmutablePerson, нам нужно использовать конструктор или статический метод ImmutablePerson.copyOf, который может создать копию ImmutablePerson из объекта Person.

Если мы хотим построить экземпляр с помощью компоновщика, мы можем просто написать:

ImmutablePerson john = ImmutablePerson.builder()
  .age(42)
  .name("John")
  .build();

Сгенерированные классы являются неизменяемыми, что означает, что они не могут быть изменены. Если вы хотите изменить уже существующий объект, вы можете использовать один из методов «withX», которые не изменяют исходный объект, а создают новый экземпляр с измененным полем.

Обновим возраст john и создадим новый объект john43:

ImmutablePerson john43 = john.withAge(43);

В таком случае будут верны следующие утверждения:

assertThat(john).isNotSameAs(john43);
assertThat(john.getAge()).isEqualTo(42);

4. Дополнительные утилиты

Такая генерация класса было бы не очень полезно, если бы не было возможности настроить его. Библиотека Immutables поставляется с набором дополнительных аннотаций, которые можно использовать для настройки вывода @Value.Immutable. Чтобы увидеть их все, обратитесь к документации Immutables.

4.1. Аннотация @Value.Parameter

Аннотацию @Value.Parameter можно использовать для указания полей, для которых должен быть сгенерирован метод-конструктор.

@Value.Immutable
public abstract class Person {

    @Value.Parameter
    abstract String getName();

    @Value.Parameter
    abstract Integer getAge();
}

Если вы аннотируете свой класс следующим образом:

ImmutablePerson.of("John", 42);

его можно будет создать следующим образом:

4.2. Аннотация @Value.Default

@Value.Immutable
public abstract class Person {

    abstract String getName();

    @Value.Default
    Integer getAge() {
        return 42;
    }
}

Аннотация @Value.Default позволяет указать значение по умолчанию, которое должно использоваться, когда начальное значение не указано. Для этого вам нужно создать неабстрактный метод доступа, возвращающий фиксированное значение, и аннотировать его с помощью @Value.Default:

ImmutablePerson john = ImmutablePerson.builder()
  .name("John")
  .build();

assertThat(john.getAge()).isEqualTo(42);

Следующее утверждение будет верным:

4.3. Аннотация @Value.Auxiliary

Аннотацию @Value.Auxiliary можно использовать для аннотации свойства, которое будет храниться в экземпляре объекта, но будет игнорироваться реализациями equals, hashCode и toString.

@Value.Immutable
public abstract class Person {

    abstract String getName();
    abstract Integer getAge();

    @Value.Auxiliary
    abstract String getAuxiliaryField();

}

Если вы аннотируете свой класс следующим образом:

ImmutablePerson john1 = ImmutablePerson.builder()
  .name("John")
  .age(42)
  .auxiliaryField("Value1")
  .build();

ImmutablePerson john2 = ImmutablePerson.builder()
  .name("John")
  .age(42)
  .auxiliaryField("Value2")
  .build();

assertThat(john1.equals(john2)).isTrue();
assertThat(john1.toString()).isEqualTo(john2.toString());
assertThat(john1.hashCode()).isEqualTo(john2.hashCode());

Следующие утверждения будут истинными при использовании вспомогательного поля:

@Value.Immutable(prehash = true)
public abstract class Person {

    abstract String getName();
    abstract Integer getAge();

}

4.4. Аннотация @Value.Immutable(Prehash = True)

@Generated({"Immutables.generator", "Person"})
public final class ImmutablePerson extends Person {

    private final String name;
    private final Integer age;
    private final int hashCode;

    private ImmutablePerson(String name, Integer age) {
        this.name = name;
        this.age = age;
        this.hashCode = computeHashCode();
    }

    // generated methods
 
    @Override
    public int hashCode() {
        return hashCode;
    }
}

«Поскольку наши сгенерированные классы неизменяемы и никогда не могут быть изменены, результаты hashCode всегда будут оставаться одинаковыми и могут быть вычислены только один раз во время создания экземпляра объекта.

Если вы аннотируете свой класс следующим образом:

При проверке сгенерированного класса вы можете увидеть, что значение хэш-кода теперь предварительно вычислено и сохранено в поле:

Метод hashCode() возвращает значение предварительно вычисленный хэш-код, сгенерированный при создании объекта.