Imutabilidade, ou Objetos imutáveis, em linhas gerais, são objetos que, uma vez instanciados, não podem ter seus estados internos modificados. Na API Java temos alguns exemplos conhecidos, como a classe String e as classes Wrappers (Integer, Double, etc).

A seguir veremos alguns dos benefícios de se utilizar essa abordagem.

Confira os Cursos de Java da DevMedia

Evitando Efeitos colaterais

Observe na Listagem 1 um exemplo de objeto mútavel.

Listagem 1. Objeto Mutável e Efeito Colateral

  package br.com.devmedia.imutabilidade;
   
  public class Exemplo1 
  {
      public static class Texto {
          
          private String linha;
   
          public String getLinha() {
              return linha;
          }
   
          public void setLinha(String linha) {
              this.linha = linha;
          }
      }    
      
      public static void main( String[] args ) {
          Texto texto = new Texto();
          texto.setLinha("TESTE");
          
          Tela.imprimir(texto);
          
          System.out.println("texto.getLinha() = " + texto.getLinha());
      }
  }

Analisando o código acima, o que podemos afirmar sobre a saída do programa?

  • Será impresso "texto.getLinha() = TESTE"
  • Não sei.

Sem ter em mãos o código fonte da classe Tela é impossível saber qual será o valor do atributo linha, pois o método imprimir pode ou não invocar o método setLinha do objeto recebido como argumento. Temos, então, infinitas possibilidades de saída do programa.

Agora, na Listagem 2 temos o mesmo exemplo, mas com o objeto imutável.

Listagem 2. Objeto Imutável

  package br.com.devmedia.imutabilidade;
   
  public class Exemplo1 
  {
      public static final class Texto {
          
          private final String linha;
          
          public Texto(String linha) {
              this.linha = linha;
          }
   
          public String getLinha() {
              return linha;
          }
      }    
      
      public static void main( String[] args ) {
          Texto texto = new Texto("TESTE");
          
          Tela.imprimir(texto);
          
          System.out.println("texto.getLinha() = " + texto.getLinha());
      }
  }

Dessa vez, a classe Texto é imutável. Veja o que mudou em relação ao código da Listagem 1:

  • Se a execução chegar na linha System.out.println("texto.getLinha() = " + texto.getLinha()), a saída agora é prevísivel, pois sempre será "texto.getLinha() = TESTE", independente do código escrito no método imprimir. (*)
  • Não é necessário ter em mãos o código da classe Tela, pois o efeito colateral foi eliminado. (*)
  • Não existem mais múltiplas possibilidades de saída para a linha do System.out.

Nota: O método imprimir pode ter uma chamada a System.exit ou lançar uma exceção não-verificada, por isso a execução do programa pode não chegar ao System.out.

Ao eliminar o efeito colateral que poderia ser produzido pelo método imprimir, tornamos o programa muito mais prevísivel e simples. Isso só foi possível porque estabelecemos uma invariante: a classe Texto é imutável.

Minimizar pontos de mudança e aplicar invariantes torna o programa muito mais seguro, pois diminui a probabilidade de alterações feitas por um programador afetarem o trabalho de outro, além de simplificar os testes e tornar o comportamento do sistema mais prevísivel.

Bom Cidadão (Good Citizien)

A imutabilidade favorece a aplicação do padrão Good Citizien, que prega que um objeto deve manter um estado consistente em qualquer instante do tempo. Vejamos a classe mutável Texto da Listagem 1, mas com algumas modificações, conforme a Listagem 3.

Listagem 3. NullPointerException

  package br.com.devmedia.imutabilidade;
   
  public class Exemplo1 {
   
      public static class Texto {
   
          private String linha;
   
          public String getLinha() {
              return linha;
          }
   
          public void setLinha(String linha) {
              this.linha = linha;
          }
      }
   
      public static void main(String[] args) {
          Texto texto = new Texto();
          System.out.println("texto.getLinha() = " + texto.getLinha().toLowerCase());        
      }
  }

