Java Encapsulamento (get e set)

Encapsular é guardar algo numa cápsula: os dados de um objeto ficam escondidos lá dentro, e o mundo de fora só consegue lê-los ou mudá-los pelos métodos que a classe oferece. É o primeiro pilar da orientação a objetos e a razão de existirem os famosos getters e setters.

Você vai aprender
  • Que problema o encapsulamento resolve (a conta com saldo negativo).
  • O padrão do Java: atributos private + métodos get e set públicos.
  • Como validar no setter e quando é melhor ter métodos como depositar em vez de setSaldo.
  • A convenção de nomes: getNome, setNome, isAtivo.
  • Classes imutáveis (sem setters, com final) e o record como atalho.

Esta aula junta três anteriores: Métodos de Objeto, this e Modificadores de Acesso.


O problema: a conta com saldo negativo

O banco da Ana usa a classe Conta com atributos abertos, como nas primeiras aulas de objetos. Qualquer parte do programa mexe neles diretamente:

class Conta {
    String titular;
    double saldo;
}

public class Main {
    public static void main(String[] args) {
        Conta c = new Conta();
        c.titular = "Ana";
        c.saldo = 100;
        c.saldo = c.saldo - 600;
        c.titular = "";
        System.out.println("Titular: [" + c.titular + "]  Saldo: " + c.saldo);
    }
}
$ javac Main.java && java Main
Titular: []  Saldo: -500.0       « uma conta sem dono e com 500 negativos

O Java não reclamou de nada: compilou e rodou. Mas a conta ficou num estado inválido: sacaram 600 de quem tinha 100, e o titular virou um texto vazio. Num sistema grande, com centenas de classes mexendo em saldo, descobrir quem fez isso é um pesadelo.

O problema não é a conta: é que ela não tem como se defender. Qualquer um escreve qualquer valor nos atributos dela.


A ideia: a cápsula e o balcão

Pense no caixa de um banco. Você não entra no cofre: fala com o atendente no balcão. Ele confere se o seu saque cabe no saldo, se a assinatura confere, e só então mexe no cofre. O cofre fica fechado; o balcão é a única passagem, e a passagem tem regras.

  • O cofre são os atributos, marcados com private.
  • O balcão são os métodos public: getSaldo() para consultar, depositar() e sacar() para mudar, cada um com as suas conferências.

A conta encapsulada

class Conta {
    private String titular;
    private double saldo;

    public Conta(String titular) {
        setTitular(titular);
    }

    public String getTitular() {
        return titular;
    }

    public void setTitular(String titular) {
        if (titular == null || titular.isBlank()) {
            System.out.println("Titular inválido; mantido: " + this.titular);
            return;
        }
        this.titular = titular;
    }

    public double getSaldo() {
        return saldo;
    }

    public void depositar(double valor) {
        if (valor <= 0) {
            System.out.println("Depósito inválido: " + valor);
            return;
        }
        saldo += valor;
    }

    public boolean sacar(double valor) {
        if (valor <= 0 || valor > saldo) {
            System.out.println("Saque recusado: " + valor);
            return false;
        }
        saldo -= valor;
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        Conta c = new Conta("Ana");
        c.depositar(100);
        c.sacar(600);
        c.depositar(-50);
        c.setTitular("");
        c.setTitular("Ana Souza");
        System.out.println(c.getTitular() + ": " + c.getSaldo());
    }
}
$ javac Main.java && java Main
Saque recusado: 600.0
Depósito inválido: -50.0
Titular inválido; mantido: Ana
Ana Souza: 100.0          « a conta continua válida

As mesmas tentativas do exemplo anterior agora são barradas. Peça por peça:

private String titular; private double saldo;
O cofre. De fora da classe, c.saldo = -500 nem compila (saldo has private access in Conta).
public String getTitular()
Um getter ("pegador"): devolve o valor do atributo. Só leitura.
public void setTitular(String titular)
Um setter ("definidor"): recebe um valor novo e, se ele for válido, guarda no atributo. Aqui, recusa null e textos em branco (isBlank(), veja Métodos de String).
this.titular = titular;
O padrão da aula this: parâmetro e atributo com o mesmo nome.
setTitular(titular); no construtor
O construtor reaproveita o setter, para que a mesma regra valha desde o nascimento do objeto.
public double getSaldo()
Getter do saldo. Repare: não existe setSaldo.
depositar e sacar
As únicas formas de mudar o saldo, cada uma com a sua regra de negócio.

Nem todo atributo precisa de setter

Um erro comum é criar, por reflexo, um get e um set para cada atributo. Um setSaldo(double) público deixaria qualquer um escrever c.setSaldo(1000000): o atributo estaria privado só no nome. Pergunte sempre: como esse dado muda no mundo real? Saldo muda por depósito e saque, então a classe oferece depositar e sacar. O número da conta nunca muda, então não tem setter nenhum.

AtributoGetter?Setter?Por quê
titularSimSim, com validaçãoO nome pode ser corrigido, mas nunca vazio
saldoSimNão: depositar/sacarMuda só por operações com regras
numero da contaSimNãoDefinido no construtor e nunca muda
senhaNãotrocarSenha(antiga, nova)Ninguém de fora precisa ler a senha

Missão: complete o setNota da classe Aluno: ele só deve aceitar notas de 0 a 10. Para valores fora disso, imprima Nota recusada: seguido do valor e não mude nada. O main tenta 12, -1 e 9.5.

class Aluno {
    private String nome;
    private double nota;

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

    public String getNome() {
        return nome;
    }

    public double getNota() {
        return nota;
    }

    public void setNota(double nota) {
        // valide aqui
        this.nota = nota;
    }
}

public class Main {
    public static void main(String[] args) {
        Aluno a = new Aluno("Carla");
        a.setNota(12);
        a.setNota(-1);
        a.setNota(9.5);
        System.out.println(a.getNome() + ": " + a.getNota());
    }
}
class Aluno {
    private String nome;
    private double nota;

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

    public String getNome() {
        return nome;
    }

    public double getNota() {
        return nota;
    }

    public void setNota(double nota) {
        if (nota < 0 || nota > 10) {
            System.out.println("Nota recusada: " + nota);
            return;
        }
        this.nota = nota;
    }
}

public class Main {
    public static void main(String[] args) {
        Aluno a = new Aluno("Carla");
        a.setNota(12);
        a.setNota(-1);
        a.setNota(9.5);
        System.out.println(a.getNome() + ": " + a.getNota());
    }
}

Recusar com aviso ou com exceção?

Nos exemplos acima, o setter imprime um aviso e ignora o valor. É simples e bom para aprender, mas tem um defeito: quem chamou pode não perceber que nada aconteceu. Em programas reais, o costume é lançar uma exceção: o programa para (a menos que alguém trate o problema) e a mensagem diz exatamente o que houve.

class Conta {
    private double saldo;

    public void depositar(double valor) {
        if (valor <= 0) {
            throw new IllegalArgumentException("Depósito inválido: " + valor);
        }
        saldo += valor;
    }
}

public class Main {
    public static void main(String[] args) {
        Conta c = new Conta();
        c.depositar(100);
        c.depositar(-50);
        System.out.println("Esta linha não roda");
    }
}
$ javac Main.java && java Main
Exception in thread "main" java.lang.IllegalArgumentException: Depósito inválido: -50.0
	at Conta.depositar(Main.java:6)
	at Main.main(Main.java:16)

IllegalArgumentException quer dizer "argumento inválido", e o stack trace mostra o caminho: o main (linha 16) chamou depositar, que lançou o erro na linha 6. Lançar e tratar exceções é assunto de throw e throws e try e catch. Por enquanto, saiba que é a forma profissional de um setter dizer "não".


A convenção de nomes

Os nomes de getters e setters seguem uma convenção antiga do Java (das "JavaBeans"). Bibliotecas, frameworks e IDEs dependem dela, então vale seguir à risca:

AtributoGetterSetter
String nomeString getNome()void setNome(String nome)
double precoUnitariodouble getPrecoUnitario()void setPrecoUnitario(double precoUnitario)
boolean ativoboolean isAtivo()void setAtivo(boolean ativo)
(calculado, sem atributo)double getMedia()—
  • get + nome do atributo com a primeira letra maiúscula; sem parâmetros; devolve o tipo do atributo.
  • set + nome com maiúscula; um parâmetro do tipo do atributo; devolve void.
  • Para boolean, o getter usa is ("é"): isAtivo() lê como uma pergunta, "está ativo?".
  • Um getter pode devolver algo calculado: getMedia() não precisa de um atributo media.
class Aluno {
    private String nome;
    private boolean ativo;
    private double nota1;
    private double nota2;

    public Aluno(String nome) {
        this.nome = nome;
        this.ativo = true;
    }

    public String getNome() {
        return nome;
    }

    public boolean isAtivo() {
        return ativo;
    }

    public void setAtivo(boolean ativo) {
        this.ativo = ativo;
    }

    public void setNotas(double nota1, double nota2) {
        this.nota1 = nota1;
        this.nota2 = nota2;
    }

    public double getMedia() {
        return (nota1 + nota2) / 2;
    }
}

public class Main {
    public static void main(String[] args) {
        Aluno a = new Aluno("Bruno");
        a.setNotas(8, 7);
        System.out.println(a.getNome() + " ativo? " + a.isAtivo() + ", média " + a.getMedia());
        a.setAtivo(false);
        System.out.println(a.getNome() + " ativo? " + a.isAtivo());
    }
}
$ javac Main.java && java Main
Bruno ativo? true, média 7.5
Bruno ativo? false

A grande vantagem aparece com o tempo: se amanhã a Ana decidir guardar as notas num array em vez de nota1 e nota2, só o interior da classe muda. Quem usa getMedia() nem fica sabendo. É isso que "esconder os detalhes" compra: liberdade para mudar por dentro sem quebrar quem está de fora.


Classes imutáveis: sem setters

Alguns objetos nunca deveriam mudar depois de criados: um CPF, uma data de nascimento, uma coordenada. Para eles, a melhor proteção é a imutabilidade: atributos private final, preenchidos no construtor, e nenhum setter.

class Cpf {
    private final String numero;

    public Cpf(String numero) {
        this.numero = numero;
    }

    public String getNumero() {
        return numero;
    }

    public void trocar(String novo) {
        numero = novo;
    }
}

public class Main {
    public static void main(String[] args) {
        Cpf cpf = new Cpf("123.456.789-09");
        System.out.println(cpf.getNumero());
    }
}
$ javac Main.java
Main.java:13: error: cannot assign a value to final variable numero
        numero = novo;
        ^
1 error

O final ("final, definitivo") diz que o atributo recebe valor uma única vez, no construtor. Até um método da própria classe é impedido de trocá-lo: "não é possível atribuir um valor à variável final numero". Sem o método trocar, a classe compila e o CPF fica imutável para sempre. Mais sobre a palavra em final.

Objetos imutáveis são mais simples de entender (o valor que você leu agora é o mesmo de amanhã) e seguros de compartilhar. A String do Java é imutável pelo mesmo motivo.

O atalho: record Java 16+

Escrever atributos private final, construtor e getters à mão para uma classe que só carrega dados é trabalhoso. O record faz tudo isso numa linha:

record Fruta(String nome, double preco) {}

public class Main {
    public static void main(String[] args) {
        Fruta maca = new Fruta("maçã", 7.9);
        System.out.println(maca.nome());
        System.out.println(maca.preco());
        System.out.println(maca);
    }
}
$ javac Main.java && java Main
maçã
7.9
Fruta[nome=maçã, preco=7.9]     « o record também cria um toString legível

A linha record Fruta(String nome, double preco) {} cria uma classe com dois atributos private final, um construtor que recebe os dois, os "getters" nome() e preco() (sem o prefixo get) e mais alguns métodos úteis. Tentar mudar um campo dá o erro que você já conhece:

$ javac Main.java
Main.java:6: error: preco has private access in Fruta
        maca.preco = 5.0;
            ^
1 error

Records têm uma aula própria: Records.


Formas diferentes de proteger os dados

FormaComo ficaQuando usar
Atributos abertosdouble saldo;Só em exemplos e protótipos rápidos
private + getter + setter com validaçãogetNome(), setNome(…)Dados que podem mudar livremente, desde que válidos
private + getter + métodos de negóciogetSaldo(), depositar(…), sacar(…)Dados que mudam por operações com regras
private final + getter, sem setterclasse CpfDados que nunca mudam (imutáveis)
recordrecord Fruta(String nome, double preco) {}Classes imutáveis que só carregam dados

Quando dá errado

"… has private access in …"

O que aconteceu: o código de fora tentou usar um atributo privado diretamente (c.saldo).

Como resolver: use o getter (c.getSaldo()) ou o método de negócio (c.depositar(…)). Se nenhum método faz o que você precisa, pergunte se a classe deveria permitir aquilo antes de criar um setter.

"cannot find symbol … symbol: variable getSaldo" (faltaram os parênteses)
$ javac Main.java
Main.java:12: error: cannot find symbol
        System.out.println(c.getSaldo);
                            ^
  symbol:   variable getSaldo
  location: variable c of type Conta
1 error

O que aconteceu: sem parênteses, o Java procura um atributo chamado getSaldo (repare: symbol: variable). Getter é método.

Como resolver: c.getSaldo().

O setter "não funciona" (o valor não muda)

O que aconteceu: quase sempre é o bug clássico da aula this: public void setNome(String nome) { nome = nome; }. O parâmetro faz sombra no atributo, e nada muda. Outra possibilidade: a validação recusou o valor em silêncio.

Como resolver: this.nome = nome;. E faça o setter avisar (mensagem ou exceção) quando recusar algo.

"cannot assign a value to final variable …"

O que aconteceu: algum código (até da própria classe) tentou mudar um atributo final fora do construtor.

Como resolver: se o valor deve mesmo mudar, tire o final e crie um método com validação. Se não deve, crie um objeto novo com o valor novo, em vez de alterar o antigo.


Referência

EscritaSignificado
private Tipo campo;Atributo escondido; só a classe mexe
public Tipo getCampo() { return campo; }Getter: leitura
public boolean isAtivo() { return ativo; }Getter de boolean
public void setCampo(Tipo campo) { /* valida */ this.campo = campo; }Setter: escrita com validação
throw new IllegalArgumentException("…");Recusa um valor inválido interrompendo o programa
private final Tipo campo;Atributo que só recebe valor no construtor
record Nome(Tipo a, Tipo b) {}Classe imutável pronta, com getters a() e b()

Missão final: a lâmpada encapsulada

Missão: complete a classe Lampada: atributos private boolean ligada e private int potencia; métodos ligar(), isLigada(), getPotencia() e setPotencia(int potencia), que só aceita de 1 a 200 (fora disso, imprime Potência recusada: e o valor). O main está pronto.

class Lampada {
    // atributos e métodos aqui
}

public class Main {
    public static void main(String[] args) {
        Lampada sala = new Lampada();
        sala.setPotencia(60);
        sala.setPotencia(500);
        sala.ligar();
        System.out.println("Ligada? " + sala.isLigada());
        System.out.println("Potência: " + sala.getPotencia());
    }
}
class Lampada {
    private boolean ligada;
    private int potencia;

    public void ligar() {
        this.ligada = true;
    }

    public boolean isLigada() {
        return ligada;
    }

    public int getPotencia() {
        return potencia;
    }

    public void setPotencia(int potencia) {
        if (potencia < 1 || potencia > 200) {
            System.out.println("Potência recusada: " + potencia);
            return;
        }
        this.potencia = potencia;
    }
}

public class Main {
    public static void main(String[] args) {
        Lampada sala = new Lampada();
        sala.setPotencia(60);
        sala.setPotencia(500);
        sala.ligar();
        System.out.println("Ligada? " + sala.isLigada());
        System.out.println("Potência: " + sala.getPotencia());
    }
}

Exercícios

1. O nome do getter

Qual é o getter do atributo String email? (só o nome com os parênteses)

2. Getter de boolean

E o getter do atributo boolean matriculado?

3. O corpo do setter

Complete a última linha do setEmail(String email), que guarda o parâmetro no atributo:

4. Qual é a melhor forma de mudar o saldo de uma conta encapsulada?

5. O que torna um objeto imutável?

6. Para record Ponto(int x, int y) {}, como se lê o x de um ponto p?


Resumo

  • Encapsular é esconder os atributos (private) e expor só métodos com regras (public). O objeto passa a se defender de valores inválidos.
  • Getter lê (getNome(), isAtivo() para boolean); setter escreve com validação (setNome(…), com this.nome = nome).
  • Nem todo atributo precisa de setter: prefira métodos de negócio (depositar, sacar) quando a mudança tem regras.
  • Para recusar um valor, imprimir um aviso é simples; lançar IllegalArgumentException é o jeito profissional.
  • Imutável: private final + construtor + só getters. Atalho: record.
  • Com os detalhes escondidos, você pode mudar o interior da classe sem quebrar quem a usa.