Como criar uma classe imutável em Java?

Em Java, imutabilidade significa que, uma vez criado um objeto, seu estado interno não pode ser alterado. Classes imutáveis ​​​​em Java oferecem muitas vantagens, como segurança de thread, depuração fácil e tudo mais. Em Java todos os classes de wrapper (como Integer Boolean Byte Short) e a classe String é imutável. Também podemos criar nossa própria classe imutável.

Neste artigo vamos aprender:

  • O que significa imutabilidade
  • Por que é útil
  • Como criar nossa própria classe imutável
  • Por que a cópia profunda é importante
  • Quais são as limitações que os tipos de registro Java têm

O que é uma classe imutável?

Uma classe imutável é uma classe cujos objetos não podem ser alterados depois de criados. Se fizermos alguma modificação, isso resultará em um novo objeto. Este método é usado em aplicativos simultâneos.

Regras para criar uma classe imutável

  • A classe deve ser declarada como final para que as classes filhas não possam ser criadas.
  • Os membros de dados da classe devem ser declarados privado para que o acesso direto não seja permitido.
  • Os membros de dados da classe devem ser declarados como final para que não possamos alterar seu valor após a criação do objeto.
  • Um construtor parametrizado deve inicializar todos os campos executando um cópia profunda para que os membros de dados não possam ser modificados com uma referência de objeto.
  • A cópia profunda de objetos deve ser executada nos métodos getter para retornar uma cópia em vez de retornar a referência real do objeto.

Observação : Não deve haver setters ou, em termos mais simples, não deve haver opção para alterar o valor da variável de instância.


Exemplo: implementação de classe imutável

Aluno.java

Java
   // Java Program to Create An Immutable Class   import     java.util.HashMap  ;   import     java.util.Map  ;   // declare the class as final   final     class   Student     {      // make fields private and final      private     final     String     name  ;      private     final     int     regNo  ;      private     final     Map   <  String       String  >     metadata  ;      // initialize all fields via constructor      public     Student  (  String     name       int     regNo       Map   <  String       String  >     metadata  )     {      this  .  name     =     name  ;      this  .  regNo     =     regNo  ;      // deep copy of mutable object (Map)      Map   <  String       String  >     tempMap     =     new     HashMap   <>  ();      for     (  Map  .  Entry   <  String       String  >     entry     :     metadata  .  entrySet  ())     {      tempMap  .  put  (  entry  .  getKey  ()     entry  .  getValue  ());      }      this  .  metadata     =     tempMap  ;      }      // only provide getters (no setters)      public     String     getName  ()     {      return     name  ;      }      public     int     getRegNo  ()     {      return     regNo  ;      }      // return deep copy to avoid exposing internal state      public     Map   <  String       String  >     getMetadata  ()     {      Map   <  String       String  >     tempMap     =     new     HashMap   <>  ();      for     (  Map  .  Entry   <  String       String  >     entry     :     this  .  metadata  .  entrySet  ())     {      tempMap  .  put  (  entry  .  getKey  ()     entry  .  getValue  ());      }      return     tempMap  ;      }   }   

Neste exemplo, criamos uma classe final chamada Estudante. Possui três membros de dados finais, um construtor parametrizado e métodos getter. Observe que não há método setter aqui. Observe também que não precisamos realizar cópia profunda ou clonagem de membros de dados de tipos de wrapper, pois eles já são imutáveis.

Geeks.java:

Java
   import     java.util.HashMap  ;   import     java.util.Map  ;   public     class   Geeks     {      public     static     void     main  (  String  []     args  )     {      // create a map and adding data      Map   <  String       String  >     map     =     new     HashMap   <>  ();      map  .  put  (  '1'       'first'  );      map  .  put  (  '2'       'second'  );      // create an immutable Student object      Student     s     =     new     Student  (  'GFG'       101       map  );      // accessing data      System  .  out  .  println  (  s  .  getName  ());         System  .  out  .  println  (  s  .  getRegNo  ());         System  .  out  .  println  (  s  .  getMetadata  ());         // try to modify the original map      map  .  put  (  '3'       'third'  );      System  .  out  .  println  (  s  .  getMetadata  ());         // try to modify the map returned by getMetadata()      s  .  getMetadata  ().  put  (  '4'       'fourth'  );      System  .  out  .  println  (  s  .  getMetadata  ());         }   }   

Mesmo depois de modificar o Mapa original ou retornado, o estado interno do objeto Aluno permanece inalterado. Isso confirma o conceito de imutabilidade.

Saída:

 GFG   
101
{1=first 2=second}
{1=first 2=second}
{1=first 2=second}


Limitação de registro Java com campos mutáveis

Java 14 introduzido registro . Esta é uma maneira clara e concisa de definir classes semelhantes imutáveis:

gravar Aluno(Nome da string int regNo Mapa metadados) {}


Mas isso oferece apenas uma imutabilidade superficial. Se o Mapa for modificado externamente, o estado interno do registro muda:

Mapa mapa = novo HashMap <>();

map.put('1' 'primeiro');


Aluno s = novo Aluno(mapa 'ABC' 101);


// Muda o estado interno — NÃO é seguro

map.put('2' 'segundo');

s.metadata().put('3' 'terceiro');

Observação : use registro apenas se todos os campos forem de tipos imutáveis, como String int ou outros registros.