SkillsTecnológicas
Menu
Back-end

Orientação a Objetos em Java: Classes, Herança e Polimorfismo

Aprenda orientação a objetos em Java com exemplos de classes, encapsulamento, herança, polimorfismo, interfaces, composição e testes.

Marcos RodriguesPublicado em 15 de outubro de 2024Atualizado em 23 de agosto de 202613 min de leitura
Desenvolvedor modela classes e objetos de uma aplicação Java em um monitor

A orientação a objetos em Java organiza um sistema como uma colaboração entre objetos que possuem estado e comportamento. Classes descrevem esses objetos; encapsulamento protege regras; herança especializa tipos; e polimorfismo permite usar diferentes implementações por meio de um contrato comum.

Esses conceitos não servem apenas para “separar o código em classes”. Uma boa modelagem torna mudanças mais localizadas, reduz estados inválidos e facilita testes. Uma modelagem ruim cria hierarquias rígidas, classes enormes e métodos que dependem de detalhes demais.

Neste guia, você verá classes, objetos, construtores, modificadores de acesso, herança, sobrescrita, polimorfismo, classes abstratas, interfaces e composição com exemplos práticos. Se conceitos como variável, condição e método ainda forem novos, revise antes as bases da programação.

O que é orientação a objetos em Java?

Orientação a objetos é um paradigma de programação no qual dados e operações relacionadas ficam reunidos em objetos. Em Java, classes e interfaces definem tipos; objetos são instâncias criadas a partir desses tipos. A trilha oficial do Dev.java sobre programação orientada a objetos apresenta objetos, classes, interfaces, pacotes e herança como partes conectadas do modelo da linguagem.

Um objeto costuma ter três aspectos: estado, guardado em campos; comportamento, exposto por métodos; e identidade, que distingue duas instâncias mesmo quando seus dados parecem iguais. O objetivo não é imitar literalmente o mundo real, mas criar abstrações úteis para as regras do software.

Classe, objeto, estado e comportamento

Uma classe é a definição de um tipo. Ela declara campos, construtores e métodos. Um objeto é uma instância concreta dessa classe. A documentação oficial de classes e objetos em Java detalha criação de classes, métodos, construtores e membros.

public class ContaBancaria {
    private final String numero;
    private double saldo;

    public ContaBancaria(String numero, double saldoInicial) {
        this.numero = numero;
        this.saldo = saldoInicial;
    }

    public void depositar(double valor) {
        if (valor <= 0) {
            throw new IllegalArgumentException("Valor deve ser positivo");
        }
        saldo += valor;
    }

    public double consultarSaldo() {
        return saldo;
    }
}

numero e saldo formam o estado; depositar e consultarSaldo formam o comportamento. O operador new cria a instância: ContaBancaria conta = new ContaBancaria("001", 100);.

Construtores e validação de invariantes

O construtor coloca o objeto em um estado inicial válido. Uma invariante é uma regra que deve continuar verdadeira durante a vida do objeto. Se uma conta não pode existir sem número, o construtor deve rejeitar null e texto em branco, em vez de permitir uma instância parcialmente válida.

public ContaBancaria(String numero, double saldoInicial) {
    if (numero == null || numero.isBlank()) {
        throw new IllegalArgumentException("Número obrigatório");
    }
    if (saldoInicial < 0) {
        throw new IllegalArgumentException("Saldo inicial inválido");
    }
    this.numero = numero;
    this.saldo = saldoInicial;
}

Essa validação reduz verificações espalhadas. Depois que a instância existe, o restante do sistema pode confiar que ela respeita seu contrato básico.

Encapsulamento não é criar getters para tudo

Encapsular significa esconder detalhes mutáveis e oferecer operações que preservam as regras do domínio. Tornar um campo private e criar um setSaldo(double saldo) público não resolve o problema se qualquer valor puder ser gravado.

Prefira métodos com intenção, como depositar, sacar e transferirPara. Eles dizem por que o estado muda e podem aplicar validações. Getters são adequados quando outro componente realmente precisa consultar um dado; setters indiscriminados expõem a representação interna e tornam o objeto apenas um recipiente.

Modificadores de acesso

ModificadorMesma classeMesmo pacoteSubclasseQualquer pacote
privateSimNãoNãoNão
sem modificadorSimSimSó no pacoteNão
protectedSimSimSimNão diretamente
publicSimSimSimSim

