O que é uma classe abstrata e quando ela faz sentido
Uma classe abstrata é uma classe que não pode ser instanciada diretamente e existe para servir como base para outras classes. Ela é útil quando você precisa de reutilização controlada: parte do comportamento é comum (e deve ser reaproveitado), mas algumas etapas variam e precisam ser fornecidas pelas subclasses.
Use classe abstrata quando houver:
- Estado compartilhado (campos) que faz sentido existir em todas as variações.
- Comportamento parcialmente comum: você quer fornecer uma implementação padrão para parte do fluxo e obrigar as subclasses a completar o restante.
- Regras de execução (ordem de passos) que devem ser preservadas para manter consistência.
Diferença prática entre classe abstrata e interface (sem repetir o capítulo de interfaces)
Ambas ajudam a modelar polimorfismo, mas a escolha muda o tipo de reutilização e acoplamento:
| Aspecto | Classe abstrata | Interface |
|---|---|---|
| Estado (campos) | Boa para estado compartilhado e invariantes comuns | Não é o foco; tende a ser contrato (sem estado de instância) |
| Reuso de código | Forte: métodos concretos + abstratos no mesmo tipo | Possível via métodos default, mas com menos controle de estado |
| Herança | Uma única superclasse | Múltiplas interfaces |
| Quando preferir | Quando há algoritmo comum e dados comuns que devem ser centralizados | Quando você quer capacidade/contrato sem impor hierarquia |
Componentes típicos de uma classe abstrata
Métodos abstratos (o que varia)
Um método abstrato declara a assinatura e não fornece corpo. Ele define um “gancho obrigatório”: a subclasse precisa implementar.
abstract class ExemploBase { abstract void passoObrigatorio();}Métodos concretos (o que é comum)
Uma classe abstrata pode ter métodos concretos com implementação completa. Eles encapsulam o comportamento comum e reduzem duplicação.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
abstract class ExemploBase { void passoComum() { // implementação compartilhada }}Campos protegidos: usar com cautela
Campos protected permitem acesso direto pelas subclasses. Isso pode ser conveniente, mas aumenta o acoplamento e pode facilitar violações de invariantes (subclasses alterando estado sem validação).
Boas práticas ao precisar de estado compartilhado:
- Prefira campos privados + métodos
protected(getters/setters controlados) para manter validações. - Se usar
protected, mantenha-os imutáveis quando possível (ex.:final) ou restrinja alterações a métodos bem definidos. - Evite expor coleções mutáveis diretamente; prefira cópias ou visões não modificáveis.
Template de comportamento: o padrão Template Method (conceito aplicado)
O padrão Template Method aparece naturalmente com classes abstratas: você define um método “template” (geralmente final) que descreve a sequência do algoritmo. Algumas etapas são comuns (métodos concretos) e outras variam (métodos abstratos ou “hooks” opcionais).
Objetivo: garantir que a ordem do algoritmo e as regras essenciais sejam preservadas, permitindo extensões apenas nos pontos previstos.
Passo a passo prático: processamento de arquivos com etapas variáveis
Cenário: você precisa processar arquivos de diferentes formatos (CSV e JSON). O fluxo geral é o mesmo:
- Validar entrada
- Ler conteúdo
- Interpretar (parse) conforme formato
- Validar dados interpretados
- Persistir resultado
A leitura e persistência podem ser comuns, mas o parse muda por formato. Além disso, alguns formatos podem exigir validações extras.
1) Defina a classe abstrata com o algoritmo comum (template)
import java.nio.file.Files;import java.nio.file.Path;import java.io.IOException;import java.util.List;abstract class FileProcessor<T> { private final Path path; protected FileProcessor(Path path) { if (path == null) throw new IllegalArgumentException("path é obrigatório"); this.path = path; } public final void process() throws IOException { validateInput(); String raw = readFile(); T parsed = parse(raw); validateParsed(parsed); persist(parsed); } protected void validateInput() { if (!Files.exists(path)) { throw new IllegalStateException("Arquivo não existe: " + path); } if (!Files.isRegularFile(path)) { throw new IllegalStateException("Não é um arquivo regular: " + path); } } protected String readFile() throws IOException { return Files.readString(path); } protected abstract T parse(String raw); protected void validateParsed(T parsed) { if (parsed == null) { throw new IllegalStateException("Conteúdo interpretado não pode ser nulo"); } } protected void persist(T parsed) { // Exemplo didático: persistência simulada System.out.println("Persistindo: " + parsed); }}Pontos importantes no código:
process()éfinalpara impedir que subclasses mudem a ordem do algoritmo.parseé abstrato: é a etapa variável obrigatória.validateParsedé um “hook” concreto: pode ser sobrescrito se necessário, mas já tem um padrão seguro.- O estado compartilhado (
path) éprivatepara evitar manipulação direta; o acesso ocorre por métodos comuns (readFileevalidateInput).
2) Crie um tipo de domínio simples para o resultado
import java.util.List;record People(List<String> names) {}3) Implemente uma subclasse para CSV (variação no parse)
import java.nio.file.Path;import java.util.Arrays;import java.util.List;class CsvPeopleProcessor extends FileProcessor<People> { public CsvPeopleProcessor(Path path) { super(path); } @Override protected People parse(String raw) { // Exemplo simples: cada linha é um nome List<String> names = Arrays.stream(raw.split("\\R")) .map(String::trim) .filter(s -> !s.isBlank()) .toList(); return new People(names); } @Override protected void validateParsed(People parsed) { super.validateParsed(parsed); if (parsed.names().isEmpty()) { throw new IllegalStateException("CSV sem nomes"); } }}4) Implemente uma subclasse para JSON (variação no parse)
Sem depender de bibliotecas externas, vamos simular um parse bem simplificado (apenas para ilustrar o ponto do template). Em um projeto real, você usaria um parser JSON adequado.
import java.nio.file.Path;import java.util.ArrayList;import java.util.List;class JsonPeopleProcessor extends FileProcessor<People> { public JsonPeopleProcessor(Path path) { super(path); } @Override protected People parse(String raw) { // Simulação didática: extrai valores entre aspas como "nomes" List<String> names = new ArrayList<>(); boolean inQuotes = false; StringBuilder current = new StringBuilder(); for (int i = 0; i < raw.length(); i++) { char c = raw.charAt(i); if (c == '"') { inQuotes = !inQuotes; if (!inQuotes) { String value = current.toString().trim(); if (!value.isBlank()) names.add(value); current.setLength(0); } continue; } if (inQuotes) current.append(c); } return new People(names); } @Override protected void validateParsed(People parsed) { super.validateParsed(parsed); // Exemplo: JSON exige pelo menos 2 nomes if (parsed.names().size() < 2) { throw new IllegalStateException("JSON deve conter ao menos 2 nomes"); } }}5) Use as subclasses de forma uniforme (polimorfismo via base abstrata)
import java.nio.file.Path;class Demo { public static void main(String[] args) throws Exception { FileProcessor<People> csv = new CsvPeopleProcessor(Path.of("people.csv")); FileProcessor<People> json = new JsonPeopleProcessor(Path.of("people.json")); csv.process(); json.process(); }}Repare que o código cliente chama process() da mesma forma, independentemente do formato. O que muda está encapsulado nas subclasses, nos pontos previstos pelo template.
Boas extensões: como evoluir sem quebrar o algoritmo comum
Adicionar uma nova variação sem alterar a base
Para suportar um novo formato (por exemplo, XML), você cria XmlPeopleProcessor e implementa parse (e opcionalmente sobrescreve validateParsed). O método process() permanece intacto, preservando a ordem e as regras comuns.
Evitar armadilhas comuns com classes abstratas
- Base abstrata “gorda”: se a classe abstrata começar a acumular responsabilidades não relacionadas, ela vira um ponto de acoplamento. Mantenha o template focado em um fluxo coeso.
- Excesso de
protected: prefira expor pontos de extensão por métodosprotectedbem definidos, não por campos mutáveis. - Subclasses alterando a ordem do algoritmo: use
finalno método template quando a sequência for parte da regra do domínio. - Hooks sem necessidade: só crie métodos sobrescrevíveis quando houver um caso real de variação; do contrário, mantenha o comportamento fechado.
Checklist de decisão: classe abstrata aqui é a melhor opção?
- Existe um fluxo que deve ser seguido sempre, com variações em etapas específicas?
- Há estado compartilhado ou utilidades comuns que justificam uma base?
- Você quer controlar onde a extensão acontece (métodos abstratos/hooks) e impedir mudanças na ordem (template
final)? - As subclasses representam variações do mesmo “processo” e não apenas capacidades soltas?