O que é encapsulamento (na prática)
Encapsulamento é a prática de proteger o estado interno de um objeto e expor apenas operações seguras para manipulá-lo. Em Java, isso é feito principalmente com modificadores de acesso e com uma API pública desenhada para manter invariantes (regras que devem ser verdadeiras durante toda a vida do objeto).
Um objeto encapsulado evita que código externo coloque a instância em um estado inválido, reduz acoplamento e torna mudanças internas menos arriscadas. A ideia central é: o objeto controla suas próprias regras.
Visibilidade em Java: o que cada modificador permite
Os modificadores de acesso determinam quem pode acessar campos e métodos. Isso impacta diretamente manutenção, testes e a estabilidade da API.
| Modificador | Onde é acessível | Uso típico | Impacto |
|---|---|---|---|
private | Apenas dentro da própria classe | Campos e detalhes internos | Maior proteção e liberdade para refatorar |
| package-private (sem modificador) | Dentro do mesmo pacote | Colaboração entre classes do mesmo módulo | Bom para “API interna”; reduz exposição pública |
protected | Mesmo pacote + subclasses (mesmo em outros pacotes) | Pontos de extensão para herança | Pode “vazar” detalhes; cuidado com invariantes em subclasses |
public | Qualquer lugar | API pública | Mais difícil de mudar sem quebrar consumidores |
Regra prática de design
- Comece com o menor acesso possível (
private), aumente apenas quando necessário. - Evite campos
public(mesmo que sejam “simples”). - Use package-private para manter uma “API interna” testável e coesa dentro do pacote.
- Use
protectedcom parcimônia: herança amplia a superfície de quebra de invariantes.
Invariantes: as regras que o objeto deve sempre respeitar
Invariantes são condições que devem ser verdadeiras após a construção do objeto e após qualquer operação pública. Exemplos:
- Saldo de conta não pode ser negativo.
- Quantidade em estoque não pode ser menor que zero.
- Email deve ter formato válido.
- Intervalo de datas: início não pode ser depois do fim.
Encapsulamento eficaz significa: nenhum caminho público deve permitir violar invariantes.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Erro comum: setters permissivos e estado inválido
Um dos problemas mais frequentes é expor setters que aceitam qualquer valor, permitindo estados inválidos.
public class ContaBancaria { public double saldo; // ERRO: estado exposto public void setSaldo(double saldo) { // ERRO: setter permissivo this.saldo = saldo; }}Problemas desse design:
- Qualquer código pode fazer
conta.saldo = -1000. - O setter não valida nada.
- Fica difícil garantir regras de negócio (invariantes).
- Qualquer mudança interna quebra consumidores (porque tudo é público).
Refatoração: proteger estado e expor operações seguras
Passo a passo prático
- Passo 1: tornar campos
private. - Passo 2: remover setters genéricos quando a operação correta é um comportamento (ex.:
depositar,sacar). - Passo 3: validar entradas e manter invariantes em cada método público.
- Passo 4: expor apenas o necessário (API pública mínima).
public class ContaBancaria { private double saldo; public ContaBancaria(double saldoInicial) { if (saldoInicial < 0) { throw new IllegalArgumentException("Saldo inicial não pode ser negativo"); } this.saldo = saldoInicial; } public double getSaldo() { return saldo; } public void depositar(double valor) { if (valor <= 0) { throw new IllegalArgumentException("Depósito deve ser positivo"); } saldo += valor; } public void sacar(double valor) { if (valor <= 0) { throw new IllegalArgumentException("Saque deve ser positivo"); } if (valor > saldo) { throw new IllegalStateException("Saldo insuficiente"); } saldo -= valor; }}Note que não existe setSaldo. O saldo só muda por operações válidas, e cada operação protege a invariante “saldo não negativo”.
Getters e setters: quando usar e como validar
Getters e setters não são obrigatórios; eles são ferramentas. Um setter só faz sentido quando:
- Existe um motivo real para alterar o valor diretamente.
- A alteração pode ser validada e não quebra invariantes.
- O nome “setX” representa de fato uma operação segura e coerente.
Exemplo: entidade com validação em setter
public class Usuario { private String email; public Usuario(String email) { setEmail(email); // reutiliza validação } public String getEmail() { return email; } public void setEmail(String email) { if (email == null || email.isBlank()) { throw new IllegalArgumentException("Email é obrigatório"); } if (!email.contains("@")) { throw new IllegalArgumentException("Email inválido"); } this.email = email; }}Aqui o setter existe, mas não é permissivo: ele impede estados inválidos.
Evite “vazar” referências mutáveis (encapsulamento profundo)
Mesmo com campos private, você pode quebrar encapsulamento ao retornar objetos mutáveis diretamente. Exemplo com lista:
import java.util.ArrayList;import java.util.List;public class Carrinho { private final List<String> itens = new ArrayList<>(); public List<String> getItens() { return itens; // ERRO: código externo pode modificar livremente }}Qualquer consumidor pode fazer getItens().clear() e quebrar regras do carrinho.
Refatoração: retornar visão imutável ou cópia defensiva
import java.util.ArrayList;import java.util.Collections;import java.util.List;public class Carrinho { private final List<String> itens = new ArrayList<>(); public void adicionarItem(String item) { if (item == null || item.isBlank()) { throw new IllegalArgumentException("Item inválido"); } itens.add(item); } public List<String> getItens() { return Collections.unmodifiableList(itens); }}Agora o consumidor pode ler, mas não consegue alterar a lista diretamente. A mutação passa por métodos que validam.
Construtores vs métodos de fábrica estáticos
Construtores são ótimos, mas métodos de fábrica estáticos podem melhorar encapsulamento e consistência da criação de objetos.
Quando métodos de fábrica ajudam
- Quando você quer nomes mais expressivos do que um construtor.
- Quando precisa validar e normalizar dados antes de criar.
- Quando quer controlar instâncias (ex.: cache, singletons, reutilização).
- Quando há múltiplas formas de criar o objeto e você quer evitar sobrecarga confusa de construtores.
Exemplo: criação segura com normalização
public class Email { private final String valor; private Email(String valor) { this.valor = valor; } public static Email of(String raw) { if (raw == null) { throw new IllegalArgumentException("Email é obrigatório"); } String normalizado = raw.trim().toLowerCase(); if (normalizado.isBlank() || !normalizado.contains("@")) { throw new IllegalArgumentException("Email inválido"); } return new Email(normalizado); } public String getValor() { return valor; }}Benefícios:
- O construtor é
private, então ninguém criaEmailsem passar pela validação. - A classe garante a invariante “email válido e normalizado”.
- O nome
ofcomunica intenção de fábrica.
API pública mínima e consistente
Uma API pública mínima expõe apenas o que consumidores precisam para usar o objeto corretamente. Isso reduz dependências e facilita refatorações internas.
Checklist para refatorar uma API
- Remova setters que não representam uma operação de negócio (troque por métodos com intenção:
ativar,cancelar,alterarEmail). - Evite getters que expõem detalhes internos desnecessários (especialmente coleções mutáveis).
- Prefira métodos que preservem invariantes e falhem rápido com exceções claras.
- Padronize validações (null/blank/intervalos) e mensagens.
- Defina estados válidos e impeça transições inválidas.
Exemplo de erro: objeto com estado parcialmente configurado
public class Pedido { private String clienteId; private double total; public void setClienteId(String clienteId) { this.clienteId = clienteId; // sem validação } public void setTotal(double total) { this.total = total; // pode ser negativo } public void finalizar() { // pode finalizar sem clienteId, com total negativo etc. }}Esse design permite criar um Pedido e deixá-lo em estado inválido por tempo indeterminado.
Refatoração: invariantes garantidas desde a criação e transições seguras
public class Pedido { private final String clienteId; private double total; private boolean finalizado; public Pedido(String clienteId) { if (clienteId == null || clienteId.isBlank()) { throw new IllegalArgumentException("Cliente é obrigatório"); } this.clienteId = clienteId; this.total = 0.0; this.finalizado = false; } public String getClienteId() { return clienteId; } public double getTotal() { return total; } public boolean isFinalizado() { return finalizado; } public void adicionarItem(double preco) { if (finalizado) { throw new IllegalStateException("Pedido já finalizado"); } if (preco <= 0) { throw new IllegalArgumentException("Preço deve ser positivo"); } total += preco; } public void finalizar() { if (finalizado) { throw new IllegalStateException("Pedido já finalizado"); } if (total <= 0) { throw new IllegalStateException("Pedido sem itens não pode ser finalizado"); } finalizado = true; }}Agora:
clienteIdé obrigatório e imutável.totalsó aumenta por operações válidas.- Há regras claras de transição: não dá para adicionar itens após finalizar.
Encapsulamento e testes: como equilibrar
Um motivo comum para “abrir” campos/métodos é facilitar testes. Em vez de expor estado interno, prefira:
- Testar via comportamento (métodos públicos) e efeitos observáveis.
- Usar package-private para colaboradores internos dentro do mesmo pacote, quando fizer sentido modularmente.
- Evitar testar detalhes de implementação (ex.: campos), pois isso torna o teste frágil.
Quando você mantém invariantes e uma API pública pequena, os testes tendem a ficar mais simples: você valida entradas/saídas e regras de transição, sem depender de como o estado é armazenado.