quarta-feira, 15 de março de 2023

Sobrecarga de Métodos e Tipos Genéricos em Java

Veja nesse artigo como e quando utilizar sobrecarga de métodos e tipos genéricos e quais problemas cada um ajuda a resolver.

SUPORTE AO ALUNO

ANOTAÇÕES

FAVORITAR

CONCLUÍDO

11GOSTEI

11

Suporte ao aluno

Anotar

Marcar como concluído

Código fonte

Artigos

Java

Introdução: Sobrecarga de Métodos e Tipos Genéricos em Java

Introdução

O java nos permite trabalhar tanto com métodos sobrecarregados como com tipos genéricos, vamos ver qual a utilidade de ambos sempre mostrando primeiro o problema para depois apontar a solução. Será mostrado a utilização dos métodos sobrecarregados e dos tipos genéricos na resolução do seu respectivo problema.

Vamos ao problema:

Ao desenvolver uma aplicação surge a necessidade de se criar um método que receba por parâmetros o nome do cliente, endereco, bairro, cidade e estado. Então foi criado o método como abaixo:

public void setDadosPessoais(String nome, String endereco, 

String bairro, String cidade, String estado)

{

// codigo do método

}

Ao longo do projeto é percebido que nem sempre será necessário a inclusão do endereco, do bairro, da cidade e do estado. Então é solicitado ao programador que fosse feito um método que recebesse apenas o nome e o sobrenome do cliente. Então foi criado o método:

public void setDadosPessoaisNome(String nome)

{

// codigo do método

}

Agora o sistema está aceitando a inserção de todos os dados pessoais, mas também aceita apenas o nome e o sobreNome do cliente. Pouco depois surgiu a necessidade de um método que recebesse apenas o nome, o endereço e o bairro. Então foi criado o método:

public void setDadosPessoaisNomeEnderecoBairro(String nome,String endereco,String bairro)

{

// codigo do método

}

A medida que as solicitações aconteciam os métodos iriam crescendo e além de crescendo os nomes estavam ficando cada vez maiores. Para cada situação diferente um novo método e um nome diferente, como o exemplo abaixo:

•    nome - setDadosPessoaisNome

•    nome, endereco - setDadosPessoaisNomeEndereco

•    nome, endereco, bairro - setDadosPessoaisNomeEnderecoBairro

•    nome, bairro - setDadosPessoaisNomeBairro

•    nome, cidade - setDadosPessoaisNomeCidade

•    nome, estado - setDadosPessoaisNomeEstado

•    endereco, bairro - setDadosPessoaisEnderecoBairro

•    cidade, estado - setDadosPessoaisCidadeEstado

•    endereco - setDadosPessoaisEndereco

•    bairro - setDadosPessoaisBairro

•    bairro, cidade - setDadosPessoaisBairroCidade

•    cidade - setDadosPessoaisCidade


     Você pode observar que a medida que forem surgindo mais parâmetros para o método o nome tende a ficar cada vez mais longo e isso além de não ficar nada bonito ainda pode fazer gerar confusão , um monte de métodos com vários nomes diferentes um maior que o outro.

     Depois de criado todas as opções pedidas, apareceu um outro problema, foi solicitado que as variareis nome, endereco, bairro, cidade e estado fossem iniciadas no momento que a classe fosse instanciada e que o sistema teria que dar as mesmas opções que os métodos deram.

    E agora? Como fazer isso se o construtor deve ter o mesmo nome da classe?

    Veremos como resolver esse problema utilizando métodos   sobrecarregados.

Sobrecarregamento de Métodos

    Vimos que a medida que as solicitações iam ocorrendo, o nosso problema só aumentaria. Vamos ver a solução para a infinidade de nomes diferentes que precisamos criar.

            O java permite que você crie métodos com o mesmo nome, desde que eles tenham parâmetros diferentes. O sobrecarregamento de métodos é muito utilizado pelos construtores que precisam ter o mesmo nome da classe. No nosso exemplo estamos cadastrando dados para o cliente, então vamos utilizar o nome Cliente para a classe, conforme foi solicitado precisamos iniciar as variareis no momento que a classe for instanciada. Vamos ver um exemplo de definição de classe e depois instanciar a mesma para entender melhor o que acontece quando uma classe é instanciada.

public class Clientes

{

private String nome;

private String endereco;

private String bairro;

private String cidade;

private String estado;


public void setNome(String nome)

{

this.nome = nome;

}

public String getNome()

{

return nome;

}


public void setEndereco(String endereco)

{

this.endereco = endereco;

}

public String getEndereco()

{

return endereco;

}

public void setBairro(String bairro)

{

this.bairro = bairro;

}

public String getBairro()

{

return bairro;

}


public void setCidade(String cidade)

{

this.cidade = cidade;

}


public String getCidade()

{

return cidade;

}

public void setEstado(String estado)

{

this.estado = estado;

}


public String getEstado()

{

return estado;

}

}

