«1. Обзор

Создание неизменяемых объектов-значений вводит немного нежелательного шаблона. Кроме того, стандартные типы коллекций Java потенциально могут привнести изменчивость в объекты-значения, где эта черта нежелательна.

В этом руководстве мы покажем, как создавать защитные копии коллекций при использовании AutoValue, полезного инструмента для сокращения стандартного кода для определения неизменяемых объектов-значений.

2. Объекты-значения и защитные копии

Сообщество Java обычно рассматривает объекты-значения как классификацию типов, представляющих неизменяемые записи данных. Конечно, такие типы могут содержать ссылки на стандартные типы коллекций Java, такие как java.util.List.

Например, рассмотрим объект-значение Person:

class Person {
    private final String name;
    private final List<String> favoriteMovies;

    // accessors, constructor, toString, equals, hashcode omitted
}

Поскольку стандартные типы коллекций Java могут быть изменяемыми, неизменяемый тип Person должен защищать себя от вызывающих объектов, которые могут изменить список избранных фильмов после создания нового Person: ~~ ~

var favoriteMovies = new ArrayList<String>();
favoriteMovies.add("Clerks"); // fine
var person = new Person("Katy", favoriteMovies);
favoriteMovies.add("Dogma"); // oh, no!

Класс Person должен создать защитную копию коллекции FavoriteMovies. Таким образом, класс Person фиксирует состояние списка FavoriteMovies, существовавшее на момент создания Person.

Конструктор класса Person может создать защитную копию списка FavoriteMovies с помощью статического фабричного метода List.copyOf:

public Person(String name, List<String> favoriteMovies) {
    this.name = name;
    this.favoriteMovies = List.copyOf(favoriteMovies);
}

В Java 10 появились статические фабричные методы защитного копирования, такие как List.copyOf. Приложения, использующие более старые версии Java, могут создавать защитную копию с помощью конструктора копирования и одного из «немодифицируемых» статических фабричных методов класса Collections:

public Person(String name, List<String> favoriteMovies) {
    this.name = name;
    this.favoriteMovies = Collections.unmodifiableList(new ArrayList<>(favoriteMovies));
}

Обратите внимание, что нет необходимости создавать защитную копию String параметр name, поскольку экземпляры String неизменяемы.

3. AutoValue и защитные копии

AutoValue — это инструмент обработки аннотаций для создания стандартного кода для определения типов объектов значений. Однако AutoValue не создает защитных копий при создании объекта-значения.

Аннотация @AutoValue предписывает AutoValue сгенерировать класс AutoValue_Person, который расширяет Person и включает методы доступа, конструктор, toString, equals и hashCode, которые мы ранее исключили из класса Person.

Наконец, мы добавляем статический фабричный метод к классу Person и вызываем сгенерированный конструктор AutoValue_Person:

@AutoValue
public abstract class Person {

    public static Person of(String name, List<String> favoriteMovies) {
        return new AutoValue_Person(name, favoriteMovies);
    }

    public abstract String name();
    public abstract List<String> favoriteMovies();
}

Конструктор, сгенерированный AutoValue, не будет автоматически создавать никаких защитных копий, в том числе одну для коллекции FavoriteMovies.

Таким образом, нам нужно создать защитную копию коллекции FavoriteMovies в статическом фабричном методе, который мы определили:

public abstract class Person {

    public static Person of(String name, List<String> favoriteMovies) {
        // create defensive copy before calling constructor
        var favoriteMoviesCopy = List.copyOf(favoriteMovies);
        return new AutoValue_Person(name, favoriteMoviesCopy);
    }

    public abstract String name();
    public abstract List<String> favoriteMovies();
}

4. Построители AutoValue и защитные копии

При желании мы можем использовать @AutoValue. Аннотация Builder, предписывающая AutoValue генерировать класс Builder:

@AutoValue
public abstract class Person {

    public abstract String name();
    public abstract List<String> favoriteMovies();

    public static Builder builder() {
        return new AutoValue_Person.Builder();
    }

    @AutoValue.Builder
    public static class Builder {
        public abstract Builder name(String value);
        public abstract Builder favoriteMovies(List<String> value);
        public abstract Person build();
    }
}

Поскольку AutoValue генерирует реализации всех абстрактных методов, неясно, как создать защитную копию List. Нам нужно использовать смесь кода, сгенерированного AutoValue, и пользовательского кода, чтобы создавать защитные копии коллекций непосредственно перед тем, как построитель создаст новый экземпляр Person.

Во-первых, мы добавим в наш конструктор два новых абстрактных метода, зависящих от пакета: FavoriteMovies() и autoBuild(). Эти методы являются частными для пакета, потому что мы хотим использовать их в нашей пользовательской реализации метода build(), но мы не хотим, чтобы потребители этого API использовали их.

@AutoValue.Builder
public static abstract class Builder {

    public abstract Builder name(String value);
    public abstract Builder favoriteMovies(List<String> value);

    abstract List<String> favoriteMovies();
    abstract Person autoBuild();

    public Person build() {
        // implementation omitted
    }
}

Наконец, мы обеспечим пользовательскую реализацию метода build(), который создает защитную копию списка перед созданием объекта Person. Мы будем использовать метод FavoriteMovies() для получения списка, установленного пользователем. Затем мы заменим список новой копией перед вызовом autoBuild() для создания объекта Person:

public Person build() {
    List<String> favoriteMovies = favoriteMovies();
    List<String> copy = Collections.unmodifiableList(new ArrayList<>(favoriteMovies));
    favoriteMovies(copy);
    return autoBuild();
}

5. Заключение

В этом руководстве мы узнали, что AutoValue не создает автоматически защитные копии, которые часто имеет важное значение для коллекций Java.

«Мы продемонстрировали, как создавать защитные копии в статических фабричных методах перед созданием экземпляров классов, сгенерированных AutoValue. Далее мы показали, как комбинировать пользовательский и сгенерированный код для создания защитных копий при использовании классов AutoValue Builder.

Как всегда, фрагменты кода, используемые в этом руководстве, доступны на GitHub.