Use a menor visibilidade necessária. Uma API pública é um compromisso de manutenção; detalhes privados podem mudar com menor impacto. A visibilidade de pacote é útil para colaboradores internos de um módulo, enquanto protected precisa de cuidado porque aumenta o acoplamento com subclasses.

Herança com extends

Herança cria uma relação de especialização: a subclasse é um tipo da superclasse. Java permite que uma classe estenda uma única classe diretamente. O material oficial sobre herança em Java também cobre sobrescrita, polimorfismo e Object como raiz da hierarquia.

public class ContaCorrente extends ContaBancaria {
    private final double limite;

    public ContaCorrente(String numero, double saldo, double limite) {
        super(numero, saldo);
        this.limite = limite;
    }

    public double consultarLimite() {
        return limite;
    }
}

super(...) chama o construtor da classe base. A relação só é saudável quando a subclasse pode ser usada onde a superclasse é esperada sem quebrar o significado das operações. Herança apenas para reaproveitar algumas linhas costuma gerar uma abstração frágil.

Módulo central aciona mecanismos diferentes para representar herança e polimorfismo em Java
No polimorfismo, uma referência do tipo mais geral pode acionar implementações diferentes em tempo de execução, conforme o objeto concreto recebido.

Sobrescrita com @Override

Uma subclasse sobrescreve um método de instância quando fornece uma implementação específica com assinatura compatível. A anotação @Override faz o compilador verificar a intenção e evita erros de digitação.

public abstract class Notificacao {
    public abstract void enviar(String mensagem);
}

public class NotificacaoEmail extends Notificacao {
    @Override
    public void enviar(String mensagem) {
        System.out.println("Enviando e-mail: " + mensagem);
    }
}

Métodos static são ocultados, não sobrescritos de forma polimórfica. Métodos final não podem ser sobrescritos; métodos private não são herdados como membros acessíveis da subclasse.

Polimorfismo e despacho dinâmico

Polimorfismo permite que uma variável de um supertipo referencie objetos de subtipos diferentes. Quando um método sobrescrito é chamado, a JVM escolhe em tempo de execução a implementação do objeto concreto. Isso é despacho dinâmico.

List<Notificacao> canais = List.of(
    new NotificacaoEmail(),
    new NotificacaoSms()
);

for (Notificacao canal : canais) {
    canal.enviar("Pedido aprovado");
}

O laço não precisa de if para descobrir o tipo de cada canal. Novas notificações podem ser acrescentadas sem alterar essa rotina, desde que respeitem o contrato. Essa substituição é uma das maiores vantagens práticas da POO.

Sobrecarga e sobrescrita são diferentes

Sobrecarga usa o mesmo nome com listas de parâmetros diferentes na mesma classe; a escolha ocorre em compilação. Sobrescrita redefine em uma subclasse um método de instância herdado; a implementação pode ser escolhida em execução.

public void enviar(String mensagem) { ... }
public void enviar(String mensagem, int prioridade) { ... } // sobrecarga

@Override
public String toString() { ... } // sobrescrita

Classes abstratas

Uma classe abstract não pode ser instanciada diretamente. Ela pode combinar estado compartilhado, métodos concretos e métodos abstratos. É útil quando os tipos possuem uma base comum real e parte do algoritmo pode ser implementada uma vez.

public abstract class Relatorio {
    public final void gerar() {
        validarDados();
        exportar();
    }

    protected void validarDados() { /* regra comum */ }
    protected abstract void exportar();
}

A classe base controla a sequência, enquanto subclasses implementam o ponto variável. Evite classes abstratas com dezenas de métodos opcionais: isso indica que a hierarquia reúne responsabilidades incompatíveis.

Interfaces como contratos

Uma interface define capacidades que classes diferentes podem oferecer. Uma variável do tipo da interface pode referenciar qualquer objeto que a implemente, como explica o guia oficial sobre uso de interface como tipo.

public interface FormaPagamento {
    Recibo pagar(double valor);
}

public final class PagamentoPix implements FormaPagamento {
    @Override
    public Recibo pagar(double valor) {
        return new Recibo("PIX", valor);
    }
}

Java permite implementar várias interfaces. Interfaces também podem declarar métodos default, static e privados de apoio, mas o contrato público deve permanecer coeso. Para aprofundar esse recurso isoladamente, consulte o guia de interfaces em Java.

Classe abstrata ou interface?