A classe cliente declara cinco variáveis do tipo String, nenhum construtor foi utilizado explicitamente, e nenhuma das variareis foi iniciada. Quando uma variável do tipo String é declarada e não é iniciada explicitamente, ela recebe o valor de null. Como estamos trabalhando com a classe String isso não é um problema, mas em alguns casos a falta de inicialização de variáveis gera uma exception. Vamos instanciar a classe clientes e imprimir o valor das variareis para entendermos melhor:

public class ClientesTest 

{

public static void main(String[] args) 

{

Clientes clientes = new Clientes();

System.out.println(clientes.getNome());

}

}

Ao chamar o método getNome() da classe Clientes é impresso o valor null. Vamos ver o que acontece quando chamamos a classe StringBuilder sem inicializá-la:

public class ClientesTest 

{

public static void main(String[] args) 

{

Clientes clientes = new Clientes();

System.out.println(clientes.getNome());

/* Obtém uma referencia do StringBuilder e adiciona o string “Teste”*/

clientes.getBuilder().append(“teste”); 

}

}



 Quando a classe ClientesTest adiciona o string “Teste” através do método append() é gerada a seguinte exceção:

Exception in thread "main" java.lang.NullPointerException at 

br.com.MetodosSobrecarregados.Problema.ClientesTest.main(ClientesTest.java:11)


A exceção ocorre porque a variável que passa a referencia do StringBuilder para o método getBuilder() não foi inicializada.

        Para evitar esse tipo de problema utilizamos os construtores que são chamados no momento em que a classe é instanciada. Com os métodos sobrecarregados podemos definir as variareis que precisam ser inicializadas no momento em que a classe é instanciada. Vamos criar um construtor que inicie apenas a variável array da classe Clientes:

public Clientes()

{

/*variável é inicializada no momento em que a classe Clientes for instanciada.*/

clientesBuilder = new StringBuilder(); 

}


 Ao executarmos o sistema não será mais mostrada a exceção. Apenas será impresso o valor null, devido ao nome não ter sido inicializado. Até o momento não vimos ainda o sobrecarregamento de métodos, vamos ver agora oferecendo para a classe ClientesTest a opção de inicializar a variável nome juntamente com a variável array:


public Clientes() //1ª opção do construtor para a classe Clientes

{

clientesBuilder = new StringBuilder();

}


public Clientes(String nome) //2ª opção do construtor para a classe Clientes

{

clientesBuilder = new StringBuilder();

setNome(nome);

}



    No exemplo acima temos o sobrecarregamento de método no construtor Clientes, o construtor pode ser chamado sem parâmetros ou com o parâmetro nome. Vejamos como fica a classe ClientesTest utilizando o construtor com o parâmetro nome:



public class ClientesTest 

{

public static void main(String[] args) 

{

//O nome Felipe é passado como parametro

Clientes clientes = new Clientes("Felipe");


System.out.println(clientes.getNome());

}

}

Ao executar a classe ClientesTest, é impresso o nome Felipe ao invés de null, ou seja, a variável nome foi inicializada corretamente. O nome do construtor é igual ao nome da classe, podemos fazer a mesma coisa para os métodos, evitando uma infinidade de nomes diferente para um mesmo método, o que precisa ser diferente são apenas os parâmetros passados. Vejamos um exemplo de sobrecarregamento para o método setDadosPessoais. Será oferecido 4 opções para o método:

    

•    Atribuir valor ao nome do cliente.

•    Atribuir valor ao nome o cliente e ao endereço do cliente.

•    Atribuir valor ao nome, endereco e cidade do cliente.

•    Atribuir valor ao nome, endereco, cidade e estado do cliente.


           Vamos ao código:

//Opção com parametro nome

public void setDadosPessoais(String nome)

{

setNome(nome);

}

//Opção com parametro nome e endereco

public void setDadosPessoais(String nome,String endereco)

{

setNome(nome);

setEndereco(endereco);

}


//Opção com parametro nome, endereco e cidade

public void setDadosPessoais(String nome,String endereco,String cidade)

{

setNome(nome);

setEndereco(endereco);

setCidade(cidade);

}


//Opção com parametro nome, endereco,cidade e estado

public void setDadosPessoais(String nome,String endereco,String cidade,String estado)

