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.