NecessidadeEscolha mais provável
Definir uma capacidade implementável por tipos sem parentescoInterface
Permitir várias capacidades no mesmo tipoInterface
Compartilhar estado protegido e construção comumClasse abstrata
Compartilhar parte de um algoritmo entre especializaçõesClasse abstrata
Apenas trocar um serviço usado pela classeInterface com composição

Não escolha pela quantidade de código. Pergunte se existe uma família de tipos com identidade comum ou apenas uma capacidade. Em aplicações, interfaces pequenas combinadas com composição frequentemente preservam melhor a flexibilidade.

Composição em vez de herança

Componentes intercambiáveis conectados por uma porta comum representam composição e interfaces em Java
Interfaces definem contratos estáveis; a composição permite trocar implementações sem transformar toda relação de colaboração em uma hierarquia de herança.

Composição significa que um objeto recebe ou cria colaboradores para realizar seu trabalho. Em vez de dizer “Pedido é uma FormaPagamento”, dizemos “Pedido usa uma FormaPagamento”. A segunda relação corresponde ao domínio e permite trocar Pix, cartão ou uma implementação falsa de teste.

Herança acopla a subclasse à estrutura da classe base. Composição acopla o consumidor a um contrato, que pode ser pequeno e estável. Use herança para uma relação “é um” substituível; use composição para uma relação “tem um” ou “usa um”.

Exemplo prático: pedido e forma de pagamento

public record Recibo(String meio, double valor) {}

public final class Pedido {
    private final double total;
    private final FormaPagamento formaPagamento;
    private boolean pago;

    public Pedido(double total, FormaPagamento formaPagamento) {
        if (total <= 0) throw new IllegalArgumentException("Total inválido");
        this.total = total;
        this.formaPagamento = Objects.requireNonNull(formaPagamento);
    }

    public Recibo confirmarPagamento() {
        if (pago) throw new IllegalStateException("Pedido já foi pago");
        Recibo recibo = formaPagamento.pagar(total);
        pago = true;
        return recibo;
    }
}

A classe Pedido preserva suas invariantes, enquanto o pagamento varia por polimorfismo. O construtor torna a dependência explícita. Um framework como Spring pode fornecer a implementação, mas o conceito existe independentemente dele. Se esse é seu próximo passo, veja também como aprender Spring Boot.

Associação, agregação e composição

Associação é uma relação geral entre objetos: um serviço conhece um repositório. Agregação descreve uma relação todo-parte em que a parte pode existir separadamente: um time agrega jogadores. Composição indica propriedade e ciclo de vida mais forte: um pedido contém seus itens.

No código Java, todas podem aparecer como campos e referências; a diferença é semântica. Documente a propriedade, decida quem cria e quem altera cada objeto e evite compartilhar coleções mutáveis sem necessidade.

static, final e records

  • static pertence à classe, não a uma instância. É adequado para fábricas, constantes e funções que não dependem do estado do objeto.
  • Em um campo, final impede reatribuição da referência; não torna automaticamente o objeto referenciado imutável.
  • Em um método, final impede sobrescrita. Em uma classe, impede herança.
  • record oferece sintaxe compacta para transportadores transparentes de dados e gera acessores, construtor, equals, hashCode e toString.

Records são superficialmente imutáveis: seus componentes são finais, mas podem referenciar coleções mutáveis. Eles são ótimos para valores como Recibo, mas não substituem uma entidade que precisa controlar um ciclo de vida complexo.

equals, hashCode e toString

Toda classe herda de java.lang.Object, a raiz da hierarquia, conforme a API oficial de Object. Entre seus métodos estão equals, hashCode e toString.

Por padrão, equals compara identidade. Para objetos de valor, sobrescreva-o com uma regra de igualdade coerente e sobrescreva hashCode junto: objetos iguais precisam produzir o mesmo hash. Caso contrário, buscas em HashSet e HashMap ficam incorretas. toString deve ajudar no diagnóstico sem expor senhas, tokens ou dados pessoais.

Como testar código orientado a objetos

Teste comportamentos observáveis, não campos privados. Para Pedido, verifique que total inválido é rejeitado, que o pagamento recebe o valor correto e que uma segunda confirmação falha. Uma interface permite fornecer uma implementação controlada sem chamar um gateway real.

@Test
void devePagarUmaUnicaVez() {
    FormaPagamento fake = valor -> new Recibo("FAKE", valor);
    Pedido pedido = new Pedido(150.0, fake);

    Recibo recibo = pedido.confirmarPagamento();

    assertEquals(150.0, recibo.valor());
    assertThrows(IllegalStateException.class,
        pedido::confirmarPagamento);
}

