O Que São Tipos Imutáveis Na Programação?

Se você já programa em Java há algum tempo, provavelmente deve ter se deparado com a frase: Strings são imutáveis.
Beleza, mas o que isso significa?

Um tipo imutável não permite a alteração do estado de um objeto criado a partir dele de forma desautorizada.

Vamos ver um tipo “normal” (mutável):

public class Pessoa {

    private String nome;

    public Pessoa() { }

    public Pessoa(String nome) {
        this.nome = nome;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }
}

Até aqui nada demais.
Agora vamos trabalhar com essa clsse:

public static void main(String[] args) {
    Pessoa p1 = new Pessoa();
    Pessoa p2 = new Pessoa("Igor");

    p1.setNome("Victor");
    p2.setNome("João");

    System.out.println(p1.getNome() + ", " + p2.getNome());
}

Saída:
Victor, João

Podemos perceber que o objeto p1 foi criado sem informar o nome da pessoa, dessa forma, o atributo nome deveria ser nulo, mas é Victor.
Já o objeto p2 foi criado com o nome Igor, mas seu nome atual é João.
Bom, até onde sei, são extremamente raros os casos onde uma pessoa muda de nome… Então por que permitir uma alteração desse tipo?

Nesse caso, não faz sentido permitir que o tipo Pessoa seja mutável.
Pois então, como torná-lo imutável? Vamos lá:

Primeiramente, precisamos declarar seu atributo como final.
A palavra reservada final diz que essa variável/atributo receberá uma atribuição apenas uma única vez. Normalmente essa atribuição acontece na declaração. Ex:

final String nome = "Igor";

Isso funciona, mas não faz sentido no nosso caso, pois não queremos que todas as instâncias de Pessoa se chamem Igor.
Então? Como fazemos?
Podemos declarar o atributo como final, sem inicializá-lo como o exemplo acima, mas inicializá-lo pelo construtor:

private final String nome;

public Pessoa(String nome) {
    this.nome = nome;
}

Pronto. Assim bloqueamos qualquer outra atribuição que o atributo nome poderia sofrer.
Mas e o construtor sem argumentos, como fica? Bom, nesse caso, não tem muito o que fazer:

private final String nome;

public Pessoa() {
    nome = null;
}

Não importa se o construtor tem ou não argumentos. O importante aqui é que todos os atributos marcados como final sejam inicializados em todos os construtores.

Ótimo! Temos um atributo final e depois de criada uma instância sabemos que o nome nunca será alterado, mas tem coisa errada aí.
O método setter de nome.

Nessa altura do campeonato ele já não compila mais jutamente pelo fato de fazer:

this.nome = nome;

Uma vez que o atributo nome é final, qualquer tentativa de atribuição gera um erro de compilação.

E agora? Devemos remover o método? Sim!
Essa é uma outra característica de tipos imutáveis: não possuir métodos setter.


Aqui estamos exemplificando um tipo onde queremos que seja 100% imutável, mas é bem comum encontrarmos situaçãoes onde só alguns atributos queremos que sejam imutáveis e os demais não.
Não tem nenhum problema com isso, basta colocar os atributos que deseja ser imutável como final, inicialiá-los pelos construtores (todos) e remover seu método setter.

Podemos citar como exemplo caso o tipo Pessoa tivesse um atributo idade.
Não faria nenhum sentido deixar esse atributo como final, justamente por que nós envelhecemos e nossa idade muda.

Eu, particularmente, ao invés de criar um método setter para esse atributo, criaria algo como:

public void fazAniversario() {
    idade++;
}

Pois num método setter, alugém poderia setar uma idade que antes era 15 para 25 e isso não faz sentido.

Mas estamos fugindo um pouco do foco… Voltando:

Afinal, quais são as vantagens de usar um tipo imutável?

De cabeça lembro de duas principais:

  • Concorrência:
    Nada pior do que um cenário com múltiplas threads mexendo num mesmo objeto :(. Com um tipo imutável você garante que o seu objeto estará sempre bonitinho.

  • Segurança:
    Você sabe exatamente o que tem no seu objeto!


Show, mas até agora exemplificamos um tipo imutável com atributos de tipos imutáveis também.
Como fazemos com atributos de tipos não imutáveis?

Vamos supôr que a classe Pessoa tenha um atirbuto do tipo Endereco, que é mutável:

public class Endereco {

    private String rua;
    private Integer numero;

    public Endereco(String rua, Integer numero) {
        this.rua = rua;
        this.numero = numero;
    }

    public String getRua() {
        return rua;
    }

    public void setRua(String rua) {
        this.rua = rua;
    }

    public Integer getNumero() {
        return numero;
    }

    public void setNumero(Integer numero) {
        this.numero = numero;
    }
}

public class Pessoa {

    private final String nome;
    private final Endereco endereco;

    public Pessoa() {
        nome = null;
        endereco = null;
     }

    public Pessoa(String nome, Endereco endereco) {
        this.nome = nome;
        this.endereco = endereco;
    }

    public String getNome() {
        return nome;
    }

    public Endereco getEndereco() {
        return endereco;
    }
}

Legal, o atributo endereco está final, mas isso não garante nada pois Endereco não é imutável. Vejamos:
Caso alguém invoque o método getter de endereco, essa pessoa pode alterar tanto a rua quanto o número:

public static void main(String[] args) {
    Endereco e = new Endereco("Av. Paulista", 10);
    Pessoa p = new Pessoa("Igor", e);

    Endereco enderecoModificado = p.getEndereco();
    enderecoModificado.setRua("Av. Liberdade");
    enderecoModificado.setNumero(200);

    System.out.println(p.getEndereco().getRua());
}

Saída:
Av. Liberdade

Nesse caso, devemos criar uma cópia do objeto que queremos retornar, para que seja devolvida uma nova referência, deiferente da referência ao objeto original:

public Endereco getEndereco() {
    return new Endereco(endereco.getRua(), endereco.getNumero());
}

Ao implementarmos o getter dessa forma, temos o mesmo exemplo de execução que retornou Av. Liberdade agora retornando Av. Paulista.

Por último, temos os casos de listas.
Escrever métodos de cópias de listas é algo bixeira. Para isso devemos usar o método estático Collections.unmodifiableList(list):

import java.util.Collections;   
import java.util.List; 
import java.util.ArrayList;   

public class Pessoa {

    private final String nome;
    private final List<Endereco> enderecos;

    public Pessoa() {
        nome = null;
        enderecos = null;
     }

    public Pessoa(String nome, List<Endereco> enderecos) {
        this.nome = nome;
        this.enderecos = enderecos;
    }

    public String getNome() {
        return nome;
    }

    public List<Endereco> getEnderecos() {
        return Collections.unmodifiableList(enderecos);
    }
}

Ao tentarmos executar o código abaixo:

public static void main(String[] args) {
    List<Endereco> es = new ArrayList<>();
    es.add(new Endereco("Rua 1", 10));
    es.add(new Endereco("Rua 2", 20));

    Pessoa p = new Pessoa("Igor", es);

    List<Endereco> listNaoModif = p.getEnderecos();
    listNaoModif.add(new Endereco("Rua 3", 30));
}

Recebemos a Exception java.lang.UnsupportedOperationException, pois a lista retornada pelo unmodifiableList(list), como o nome já diz, não pode ser modificada.

Dessa forma conseguimos garantir o estado dos nossos objetos!