Cópias defensivas para coleções usando o AutoValue

Cópias defensivas para coleções usando o AutoValue

1. Visão geral

Criar objetos de valor imutável introduz um pouco de clichê indesejado. Além disso,Java’s standard collections types tem o potencial de introduzir mutabilidade em objetos de valor onde essa característica é indesejável.

Neste tutorial, demonstraremos como criar cópias defensivas de coleções ao usarAutoValue, uma ferramenta útil para reduzir o código clichê para definir objetos de valor imutável.

2. Objetos de valor e cópias defensivas

A comunidade Java geralmente considera que objetos de valor são uma classificação de tipos que representam registros de dados imutáveis. Obviamente, esses tipos podem conter referências a tipos de coleções Java padrão comojava.util.List.

Por exemplo, considere um objeto de valorPerson:

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

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

Como os tipos de coleção padrão do Java podem ser mutáveis, o tipoPerson imutável deve se proteger de chamadores que modificariam a listafavoriteMovies após criar um novoPerson:

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

A classePerson deve fazer uma cópia defensiva da coleçãofavoriteMovies. Ao fazer isso, a classePerson captura o estado da listafavoriteMovies como ela existia quandoPerson foi criado.

O construtor da classePerson pode fazer uma cópia defensiva da listafavoriteMovies usando o método de fábrica estáticoList.copyOf:

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

Java 10 introduziu métodos de fábrica estáticos de cópia defensiva, comoList.copyOf. Os aplicativos que usam versões mais antigas do Java podem criar uma cópia defensiva usando um construtor de cópia e um dos métodos de fábrica estáticos “não modificáveis” na classeCollections:

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

Observe que não há necessidade de fazer uma cópia defensiva do parâmetroString name uma vez que as instânciasString são imutáveis.

3. Valor automático e cópias defensivas

O AutoValue é uma ferramenta de processamento de anotação para gerar o código padrão para definir os tipos de objetos de valor. No entanto,AutoValue does not make defensive copies when constructing a value object.

A anotação@AutoValue instrui AutoValue a gerar uma classeAutoValue_Person, que estendePersone inclui os acessadores, construtor,toString,equals ehashCode métodos que omitimos anteriormente da classePerson.

Por último, adicionamos um método de fábrica estático à classePersone invocamos o construtorAutoValue_Person gerado:

@AutoValue
public abstract class Person {

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

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

O construtor AutoValue gerado não criará automaticamente nenhuma cópia defensiva, incluindo uma para a coleçãofavoriteMovies.

Portanto, precisamoscreate a defensive copy of the favoriteMovies collection in the static factory method que definimos:

public abstract class Person {

    public static Person of(String name, List 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 favoriteMovies();
}

4. Criadores de AutoValor e cópias defensivas

Quando desejado, podemos usar a anotação@AutoValue.Builder, que instrui AutoValue a gerar uma classeBuilder:

@AutoValue
public abstract class Person {

    public abstract String name();
    public abstract List 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 value);
        public abstract Person build();
    }
}

Como o AutoValue gera as implementações de todos os métodos abstratos, não está claro como criar uma cópia defensiva doList. Precisamos usar uma mistura de código gerado por AutoValue e código personalizado para fazer cópias defensivas das coleções pouco antes de o construtor construir a nova instânciaPerson.

Primeiro, vamos complementar nosso construtor com dois novos métodos abstratos de pacote privado:favoriteMovies()eautoBuild(). Esses métodos são privados do pacote porque queremos usá-los em nossa implementação personalizada do métodobuild(), mas não queremos que os consumidores desta API os usem.

@AutoValue.Builder
public static abstract class Builder {

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

    abstract List favoriteMovies();
    abstract Person autoBuild();

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

Finalmente, forneceremos umcustom implementation of the build() method that creates the defensive copy da lista antes de construir oPerson. Usaremos o métodofavoriteMovies() para recuperar oList que o usuário definiu. Em seguida, substituiremos a lista por uma nova cópia antes de chamarautoBuild() para construir oPerson:

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

5. Conclusão

Neste tutorial, aprendemos que o AutoValue não cria automaticamente cópias defensivas, o que geralmente é importante para coleções Java.

Demonstramos como criar cópias defensivas nos métodos estáticos de fábrica antes de construir instâncias das classes geradas pelo AutoValue. A seguir, mostramos como combinar código personalizado e gerado para criar cópias defensivas ao usar as classesBuilder do AutoValue.

Como sempre, os trechos de código usados ​​neste tutorial estão disponíveisover on GitHub.