Se executarmos esse código, receberemos uma NullPointerException, pois o atributo linha não foi inicializado e por isso o valor default é nulo. Geralmente, nos esquecemos de inicializar alguns dos atributos do nosso objeto, e recebemos esse tipo de exceção.

Com um objeto imutável, somos forçados a fornecer valores para os atributos já na instanciação do objeto. Veja um exemplo na Listagem 4.

Listagem 4. Bom Cidadão

  package br.com.devmedia.imutabilidade;
   
  public class Exemplo1 
  {
      public static final class Texto {
          
          private final String linha;
          
          public Texto(String linha) {
              if(linha == null) {
                   this.linha = "";
              } else {
                   this.linha = linha;
              }
          }
   
          public String getLinha() {
              return linha;
          }
      }    
      
      public static void main( String[] args ) {
          Texto texto = new Texto(null);        
          System.out.println("texto.getLinha() = " + texto.getLinha().toLowerCase());
      }
  }

Agora não teremos mais o problema do NullPointerException. Apesar do programa rodar sem gerar exceção, o trecho new Texto(null), não é muito elegante. Pode-se usar Static Method Factory para tornar a coisa melhor, conforme mostra a Listagem 5.

Nota: Caso tenha alguma dúvida sobre exceções em java, não deixe de dar uma conferida nesse artigo.

Listagem 5. Static Method Factory

  package br.com.devmedia.imutabilidade;
   
  public class Exemplo1 
  {
      public static class Texto {
          
          private String linha;
          
          // Construtor torna-se privado
          private Texto(String linha) {
              this.linha = linha;
          }
          
          public static Texto make(String linha) {
              if(linha == null) {
                  return make();
              } else {
                  return new Texto(linha);
              }
          }
          
          public static Texto make() {
              return new Texto("");
          }       
   
          public String getLinha() {
              return linha;
          }
      }    
      
      public static void main( String[] args ) {
          Texto texto = Texto.make();
          System.out.println("texto.getLinha() = " + texto.getLinha().toLowerCase());
      }
  }

Com um pouco mais de trabalho, poderíamos aplicar o Padrão Objeto Nulo e tornar ainda mais elegante o código, mas isso fica para um próximo artigo.

Para não ferir o SRP (Single Responsibility Principle), a parte de criação poderia ser delegada para uma Factory. No caso de ter uma classe com muitos atributos, considere o uso do Padrão Builder.

Thread Safety

A programação concorrente é, provavelmente, um dos aspectos mais complexos que um programador pode lidar. Por isso, a imutabilidade é muito bem vinda nessa área.

Objetos imutáveis podem ser compartilhados por várias threads de forma segura, pois uma vez criados, não serão mais alterados. O uso de objetos imutáveis pode ajudar a evitar o uso de esquemas custosos de sincronização/bloqueio, diminuindo a complexidade do código concorrente e a quantidade de erros de programação.

Encadeamento de métodos

