Encapsulamento em Java
Agrupe dados e métodos em classes Java e oculte detalhes de implementação usando campos privados e acessores públicos.
Encapsulamento é o primeiro dos quatro pilares da OOP, e o mais fácil de colocar em prática: mantenha os dados de uma classe como privados e exponha o comportamento através de métodos. A classe torna-se responsável pelo seu próprio estado — nenhum código externo pode colocá-la em um estado inválido — e o restante do programa interage com ela por meio de uma interface que você controla.
O mecanismo são campos private mais métodos public. A disciplina é "não exponha estado que você não pode controlar."
O padrão
Uma classe não encapsulada é apenas um conjunto de campos públicos:
public class Account {
public int balance;
}
Account a = new Account();
a.balance = 100;
a.balance = -50; // nothing stops thisA versão encapsulada oculta o campo e expõe operações deliberadas:
public class Account {
private int balance;
public int balance() { return balance; }
public void deposit(int amount) {
if (amount <= 0) throw new IllegalArgumentException();
balance += amount;
}
public boolean withdraw(int amount) {
if (amount <= 0) throw new IllegalArgumentException();
if (amount > balance) return false;
balance -= amount;
return true;
}
}Agora não há como o código externo definir balance com um número negativo, sobrescrevê-lo sem passar pela validação, ou verificá-lo sem passar por balance().
O que você ganha
Invariantes em que você pode confiar. A classe Account garante que "balance ≥ 0 após cada operação pública." Como nada externo pode tocar em balance, a regra pode ser aplicada a partir de um único arquivo, não de toda a base de código.
Liberdade para mudar. Com balance privado, você pode mais tarde alterar seu tipo de int para long, mudar a unidade de dólares para centavos, armazená-lo em um BigDecimal ou movê-lo para um banco de dados — sem modificar um único chamador. Com um campo público, o tipo e o armazenamento ficam incorporados em cada ponto de chamada.
Uma API menor e mais clara. Os chamadores veem apenas deposit, withdraw, balance — não as dezenas de auxiliares privados que os fazem funcionar. A classe anuncia o que faz, não como.
Localidade de raciocínio. Quando algo dá errado com balance, o bug está em um de três métodos, não em qualquer um dos mil lugares que poderiam, de outra forma, escrever no campo.
Encapsulamento ≠ getters e setters para tudo
Um antipadrão comum é gerar mecanicamente um getter e um setter para cada campo:
public class Account {
private int balance;
public int getBalance() { return balance; }
public void setBalance(int v) { this.balance = v; } // same as public field, with extra steps
}Isso é tecnicamente "encapsulado" no sentido do livro didático, mas não alcança nenhum dos benefícios reais do encapsulamento — qualquer um ainda pode colocar o objeto em qualquer estado que desejar. O encapsulamento real expressa operações, não acesso bruto ao campo:
deposit(amount)em vez desetBalance(balance + amount)withdraw(amount)retornando sucesso/falha em vez desetBalance(balance - amount)balance()(um acessor somente leitura) sem umsetBalancecorrespondente
O próximo capítulo sobre getters e setters aborda as convenções para quando cada um é apropriado.
Cópias defensivas
Se o tipo de um campo é mutável (um array, uma lista, um Date), retorná-lo diretamente vaza o controle:
public class Order {
private final List<String> items = new ArrayList<>();
public List<String> items() { return items; } // leak!
}
Order o = new Order();
o.items().add("apple"); // outside code mutated the orderA correção é retornar uma visão não modificável ou uma cópia defensiva:
public List<String> items() {
return List.copyOf(items); // immutable snapshot
}O mesmo cuidado se aplica a setters que recebem valores mutáveis — copie-os na entrada:
public Order(List<String> items) {
this.items = new ArrayList<>(items);
}O capítulo sobre classes imutáveis aprofunda o assunto.
Escolhendo o nível de acesso correto
private e public são os dois que você usa com mais frequência, mas Java tem quatro níveis, e os intermediários importam para o encapsulamento:
private— visível apenas dentro da mesma classe. O padrão para campos e métodos auxiliares.- package-private (sem palavra-chave) — visível para outras classes no mesmo pacote. Útil quando algumas classes cooperantes formam uma unidade e devem ver os internos umas das outras, mas o mundo externo não deve.
protected— package-private mais visível para subclasses. Reserve para membros que uma subclasse genuinamente precisa sobrescrever ou construir sobre.public— visível em qualquer lugar. Esta é sua API publicada; uma vez que código externo dependa dela, alterá-la quebra os chamadores.
A regra prática é o princípio da menor exposição: dê a cada membro o acesso mais restrito que ainda permita o funcionamento do código, e amplie apenas quando uma necessidade concreta surgir. Um campo ou método public é uma promessa que você precisa cumprir. Consulte o capítulo sobre modificadores de acesso para a tabela completa.
Encapsulamento no design
A partir de um certo tamanho, o encapsulamento torna-se uma ferramenta de design, não apenas uma regra de codificação. Cada classe traça um limite ao redor de um bloco de estado e das operações sobre ele; o restante do sistema interage com ele por meio desse limite. A maioria dos padrões arquiteturais — em camadas, hexagonal, MVC, Clean — são argumentos sobre onde traçar os limites.
Diretrizes práticas:
- Campos são privados até que se prove o contrário. Comece assim; amplie apenas com uma razão.
- Exponha verbos, não substantivos.
cancel(),pay(),ship()são melhores quesetStatus(...). - Não retorne internos mutáveis diretamente. Envolva, copie ou use uma visão imutável.
- Valide na entrada. Rejeite estado impossível no limite para que o código interno possa assumir que é válido.
Um exemplo prático
O que vem a seguir
A parte mecânica do encapsulamento — os campos private com métodos public que os leem ou escrevem — tem suas próprias convenções que vale a pena conhecer. Continue para getters e setters.