Encapsulamento em Java OOP: visibilidade, invariantes e API pública

Capítulo 2

Tempo estimado de leitura: 8 minutos

+ Exercício

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.

ModificadorOnde é acessívelUso típicoImpacto
privateApenas dentro da própria classeCampos e detalhes internosMaior proteção e liberdade para refatorar
package-private (sem modificador)Dentro do mesmo pacoteColaboração entre classes do mesmo móduloBom para “API interna”; reduz exposição pública
protectedMesmo pacote + subclasses (mesmo em outros pacotes)Pontos de extensão para herançaPode “vazar” detalhes; cuidado com invariantes em subclasses
publicQualquer lugarAPI públicaMais 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 protected com 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 cria Email sem passar pela validação.
  • A classe garante a invariante “email válido e normalizado”.
  • O nome of comunica 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.
  • total só 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.

Agora responda o exercício sobre o conteúdo:

Ao refatorar uma classe para melhorar o encapsulamento e garantir invariantes, qual abordagem é a mais adequada para evitar que o objeto entre em estado inválido?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Encapsulamento eficaz protege o estado interno com campos private e expõe apenas operações seguras que validam dados e impedem violações de invariantes (como saldo negativo), evitando também “vazar” mutabilidade.

Próximo capitúlo

Construtores e inicialização segura em Java OOP: sobrecarga, validação e objetos consistentes

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
12%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.