Objetos imutáveis permitem o uso do método chaining (http://martinfowler.com/dslCatalog/methodChaining.html). A classe String, presente na Listagem 6 é o bom exemplo disso.

Listagem 6. Method Chaining com a classe String

  package br.com.devmedia.imutabilidade;
   
  public class Exemplo3 {
      
      public static void main(String... args) {
          String texto = "Hello World";
          
          System.out.println(
             texto.toLowerCase()
                  .concat("!")
                  .replace('o', 'O')
                  .toString()
                  );
      }
  }

Cada método chamado no exemplo (no caso toLowerCase(), concat() e replace()) retorna o próprio objeto que fez a chamada (this), permitindo que haja o encadeamento.

A API do JodaTime é um outro exemplo faz uso intensivo de imutabilidade e encadeamento de métodos.

Facilitar cópias defensivas

Quando trabalhamos com objetos imutáveis, a criação de cópias defensivas torna-se mais fácil, uma vez que basta apenas passar a referência do objeto imutável. Vejamos um exemplo na Listagem 7.

Listagem 7. Cópias defensivas

  package br.com.devmedia.imutabilidade;
   
  import java.util.Date;
   
  public class Produto {
   
      private Integer codigo;       // imutavel 
      private String descricao;     // imutavel
      private Date dataExpiracao;   // mutavel
   
      public Produto criarCopiaDefensiva() {
          Produto produto = new Produto();
          produto.codigo = this.codigo;
          produto.descricao = this.descricao;
          produto.dataExpiracao = (Date) this.dataExpiracao.clone();
          return produto;
      }
  }

O método criarCopiaDefensiva cria um novo objeto Produto com as mesmas características do objeto original. Veja que, para os atributos imutáveis, apenas foi necessário passar a própria referência, ou seja, o objeto principal e o clone acabam compartilhando as mesmas instâncias de código e descrição, economizando memória.

Já o objeto dataExpiracao, por ser mutável, exigiu um código mais complexo para se obter a cópia.

Agora imagine que, ao invés do objeto Date, no lugar existesse um objeto X, composto por vários objetos, que por sua vez são compostos por outros. Se o objeto X é imutável, por mais complexo que ele seja internamente, na hora de fazer a cópia, basta apenas devolver a própria referência.

Por isso objetos imutáveis não precisam implementar o método clone(). Veja um exemplo na Listagem 8.

Listagem 8. Outro exemplo de cópia defensiva

  package br.com.devmedia.imutabilidade;
   
  // Nao eh imutavel...
  public class Produto {
   
      private Integer codigo;       // imutavel 
      private String descricao;     // imutavel
      private X dataExpiracao;      // imutavel
   
      public Produto criarCopiaDefensiva() {
          Produto produto = new Produto();
          produto.codigo = this.codigo;
          produto.descricao = this.descricao;
          produto.dataExpiracao = this.dataExpiracao;
          return produto;
      }
   
      .... outros métodos
  }
   
  package br.com.devmedia.imutabilidade;
   
  import java.util.Date;
   
  // Imutavel
  public final class X {
      private final Date dataExpiracao;   // mutavel
      
      public X() {
          dataExpiracao = new Date();
      }
      
      public Date getDataExpiracao() {
          return (Date)dataExpiracao.clone();
      }
  }
  

Cache

Objetos imutáveis podem ser cacheados e compartilhados por qualquer classe da aplicação. A classe Boolean, por exemplo, possui um atributo estático public:

public static final Boolean TRUE = new Boolean(true);

Por ser imutável, a variável TRUE pode ser compartilhada por qualquer classe, resultando em economia de memória. (ao invés de fazer new Boolean(true), usar Boolean.TRUE).

O próprio Pool de Strings do Java e as classes Wrappers são exemplos de cache, justamente por elas serem imutáveis.

Vejamos um outro exemplo de cacheamento, usando o Padrão Flyweight, conforme a Listagem 9.

Listagem 9. Padrão Flyweight

  package br.com.devmedia.imutabilidade;
   
  // Classe imutavel Time de Futebol
  public final class Time {    
      
      private final String nome;
      
      public Time(String nome) {
          this.nome = nome;
      }
   
      public String getNome() {
          return this.nome;
      }       
  }
   
   
  package br.com.devmedia.imutabilidade;
   
  import java.util.HashMap;
  import java.util.Map;
   
  // Flyweight Pattern TimeFactory
  public class TimeFactory {
   
      // Map com os times mais acessados
      private static final Map<String, Time> times = new HashMap<String, Time>();
      
      private static final Time CORINTHIANS = new Time("CORINTHIANS");
      private static final Time PALMEIRAS = new Time("PALMEIRAS");    
      private static final Time SANTOS = new Time("SANTOS");
      private static final Time SAO_PAULO = new Time("SAO PAULO");
      
      static {
          times.put("CORINTHIANS", CORINTHIANS);
          times.put("PALMEIRAS", PALMEIRAS);
          times.put("SANTOS", SANTOS);
          times.put("SAO_PAULO", SAO_PAULO);        
      }
      
      public static Time getTimeByName(String name) {
          // Se for um dos times mais acessados
          if(times.containsKey(name)) {
              return times.get(name);
          } else {            
              return new Time(name);
          }
      }    
  }
   
   
  package br.com.devmedia.imutabilidade;
   
  public class TimeTeste {
   
      public static void main(String[] args) {
          Time time1 = TimeFactory.getTimeByName("SANTOS");
          System.out.println(time1);        
          Time time2 = TimeFactory.getTimeByName("SANTOS");
          System.out.println(time2);
          Time time3 = TimeFactory.getTimeByName("CORINTHIANS");
          System.out.println(time3);
          Time time4 = TimeFactory.getTimeByName("PONTE PRETA");
          System.out.println(time4);
          Time time5 = TimeFactory.getTimeByName("ITUANO");
          System.out.println(time5);
          Time time6 = TimeFactory.getTimeByName("BOTAFOGO");
          System.out.println(time6);
          Time time7 = TimeFactory.getTimeByName("PALMEIRAS");
          System.out.println(time7);
      }
  }

Nesse exemplo foi aplicado o Padrão Flyweight para cachear os times mais comumente acessados pela aplicação. Com isso haverá um ganho de memória.

Obviamente, os objetos só podem ser compartilhados com qualquer outra parte da aplicação porque a classe Time é imutável.

Tiny Types

Objetos imutáveis são candidatos naturais para representarem tipos (Integer, Boolean, String, etc) e Values Objects* (CPF, RG, Dinheiro, Cor, Descrição, etc).

Ao representar os atributos como classes, ao invés de usar somente tipos primitivos e a classe String, obtemos um modelo mais fortemente tipado e rico.

Emulação de enums em versões Java abaixo de 1.5

Muitas empresas, em pleno século XXI, usam versões Java abaixo da versão 1.5, onde o Enum não está disponível. Mas para esses casos, é possível emular tal comportamento mesclando imutabilidade com atributos estáticos e/ou usando FlyWeight Pattern. Veja a Listagem 10.

Listagem 10. Emulando Enumeration

  package br.com.devmedia.imutabilidade;
   
  public final class StatusProposta {
      
      private final String status;
      
      public static StatusProposta APROVADO = new StatusProposta("APROVADA");
      public static StatusProposta REPROVADO = new StatusProposta("REPROVADO");
      public static StatusProposta PENDENTE = new StatusProposta("PENDENTE");
      public static StatusProposta REANALISE = new StatusProposta("REANALISE");
             
      // Basta deixar o construtor privado
      private StatusProposta(String status) {
          this.status = status;
      }    
   
      public String getStatus() {
          return this.status;
      }
   
      // Outros métodos. Sem métodos setter's, claro... :-)
  }

Ao invés de usar constantes de String, int, etc, usa-se um tipo (classe), que só pode assumir os valores definidos nos campos estáticos, uma vez que o construtor é privado.

Podemos implementar o Padrão FlyWeight para cachear todas as opções, ou as mais usadas, como faz a classe Integer ou a nossa classe Time nos exemplos anteriores. Limitações

Alguns cenários que podem impossibilitar o uso da imutabilidade:

  • Presença de API's que exigem classes no padrão JavaBeans;
  • Sistemas onde há criação maçica de objetos, na qual esquemas de cache resultam em pouco ganho de desempenho em relação ao todo.

A imutabilidade deve ser um dos itens da caixa de ferramenta de qualquer programador, e ser aplicada o quanto for possível.

Obrigado pessoal, e até a próxima.

Referências: