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.
- Que problema o encapsulamento resolve (a conta com saldo negativo).
- O padrão do Java: atributos
private+ métodosgetesetpúblicos. - Como validar no setter e quando é melhor ter métodos como
depositarem vez desetSaldo. - A convenção de nomes:
getNome,setNome,isAtivo. - Classes imutáveis (sem setters, com
final) e orecordcomo 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()esacar()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 = -500nem 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
nulle 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.
| Atributo | Getter? | Setter? | Por quê |
|---|---|---|---|
titular | Sim | Sim, com validação | O nome pode ser corrigido, mas nunca vazio |
saldo | Sim | Não: depositar/sacar | Muda só por operações com regras |
numero da conta | Sim | Não | Definido no construtor e nunca muda |
senha | Não | trocarSenha(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:
| Atributo | Getter | Setter |
|---|---|---|
String nome | String getNome() | void setNome(String nome) |
double precoUnitario | double getPrecoUnitario() | void setPrecoUnitario(double precoUnitario) |
boolean ativo | boolean 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; devolvevoid.- Para
boolean, o getter usais("é"):isAtivo()lê como uma pergunta, "está ativo?". - Um getter pode devolver algo calculado:
getMedia()não precisa de um atributomedia.
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
| Forma | Como fica | Quando usar |
|---|---|---|
| Atributos abertos | double saldo; | Só em exemplos e protótipos rápidos |
private + getter + setter com validação | getNome(), setNome(…) | Dados que podem mudar livremente, desde que válidos |
private + getter + métodos de negócio | getSaldo(), depositar(…), sacar(…) | Dados que mudam por operações com regras |
private final + getter, sem setter | classe Cpf | Dados que nunca mudam (imutáveis) |
record | record Fruta(String nome, double preco) {} | Classes imutáveis que só carregam dados |
Quando dá errado
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.
$ 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 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.
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
| Escrita | Significado |
|---|---|
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()paraboolean); setter escreve com validação (setNome(…), comthis.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.