{

setNome(nome);

setEndereco(endereco);

setCidade(cidade);

setEstado(estado);

}

 Os problemas citados anteriormente foram resolvidos utilizando os métodos sobrecarregados. Os códigos mostrados nesse artigo estão disponíveis para download. Existe um download para cada passo explicado aqui.

            Embora os métodos sobrecarregados ajudem bastante ainda teremos muitos métodos, se quisermos oferecer soluções para todas as possíveis inicializações, só para a inicialização de uma variável teríamos 4 métodos e o número só iria crescendo na medida que iriam crescendo as opções.    

Entendendo os Tipos Genéricos

    Em java podemos utilizar um método utilitário para executar uma tarefa em comum independente da variável passada como parâmetro, imagine uma situação em que o método precise passar um parâmetro e imprimir na tela uma resposta de acordo com o parâmetro utilizado. Poderíamos utilizar os métodos sobrecarregados para impedir a criação de nomes distintos como abaixo:

1º problema:

public static void configuraValor(String valor)

{

System.out.println(“String impressa com êxito.”);

}


public static void configuraValor(int valor)

{

System.out.println(34);

}


public static void configuraValor(double valor)

{

System.out.println(32.23);

}

 Podemos observar que os nomes dos métodos foram reduzidos a um só, o que mudam são os parâmetros passados, os métodos compartilham entre-si o método System.out.println() que imprime o tipo passado como parâmetro. Como utilizamos os métodos sobrecarregados, teremos três métodos, uma para o tipo String, outro para o tipo int e outro para o tipo double. O problema é que não é aproveitado o que é comum entre os métodos, a medida que forem surgindo mais tipos terão que ser feitos novos métodos sobrecarregados.  Vamos ver primeiro um outro exemplo para depois a solução para os dois problemas.


2º problema:


    Imagine uma classe chamada Operacao que instancie 5 classes: Clientes, Funcionarios, Fornecedores, Voluntarios e Patrocinadores. Todas as classes possuem um método chamado getInformacao que retorna os dados pessoais das mesmas.


new Clientes().getInformacao();

new Funcionarios().getInformacao();

new Fornecedores().getInformacao();

new Voluntarios().getInformacao();

new Patrocinadores().getInformacao();


    Cada classe vai retornar um número especifico de informações, podemos perceber que todas as classes tem um método getInformacao(), a classe Operacao instancia cada classe para receber essas informações, tratar essas informações e imprimir para o usuário final. Para as 5 classes precisariamos ter 5 métodos sobrecarregados, visto que a classe Operação vai receber as informações tratar os dados e imprimir o resultado. Vejamos um exemplo:


public getInformacao(Clientes clientes)

{

String informacao = clientes.getInformacao();


if (informacao != null)

{

System.out.println(informacao);

}

}


public getInformacao(Funcionarios funcionarios)

{

String informacao = clientes.getInformacao();


if (informacao != null)

{

System.out.println(informacao);

}

}



    Os outros métodos sobrecarregados encontram-se no exemplo disponível para download, existem 5 métodos um para cada classe. Temos alguns pontos a serem obervados nesse exemplo.


•   Os códigos se repetem nos métodos sobrecarregados.

•   Foi preciso criar métodos para o mesmo número de classes que é preciso trabalhar, se for preciso instanciar 5 classes, são necessários 5 métodos, se for preciso 50 classes são necessários 50 métodos.

•    A medida que forem crescendo o número de métodos, o sistema fica mais suscetível a erros, imagine que você tenha 50 classes e você precise alterar seu código no método getInformacao da classe Operacao, você precisaria fazer 50 alterações, uma em cada método sobrecarregado. 

    Podemos resolver esses problemas utilizando os tipos genéricos. Veremos a solução para os dois problemas mostrados aqui. Vamos a resolução do primeiro problema.

Solução para o 1º problema:


  public static T void configuraValorG(T valor)

{

if (valor instanceof String)

{

valor = (T)"É uma string";

}

else if (valor instanceof Integer)

{

  valor = (T)(Integer)20;

}

else if (valor instanceof Double)

{

valor = (T)(Double)5.90;

}

System.out.println(valor);

}

 Inicialmente tínhamos três métodos sobrecarregados, um para cada tipo de dados. Resolvemos esse problema transformando os três métodos em um só. Vejamos a explicação do novo método genérico.


Linha 1: Para utilizar os tipos sobre carregados preciso passar uma variável entre o sinal de maior e menor antes do tipo de retorno, foi utilizada a variável T, que entre os sinais ficou assim: <T>. Para o parâmetro valor foi passado como tipo a letra  T, mesma letra passada como variável entre os sinais de maior e menor. Se você utilizar a letra E como variável o cabeçalho do método ficaria assim:

public static E void configuraValor(E valor).


Linha 3: Foi utilizado o operador instanceof para saber se o tipo passado é do tipo String.

