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.

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
| Modificador | Mesma classe | Mesmo pacote | Subclasse | Qualquer pacote |
|---|---|---|---|---|
private | Sim | Não | Não | Não |
| sem modificador | Sim | Sim | Só no pacote | Não |
protected | Sim | Sim | Sim | Não diretamente |
public | Sim | Sim | Sim | Sim |
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.

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?
| Necessidade | Escolha mais provável |
|---|---|
| Definir uma capacidade implementável por tipos sem parentesco | Interface |
| Permitir várias capacidades no mesmo tipo | Interface |
| Compartilhar estado protegido e construção comum | Classe abstrata |
| Compartilhar parte de um algoritmo entre especializações | Classe abstrata |
| Apenas trocar um serviço usado pela classe | Interface 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

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
staticpertence à 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,
finalimpede reatribuição da referência; não torna automaticamente o objeto referenciado imutável. - Em um método,
finalimpede sobrescrita. Em uma classe, impede herança. recordoferece sintaxe compacta para transportadores transparentes de dados e gera acessores, construtor,equals,hashCodeetoString.
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
instanceofe 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
staticalterá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?
equalsehashCodetê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
- Modele uma conta com depósito e saque, impedindo valores inválidos e saldo abaixo da regra definida.
- Crie uma interface
CalculadoraFretee duas implementações. FaçaPedidodepender apenas da interface. - Modele uma classe abstrata
Funcionariocom cálculo de remuneração sobrescrito por dois tipos. - Escreva testes que comprovem o polimorfismo sem usar
instanceof. - 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.