Olá!
Agora vamos falar sobre classes imutáveis em Java e mostrar como tornar a classe imutável.
A classe é chamada imutável se for impossível alterar seu estado e conteúdo após a inicialização (criação).
Os benefícios de tais classes são:
- segurança: você, como desenvolvedor, pode ter certeza de que ninguém é capaz de mudar seu estado;
- thread-safety: o mesmo mencionado acima também é real para o ambiente multithread;
- armazenável em cache: as instâncias podem ser armazenadas em cache facilmente pelo cache da VM ou pela implementação customizada, pois temos 100% de certeza de que seus valores não serão alterados;
- hashable: tais classes podem ser seguramente colocadas dentro das coleções de hash (como
HashMap
,HashSet
etc) – é claro, se os métodosequals()
&hashCode()
são substituídos de maneira apropriada.
Então, vamos tentar implementar a classe imutável pegando o mutável e mudando sua implementação para o imutável passo a passo.
Considere, temos a seguinte classe POJO típica Hobbit
:
import java.util.List; public class Hobbit { private String name; private Address address; private List<string> stuff; public String getName() { return name; } public void setName(String name) { this.name = name; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } public List<string> getStuff() { return stuff; } public void setStuff(List<string> stuff) { this.stuff = stuff; } }
Hobbit
classe tem um campo address
do tipo Address
:
public class Address { private String country; private String city; public Address(String country, String city) { this.country = country; this.city = city; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } }
Address
A classe não será alterada neste artigo, portanto, em caso de dúvidas, consulte o código incorporado acima.
A primeira questão a ser notada são setters, que permitem alterar o valor de cada campo de classe. Então, vamos remover esses métodos:
1. removendo setters
public class Hobbit { private String name; private Address address; private List<String> stuff; // getters: public String getName() { return name; } public Address getAddress() { return address; } public List<String> getStuff() { return stuff; } }
Parece melhor, mas agora o usuário de nossa classe não tem chances de definir os valores para campos de classe. Então, vamos fornecer o construtor apropriado:
2. adicionando todos os construtores args
public class Hobbit { private String name; private Address address; private List<String> stuff; // all args constructor: public Hobbit(String name, Address address, List<String> stuff) { this.name = name; this.address = address; this.stuff = stuff; } // getters: public String getName() { return name; } public Address getAddress() { return address; } public List<String> getStuff() { return stuff; } }
Nós terminamos agora? Ainda não, restam vários problemas. Primeiro de tudo, nossa imutabilidade poderia ser hackeada usando um dos princípios da OOP – herança. Deixe-me demonstrar isso com um exemplo simples:
import java.util.List; public class Gandalf extends Hobbit { private String hackedName; public Gandalf(String name, Address address, List<String> stuff) { super(name, address, stuff); hackedName = name; } public void hackTheImmutability(String newNameValue) { hackedName = newNameValue; System.out.println("Immutability has been hacked!"); } @Override public String getName() { return hackedName; } }
import java.util.Collections; public class Hack { public static void main(String[] args) { Gandalf gandalf = new Gandalf("Frodo Baggins", new Address("Hobitton", "Shire"), Collections.emptyList()); Hobbit hobbit = (Hobbit) gandalf; System.out.println(hobbit.getName()); System.out.println(); gandalf.hackTheImmutability("Mr. Underhill"); System.out.println(); System.out.println(hobbit.getName()); } }
Se executarmos a Hack
turma, obteremos a seguinte saída:
Frodo Bolseiro
Imutabilidade foi hackeada!
Mr. Underhill
Para corrigir esse problema, precisamos:
3. classe de marcação como final para protegê-la de ser estendida
public final class Hobbit { private String name; private Address address; private List<String> stuff; // all args constructor: public Hobbit(String name, Address address, List<String> stuff) { this.name = name; this.address = address; this.stuff = stuff; } // getters: public String getName() { return name; } public Address getAddress() { return address; } public List<String> getStuff() { return stuff; } }
Agora, se executarmos a Hack
aula mais uma vez, a saída estará correta:
Frodo Bolseiro
Imutabilidade foi hackeada!
Frodo Bolseiro
Há mais uma questão interessante: como sabemos, em Java quando passamos o valor de um objeto, estamos passando a referência a ele. Assim, o estado dos objetos poderia ser alterado pelos efeitos colaterais. Vamos voltar ao código para ver isso:
import java.util.List; import java.util.ArrayList; public class Hack { public static void main(String[] args) { Address address = new Address("Hobitton", "Shire"); List<String> stuff = new ArrayList<>(); stuff.add("Sword"); stuff.add("Ring of Power"); Hobbit hobbit = new Hobbit("Frodo Baggins", address, stuff); System.out.println("Hobbit country: " + hobbit.getAddress().getCountry()); System.out.println("Hobbit city: " + hobbit.getAddress().getCity()); System.out.println("Hobbit stuff: " + hobbit.getStuff()); address.setCountry("Isengard"); address.setCity("Saruman tower"); stuff.remove("Ring of Power"); stuff.remove("Sword"); System.out.println(); System.out.println("Immutability has been hacked!"); System.out.println(); System.out.println("Hobbit country: " + hobbit.getAddress().getCountry()); System.out.println("Hobbit city: " + hobbit.getAddress().getCity()); System.out.println("Hobbit stuff: " + hobbit.getStuff()); } }
A saída deste código é:
Hobbit country: Hobitton
Hobbit city: Shire
Hobbit stuff: [Espada, Anel do Poder]
Imutabilidade foi hackeada!
Hobbit country: Isengard
Cidade de Hobbit: Saruman tower
Hobbit stuff: []
Esse problema pode ser corrigido por:
4. inicializando todos os campos mutáveis não-primitivos via construtor executando uma cópia profunda
import java.util.List; import java.util.ArrayList; public final class Hobbit { private String name; private Address address; private List<String> stuff; // all args constructor: public Hobbit(String name, Address address, List<String> stuff) { this.name = name; this.address = new Address(address.getCountry(), address.getCity()); this.stuff = new ArrayList<>(stuff); } // getters: public String getName() { return name; } public Address getAddress() { return address; } public List<String> getStuff() { return stuff; } }
Agora a saída da Hack
classe é mais previsível:
Hobbit country: Hobitton
Hobbit city: Shire
Hobbit stuff: [Espada, Anel do Poder]
Imutabilidade foi hackeada!
Hobbit country: Hobitton
Hobbit city: Shire
Hobbit stuff: [Espada, Anel do Poder]
Mas o mesmo problema de efeito colateral poderia ser produzido através de métodos getter:
import java.util.ArrayList; import java.util.List; public class Hack { public static void main(String[] args) { Address address = new Address("Hobitton", "Shire"); List<String> stuff = new ArrayList<>(); stuff.add("Sword"); stuff.add("Ring of Power"); Hobbit hobbit = new Hobbit("Frodo Baggins", address, stuff); System.out.println("Hobbit country: " + hobbit.getAddress().getCountry()); System.out.println("Hobbit city: " + hobbit.getAddress().getCity()); System.out.println("Hobbit stuff: " + hobbit.getStuff()); Address hobbitAddress = hobbit.getAddress(); hobbitAddress.setCountry("Isengard"); hobbitAddress.setCity("Saruman tower"); List<String> hobbitStuff = hobbit.getStuff(); hobbitStuff.remove("Ring of Power"); hobbitStuff.remove("Sword"); System.out.println(); System.out.println("Immutability has been hacked!"); System.out.println(); System.out.println("Hobbit country: " + hobbit.getAddress().getCountry()); System.out.println("Hobbit city: " + hobbit.getAddress().getCity()); System.out.println("Hobbit stuff: " + hobbit.getStuff()); } }
Saída:
Hobbit country: Hobitton
Hobbit city: Shire
Hobbit stuff: [Espada, Anel do Poder]
Imutabilidade foi hackeada!
Hobbit country: Isengard
Cidade de Hobbit: Saruman tower
Hobbit stuff: []
Então, vai com outro passo a ser feito:
5. executando clonagem do objeto mutável não-primitivo retornado em métodos getter
import java.util.ArrayList; import java.util.List; public final class Hobbit { private String name; private Address address; private List<String> stuff; // all args constructor: public Hobbit(String name, Address address, List<String> stuff) { this.name = name; this.address = new Address(address.getCountry(), address.getCity()); this.stuff = new ArrayList<>(stuff); } // getters: public String getName() { return name; } public Address getAddress() { return new Address(address.getCountry(), address.getCity()); } public List<String> getStuff() { return new ArrayList<>(stuff); } }
Agora a Hack
saída é:
Hobbit country: Hobitton
Hobbit city: Shire
Hobbit stuff: [Espada, Anel do Poder]
Imutabilidade foi hackeada!
Hobbit country: Hobitton
Hobbit city: Shire
Hobbit stuff: [Espada, Anel do Poder]
Há mais uma etapa opcional a ser executada, mas se todos os outros pré-requisitos forem atendidos, isso não é necessário. De qualquer forma, vamos mencionar isso:
6. marcando todos os campos de classe como final (opcional)
import java.util.ArrayList; import java.util.List; public final class Hobbit { private final String name; private final Address address; private final List<String> stuff; // all args constructor: public Hobbit(String name, Address address, List<String> stuff) { this.name = name; this.address = new Address(address.getCountry(), address.getCity()); this.stuff = new ArrayList<>(stuff); } // getters: public String getName() { return name; } public Address getAddress() { return new Address(address.getCountry(), address.getCity()); } public List<String> getStuff() { return new ArrayList<>(stuff); } }
E é isso, estamos prontos – nossa Hobbit
classe é imutável agora.
Vamos resumir os passos:
- remover setters;
- adicionando todo o construtor args;
- marcando a classe de modo
final
a protegê-la de ser estendida; - inicializar todos os campos mutáveis não primitivos via construtor executando uma cópia profunda;
- executando clonagem do objeto mutável não-primitivo retornado nos métodos getter;
- marcando todos os campos de classe como
final
(etapa opcional).
Claro, existem outras maneiras de tornar a classe imutável (por exemplo, usando o padrão Builder), mas está fora do escopo da história atual.