Linha 5: Foi atribuído a variável valor uma String qualquer, no exemplo foi passado “É uma string”. Podemos observar que é necessário fazer um Cast para T, isso é necessário para que o compilador não gere erros, já que os tipos genéricos são passados em tempo de execução.


Linha 7:  Da mesma forma da linha 3, foi utilizado o operador instanceof , porém não foi utilizado o tipo int, devido aos tipos genéricos só suportarem classes não é permito passar variáveis primitivas (int, double, boolean, float, …), para a variável int foi utilizada a classe Integer.


Linha 9: Além do Cast para a variável T, ainda é feito um Cast para a classe Integer, devido a possibilidade de receber como parâmetro um int ao invés de um Integer. Caso eu não queira tratar isso no método, eu tenho que ter certeza que será passado como parâmetro uma variável do tipo Integer.


Linha 16: É passado ao método System.out.println() a variável valor, onde seu tipo é definido em tempo real.


    Com os tipos genéricos reduzimos os métodos sobrecarregados a um método só, dessa forma podemos compartilhar o método System.out.println(). Qualquer alteração nesse método, como a impressão da variável valor antes do mesmo será feita uma vez só, ao contrário dos métodos sobrecarregados que teriam que ser feitos em cada método específico, podendo gerar erros entre os métodos.

    Só para entendermos melhor, vamos ao mesmo método utilizando como variável de tipo a letra E ao invés de T :


public static E void configuraValor(E valor)

{

if (valor instanceof String)

{

valor = (E)"É uma string";

}

else if (valor instanceof Integer)

{

valor = (E)(Integer)20;

}

else if (valor instanceof Double)

{

valor = (E)(Double)5.90;

}

System.out.println(valor);

}

 Como foi dito anteriormente, caso deseje alterar a letra entre os sinais de maior e menor, não esqueça de alterar no corpo do método também.


Solução para o 2º problema:


  public T void  getInformacao(T valores)

{

if (((Informacoes) valores).getInformacao() != null)

{                      

System.out.println(((Informacoes)valores).getInformacao());

}

}

 O que muda nessa resolução é o Cast utilizando a Interface Informacoes para obter o método comum entre todas as classes. Na linha 3 utilizamos a variável genérica e verificamos se existe algum valor para a classe. O parâmetro passado é uma classe instanciada. Independente se seja 1, 5 ou 50 classes o método continua sendo 1 e, qualquer alteração no método genérico é valido  em todas as classes. O código completo está disponível para download.


Explicação sobre os arquivos para download


    Os códigos explicados neste arquivos encontram-se disponíveis para download, os mesmos foram feitos no eclipse, para utilizá-los basta importar o projeto Artigos.


            Existem 4 pacotes:

•    br.com.metodossobrecarregados.problema: referente a não utilização dos métodos sobrecarregados

•    br.com.metodossobrecarregados.solucao: referente a resolução do problema utilizando os métodos sobrecarregados

•    br.com.tiposgenericos.primeiroproblema: referente ao problema e a solução mostrada no artigo para o primeiro problema

•    br.com.tiposgenericos.segundoproblema: referente ao problema e a solução mostrada no artigo para o segundo problema


    Os códigos podem ser lidos diretamente (fora do eclipse) através de qualquer editor de textos, acesse a pasta Artigos > src > br > com e escolha o pacote:

•    1º pasta: métodos sobrecarregados → contém os códigos para o pacote de métodos sobrecarregados, contem duas subpastas:

•    problema: contém os códigos com a não utilização dos métodos sobrecarregados

•    solução: contém a solução utilizando os métodos sobrecarregados

•    2º pasta: tipos genéricos → contém os códigos para o pacote de tipos genéricos, contem duas subpastas:

•    primeiro problema: contém os códigos para o problema e a solução do primeiro problema do artigo

•    segundo problema: contém os códigos para o problema e a solução do segundo problema do artigo


Testando as aplicações


    Para testar os pacotes, é preciso executar as classes principais (main), segue a lista das classes executoras para cada pacote:

•    br.com.metodossobrecarregados.problema: ClientesTest.java

•    br.com.metodossobrecarregados.solucao: ClientesTest.java

•    br.com.tiposgenericos.primeiroproblema: ExecutaValores.java

•    br.com.tiposgenericos.segundoproblema: Duas classes principais, uma para o problema e outra para a solução. Seguem:

•    Problema: ExecutaOperacaoMetodosSobrecarregados.java

•    Solução: ExecutaOperacaoTiposGenericos.java


    Aqui finalizamos o artigo sobre carregamento de métodos e tipos genéricos. Até a próxima.

terça-feira, 14 de março de 2023

O Reflexo da Imutabilidade no Código Limpo

 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.


Referência: https://www.devmedia.com.br/o-reflexo-da-imutabilidade-no-codigo-limpo/30697