Classes abstratas em Java OOP: template de comportamento e reutilização controlada

Capítulo 7

Tempo estimado de leitura: 7 minutos

+ Exercício

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:

AspectoClasse abstrataInterface
Estado (campos)Boa para estado compartilhado e invariantes comunsNão é o foco; tende a ser contrato (sem estado de instância)
Reuso de códigoForte: métodos concretos + abstratos no mesmo tipoPossível via métodos default, mas com menos controle de estado
HerançaUma única superclasseMúltiplas interfaces
Quando preferirQuando há algoritmo comum e dados comuns que devem ser centralizadosQuando 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.

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

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() é final para 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) é private para evitar manipulação direta; o acesso ocorre por métodos comuns (readFile e validateInput).

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étodos protected bem definidos, não por campos mutáveis.
  • Subclasses alterando a ordem do algoritmo: use final no 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?
  • 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?

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

Em um processamento de arquivos usando uma classe abstrata com Template Method, qual é a principal razão para declarar o método template (como process()) como final?

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

Você errou! Tente novamente.

Ao marcar o método template como final, a sequência do algoritmo fica fixa. As subclasses podem estender apenas nos pontos previstos (métodos abstratos e hooks), sem quebrar a ordem e as regras comuns do processo.

Próximo capitúlo

Polimorfismo em Java OOP na prática: substituibilidade e desenho de APIs

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

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.