Para estruturar a suíte, consulte o guia de testes unitários em Java com JUnit e Mockito. Testes difíceis podem revelar objetos com responsabilidades demais ou dependências ocultas.

Erros comuns em POO com Java

  • Classe Deus: concentra validação, persistência, integração e apresentação. Divida por responsabilidades e fronteiras.
  • Modelo anêmico: entidades expõem setters, enquanto todas as regras ficam em serviços procedurais.
  • Herança por conveniência: uma classe estende outra apenas para reutilizar métodos, mesmo sem relação “é um”.
  • Downcast frequente: usar instanceof e conversões em série pode indicar que o polimorfismo está ausente.
  • Interface enorme: implementações são obrigadas a fornecer métodos que não usam. Prefira contratos coesos.
  • Estado global mutável: campos static alteráveis criam dependências invisíveis e testes instáveis.
  • Expor coleções internas: devolver uma lista mutável permite que chamadores quebrem invariantes.

Padrões de projeto ajudam quando existe um problema recorrente, não como decoração arquitetural. Depois de dominar estas bases, avance para os padrões de projeto no desenvolvimento de software.

Checklist de uma modelagem saudável

  • Cada classe possui uma responsabilidade explicável em uma frase?
  • O construtor cria um objeto válido?
  • Os métodos expressam intenções do domínio, em vez de apenas alterar campos?
  • A visibilidade de cada membro é a menor necessária?
  • A herança representa substituição verdadeira?
  • Dependências variáveis são recebidas por contratos pequenos?
  • As coleções internas estão protegidas contra alteração indevida?
  • equals e hashCode têm uma regra coerente quando necessários?
  • Os testes exercitam comportamento público sem depender de detalhes internos?
  • Uma nova implementação pode ser adicionada sem grandes condicionais?

Não é preciso satisfazer um ideal abstrato em todas as classes. O objetivo é reduzir o custo das mudanças que o sistema realmente recebe. Em um portfólio, aplique isso em um dos projetos backend para praticar e registre as decisões no README.

Exercícios para praticar

  1. Modele uma conta com depósito e saque, impedindo valores inválidos e saldo abaixo da regra definida.
  2. Crie uma interface CalculadoraFrete e duas implementações. Faça Pedido depender apenas da interface.
  3. Modele uma classe abstrata Funcionario com cálculo de remuneração sobrescrito por dois tipos.
  4. Escreva testes que comprovem o polimorfismo sem usar instanceof.
  5. Refatore uma hierarquia artificial para composição e compare quais arquivos mudam ao adicionar um comportamento.

Se estiver decidindo qual linguagem usar nos exercícios, a comparação Java ou Python ajuda a separar sintaxe, ecossistema e objetivos de carreira.

Perguntas frequentes

Java permite herança múltipla?

Uma classe Java estende diretamente apenas uma classe. Ela pode, porém, implementar várias interfaces. Interfaces também podem estender mais de uma interface, e a linguagem possui regras para resolver conflitos de métodos default.

Todo atributo precisa ser private?

Campos de instância mutáveis normalmente devem ser privados para proteger invariantes. Existem exceções conscientes, como constantes public static final. A regra prática é não expor mais do que os consumidores precisam.

Quando evitar herança?

Evite-a quando a relação existe apenas para reaproveitar código, quando subclasses precisam desativar comportamentos herdados ou quando a base muda com frequência. Nesses casos, extraia o comportamento para um colaborador e use composição.

Interface pode ter implementação?

Sim. Interfaces podem ter métodos default e static, além de métodos privados de apoio. Métodos abstratos continuam sem corpo e precisam ser implementados por uma classe concreta, salvo se ela também for abstrata.

POO é obrigatória em Java?

Java é fortemente orientada a classes, mas programas modernos também usam estilo funcional com lambdas, streams e funções puras. O melhor código combina paradigmas conforme o problema, sem transformar cada operação simples em uma hierarquia.

Conclusão

Dominar orientação a objetos em Java significa ir além das definições de classe, herança e polimorfismo. O ponto central é criar objetos válidos, proteger regras, usar abstrações substituíveis e escolher composição quando uma hierarquia não representa o domínio.

Comece com classes pequenas, construtores que validam invariantes e métodos que expressam intenção. Acrescente interfaces nos pontos de variação, use herança somente para especializações verdadeiras e confirme o desenho com testes. Assim, a POO deixa de ser teoria e passa a reduzir o custo real de evoluir o software.