Orientação a Objetos¶
Orientação a Objetos (OO) é um paradigma de programação — uma forma de organizar código — alternativa ao paradigma procedural (onde tudo é função e variável solta). A ideia central: modelar o problema em termos de objetos que combinam estado (os dados que carregam) e comportamento (o que sabem fazer), de um jeito mais próximo de como pensamos sobre o mundo real.
Os exemplos desta página usam Java como linguagem, mas os conceitos valem para qualquer linguagem orientada a objetos (Python, C#, Kotlin, ...). O que é específico da sintaxe Java fica em Linguagens de Programação → Java.
De onde vem a OO e por que usá-la¶
Origem. A OO nasceu da simulação: para modelar sistemas reais (filas, tráfego, fábricas) é natural descrever entidades que mudam de estado ao longo do tempo e se relacionam (discrete event simulation). Em 1962, na Noruega, Ole-Johan Dahl e Kristen Nygaard criaram o SIMULA (versão final, SIMULA 67), a primeira linguagem com classes, objetos e herança — pensada para ser orientada a problemas, e não ao computador (ganharam o prêmio Turing em 2001). Nos anos 1970, Alan Kay, no Xerox PARC, criou o Smalltalk (e popularizou os termos "objeto" e "mensagem") para computadores pessoais; de lá vieram C++, Java, C#, Python e as demais.
O paradigma anterior (estruturado/procedural) usa três estruturas — sequência, decisão e repetição — e separa dados de operações. Funciona bem para problemas pequenos; num controle de estoque, com produto, venda, cliente e dezenas de operações, os dados e as funções de cada conceito
ficam misturados em um mesmo código, e a modularização por funções fica cada vez mais complexa. Em C, reaproveitar dados exige struct e repetição (um Pagamento dentro de Debito e de Credito, e mais cópias a cada novo tipo).
| Motivo para usar OO | Como a OO ajuda |
|---|---|
| Reúso | De comportamento (métodos) e de informação (atributos), por herança, composição e polimorfismo |
| Coesão | Dados e operações de um conceito ficam juntos em uma classe |
| Baixo acoplamento | Classes dependem de abstrações (interfaces, classes abstratas), e uma mudança não se espalha |
| Menor gap semântico | Reduz a distância entre o problema (mundo real/domínio) e a solução (código): o código fala Cliente, Pedido, Pagamento |
Os conceitos da OO se agrupam em estruturais (classe, atributo, método, objeto, tipos), relacionais (herança, associação, interface) e organizacionais (pacotes e visibilidades).
Classe e objeto¶
Uma classe é um molde: define quais atributos (dados) e métodos (comportamentos) um tipo de objeto vai ter. Um objeto é uma instância concreta desse molde — cada instância tem seus próprios valores para os atributos.
Isso só define o molde. Para existir de verdade (na memória, em tempo de execução),
precisamos instanciar um objeto com a palavra-chave new:
Definição: Classe x Objeto
Uma classe é a especificação (o que um objeto desse tipo deve ter e como deve se
comportar); um objeto é uma instância concreta dessa especificação, com seus próprios
valores. A mesma classe Livro pode gerar milhares de objetos Livro diferentes —
cada um com seu nome, valor e ISBN próprios.
Uma classe também pode ter, como atributo, outra classe — por exemplo, um Livro pode
ter um Autor. Isso é chamado de composição: montar um objeto a partir de outros
objetos, em vez de repetir os mesmos campos em lugares diferentes.
Definição: Membros da classe
Nome coletivo para tudo que pode ser declarado dentro de uma classe: atributos (variáveis), métodos e construtores. "Membro" é o termo genérico usado quando a distinção entre os três não importa para o que está sendo discutido.
Referência, não valor¶
Ponto que costuma confundir quem vem de tipos primitivos: uma variável de objeto guarda uma referência (um endereço de memória) para onde o objeto vive, não o objeto em si.
graph LR
v1["autor"] --> obj1["Autor { nome: Rodrigo Turini }"]
v2["autor2"] --> obj2["Autor { nome: Rodrigo Turini }"]
Isso tem duas consequências que caem bastante em entrevista:
- Comparar com
==compara referência, não conteúdo. Dois objetosAutorcriados separadamente, mesmo com os mesmos dados, são!=entre si com==— porque vivem em endereços de memória diferentes. (Comparar conteúdo é outro assunto: em Java, isso é o métodoequals, que por padrão também compara referência a menos que a classe sobrescreva esse comportamento.) - Atribuir um objeto a outra variável não copia o objeto — copia a referência. Depois
de
livro.autor = autor, tantoautorquantolivro.autorapontam para o mesmo objeto na memória. Mudar um atributo desseAutorpor qualquer um dos dois caminhos afeta o mesmo objeto.
Isso é o oposto do que acontece com tipos primitivos, onde atribuir sempre copia o valor.
Definição: Pilha de execução (stack) e heap
A JVM guarda variáveis locais e o controle de chamadas de método na pilha de
execução (stack) — cada método chamado empilha seu próprio quadro, com suas
próprias variáveis locais, removido quando o método retorna. Os objetos em si
(criados com new) vivem no heap, uma área de memória separada e compartilhada;
o que fica na pilha, para uma variável de objeto, é só a referência (o "endereço")
que aponta para o objeto no heap.
Java é sempre pass-by-value — mesmo para objetos¶
Um dos pontos mais mal-entendidos da linguagem: Java nunca passa parâmetros por referência — passa sempre por valor. A pegadinha é que, para uma variável de objeto, o "valor" copiado para o parâmetro é a própria referência (o endereço), não o objeto. Isso tem uma consequência importante:
static void test(Exam exam) {
exam.timeLimit = 210; // MUDA o objeto original — os dois apontam pra ele
}
static void test2(Exam exam) {
exam = new Exam(); // troca só a referência LOCAL a este método
exam.timeLimit = 520; // afeta só o objeto novo, não o original
}
- Mudar um atributo do objeto através do parâmetro afeta o objeto original — porque a cópia da referência ainda aponta para o mesmo objeto no heap.
- Reatribuir o próprio parâmetro (
exam = new Exam();ouexam = null;) só troca para onde a cópia local da referência aponta — a variável original, no método que chamou, continua apontando para o objeto de sempre. Depois quetest2retorna, a referência original está intacta.
Ou seja: dá para mutar o objeto apontado através de um parâmetro, mas não dá para fazer a variável original do chamador apontar para outro objeto de dentro do método chamado — isso é, na prática, o que diferenciaria pass-by-reference de verdade (que Java não tem).
Métodos: isolando comportamento¶
Sem métodos, todo comportamento (como montar uma mensagem, calcular um desconto) fica espalhado pelo código que usa o objeto — e repetido, se mais de um lugar precisar do mesmo comportamento. Um método isola esse comportamento dentro da própria classe:
public class Livro {
String nome;
double valor;
void mostrarDetalhes() {
System.out.println("Nome: " + nome);
System.out.println("Valor: " + valor);
}
}
Um método pode:
- Não retornar nada (
void) — só executa uma ação. - Receber parâmetros — dados que vêm de fora na hora de chamar o método:
- Retornar um valor, com
return— o tipo de retorno declarado na assinatura do método precisa bater com o que é devolvido:
Definição: Assinatura de um método
O nome do método mais os tipos (e ordem) dos parâmetros — não inclui o tipo
de retorno nem o nome dos parâmetros. aplicaDescontoDe(double) e
aplicaDescontoDe(int) são assinaturas diferentes (permitindo sobrecarga); mudar só
o nome de um parâmetro ou só o tipo de retorno não cria uma assinatura nova.
Além da assinatura, um método pode levar outros modificadores — todos opcionais, mas em
uma ordem fixa quando combinados: modificadores tipoDeRetorno nome(parâmetros)
throws Exceções. Em Java (além do modificador de visibilidade, ver Encapsulamento):
| Modificador | Efeito |
|---|---|
final |
não pode ser sobrescrito por subclasses (ver Herança) |
abstract |
obriga subclasses a implementar; sem corpo (ver Classe abstrata) |
static |
pertence à classe, não à instância (ver Java) |
synchronized |
trava a instância para acesso concorrente (multithread) |
native |
implementado em código nativo (fora da JVM), via JNI |
strictfp |
força ponto flutuante a seguir o padrão IEEE 754 à risca, para portabilidade |
Parâmetros não têm valor padrão em Java — todos são obrigatórios em toda chamada
(diferente de outras linguagens que aceitam parâmetro opcional/com valor default). O
único modificador aceito num parâmetro é final, impedindo reatribuição dentro do
método. Argumentos passados também sofrem promoção de tipo (widening) e polimorfismo,
como qualquer atribuição:
void primitivo(double d) { }
primitivo(10); // int promovido a double automaticamente
void referencia(Object o) { }
referencia(new Car()); // Car é um Object, por polimorfismo
Retorno: um método com tipo de retorno diferente de void precisa garantir que
todo caminho possível termine em return (com um valor compatível) ou lance uma
exceção — o compilador não avalia o valor de condições para decidir isso, só a
estrutura do código, então até um if/else if que cobre todos os casos "na prática" dá
erro de compilação se não tiver um return (ou throw) coletando o caso restante:
String method(int a) {
if (a > 0) {
return "> 0";
} else if (a <= 0) {
return "<= 0";
}
// erro de compilação: compilador não sabe que essas duas condições cobrem tudo
}
O retorno de um método pode ser ignorado por quem chama, mesmo que não seja void
— mas o contrário não vale: não é possível atribuir a uma variável o "retorno" de um
método void.
Definição: this
Palavra-chave que se refere ao próprio objeto, dentro de um método. É opcional na
maioria dos casos, mas necessária quando o nome de um parâmetro é igual ao nome de um
atributo da classe — sem this, o compilador entende que você está falando do
parâmetro (menor escopo), não do atributo:
public void aplicaDescontoDe(double valor) {
this.valor -= this.valor * valor; // this.valor = atributo; valor = parâmetro
}
this para acessar atributos é
considerado boa prática — deixa explícito, pra quem lê, que aquilo é um atributo da
classe.
Encapsulamento¶
Encapsulamento é o pilar de OO que trata de esconder os detalhes internos de uma
classe, expondo só o que é necessário. Na prática, isso significa: atributos ficam
privados (o modificador de visibilidade private), e o acesso a eles acontece só através
de métodos.
public class Livro {
private double valor; // não é mais acessível diretamente de fora da classe
public double getValor() {
return valor;
}
public void setValor(double valor) {
this.valor = valor;
}
}
Definição: Getter e Setter
Métodos convencionais para ler (get) e atribuir (set) um atributo privado. Um
getX() não recebe parâmetro e retorna o atributo; um setX(valor) recebe um
parâmetro do mesmo tipo do atributo e o atualiza. É a forma padrão de dar acesso
controlado a um atributo private sem quebrar o encapsulamento — mas criar um
getter/setter para todo atributo, sempre, não é obrigatório nem sempre desejável:
crie-os apenas quando existir uma necessidade real de leitura/escrita externa.
Um bom teste para saber se uma classe está bem encapsulada: olhando de fora pra ela, você consegue responder o quê ela faz, mas não como ela faz. Os métodos públicos de uma classe (sua interface) são a única forma de interagir com seus objetos — por isso, mudar a implementação interna de um método não deveria afetar quem o usa.
Definição: Modificadores de acesso
Palavras-chave que controlam de onde um atributo, método, construtor ou classe pode ser acessado. Do mais restrito ao mais aberto, cada nível inclui o anterior:
private— só dentro da própria classe.- default (sem palavra-chave nenhuma) — também dentro do mesmo pacote inteiro.
protected— também para subclasses, mesmo que estejam em outro pacote (ver Herança).public— de qualquer lugar do projeto.
Regra prática: classes costumam ser public (senão ficam invisíveis para outros
pacotes); atributos costumam ser private (encapsulamento); o que fica
protected ou default é decisão pontual, quando existe uma razão específica para
abrir esse acesso a subclasses ou ao pacote.
Detalhes que costumam pegar de surpresa:
- Só é permitido um modificador de acesso por vez (
private public int x;é erro de compilação). - Uma classe/interface top-level (não aninhada dentro de outra) só aceita
publicou default — nuncaprivatenemprotected. Classes aninhadas (nested/inner classes) podem usar os quatro, por serem membros da classe que as contém. - Variáveis locais e parâmetros não podem ter modificador de acesso (só aceitam
outros modificadores, como
final) — acesso só faz sentido para membros de classe. - Se a classe é default (invisível fora do pacote), isso vale para tudo
dentro dela, não importa que os membros individuais sejam
public— o acesso à classe é sempre o primeiro filtro.
Definição: protected entre pacotes — só através de this ou do próprio tipo
Um detalhe fino: uma subclasse em outro pacote só acessa um membro
protected herdado através de this (implícito ou explícito) ou de uma
referência do seu próprio tipo (ou subtipo) — nunca através de uma
referência tipada como a superclasse, mesmo vindo de dentro da própria
subclasse:
package another;
class Triangle extends Shape {
public void printSide() {
System.out.println(side); // ok — acesso implícito via this
Shape myself = (Shape) this;
System.out.println(myself.side); // erro de compilação!
}
}
Shape (a superclasse, de outro
pacote), a referência myself só enxerga o que Shape expõe naquele
pacote — e side não é public lá. this/Triangle funciona porque o
acesso é resolvido pelo tipo real (Triangle), não pela superclasse.
A palavra default em Java tem múltiplos significados dependendo de onde aparece
(caso padrão de switch, valor padrão de uma @Annotation, default method de
interface) — mas não existe como palavra-chave para declarar acesso: para dar
acesso default a um membro, você simplesmente omite qualquer modificador; não
existe default int x;.
Construtor¶
Um construtor é um método especial, chamado no momento em que um objeto é criado com
new — tem o mesmo nome da classe e não declara tipo de retorno:
Se você não declarar nenhum construtor, o compilador cria um construtor vazio (sem
parâmetros, sem corpo) automaticamente — é por isso que new Livro() sempre funciona,
mesmo sem você ter escrito um construtor. Todo código dentro do construtor roda toda vez
que um objeto daquele tipo é criado.
Definição: Construtor x método com o mesmo nome da classe
É permitido ter um método com o mesmo nome da classe — o que o diferencia de um
construtor é só a presença de um tipo de retorno (mesmo void já basta):
class Executor {
Executor() { } // construtor: sem tipo de retorno
void Executor() { } // método comum: tem tipo de retorno (void)
}
return; vazio (sem valor) para interromper sua
execução mais cedo — o que não é permitido é um return valor; com valor, já que
construtor não tem tipo de retorno.
O construtor gerado automaticamente pelo compilador (quando nenhum é declarado) tem a
mesma visibilidade da classe e, se houver superclasse, chama super() implicitamente
como primeira instrução — mesmo sem você escrever nada disso.
A ordem de inicialização de uma instância é sempre: 1. atributos recebem seus valores
default; 2. atributos com inicializador na própria declaração (int i = 15;) são
inicializados, na ordem em que aparecem no arquivo; 3. o corpo do construtor roda.
Definição: Perigo de chamar método sobrescrevível dentro de um construtor
Chamar, de dentro de um construtor, um método de instância que pode ser
sobrescrito por uma subclasse é arriscado: se uma subclasse sobrescrever esse
método e o novo código acessar um atributo daquela subclasse — que ainda não foi
inicializado, porque o construtor da subclasse nem começou a rodar —, o resultado é
tipicamente um NullPointerException em tempo de construção do objeto:
class Base {
String name;
Base() {
test(); // chama um método que PODE estar sobrescrito
name = "guilherme";
}
void test() {
System.out.println("testing");
}
}
class Filho extends Base {
@Override
void test() {
System.out.println(name.length()); // NullPointerException — name ainda é null
}
}
test() for private (não pode ser sobrescrito — binding é resolvido em
tempo de compilação, sempre para a versão da própria classe), esse risco desaparece.
Regra prática: evite chamar métodos sobrescrevíveis (não private, não final, não
static) de dentro de um construtor.
Construtores também recebem parâmetros — útil para obrigar que uma dependência seja passada logo na criação do objeto, em vez de deixar o objeto existir num estado incompleto:
Importante: assim que você declara qualquer construtor com parâmetro, o compilador para de gerar o construtor vazio automaticamente. Se seu código também precisar permitir criar o objeto sem argumentos, você precisa declarar esse construtor vazio explicitamente.
Definição: Sobrecarga (Overload)
Ter mais de um método (ou construtor) com o mesmo nome na mesma classe, desde que a lista de parâmetros seja diferente (em quantidade ou tipo). O compilador decide qual versão chamar de acordo com os argumentos passados, em tempo de compilação. Construtores podem ser sobrecarregados do mesmo jeito que métodos comuns.
Regras de resolução que costumam confundir:
- Só o tipo de retorno não basta para diferenciar uma sobrecarga — dois métodos com mesmo nome e mesmos parâmetros, mas retornos diferentes, é erro de compilação (ambíguo demais: quem chama sem usar o retorno não teria como o compilador saber qual invocar).
- Quando mais de uma versão sobrecarregada é compatível com os argumentos passados
(por widening ou por polimorfismo), o compilador escolhe a mais específica
— o tipo que exige menos "promoção":
Para forçar a versão mais genérica mesmo tendo uma mais específica disponível, use casting explícito no argumento:
void method(Object o) { } void method(String s) { } method("random"); // chama method(String) — mais específico que Objectmethod((Object) "random"). - Se nenhuma versão for claramente mais específica (ex.: sobrecargas simétricas
method(String, double)emethod(double, String), chamadas com doisintquaisquer — ambos os parâmetros podem ser promovidos igualmente para os dois lados), a chamada é ambígua e não compila.
Quando uma classe tem mais de um construtor, um pode delegar para o outro com this(...)
— útil para evitar repetir a mesma lógica de inicialização em cada versão:
public Livro(Autor autor) {
this(); // chama o construtor sem parâmetros primeiro
this.autor = autor;
}
public Livro() {
this.isbn = "000-00-00000-00-0";
}
Regras importantes sobre this(...):
- Só pode ser a primeira instrução do construtor — e só pode aparecer uma vez
(não é possível ter duas chamadas
this(...)seguidas no mesmo construtor). - Os argumentos passados podem envolver expressões e chamadas a métodos estáticos,
mas não a métodos de instância — o objeto ainda não foi construído, então não
existe
thisde verdade para invocar um método de instância nele ainda. - Uma cadeia de
this(...)que chama a si mesma, direta ou indiretamente, não compila (o compilador detecta a recursão de construtores). Uma recursão que não seja viathis(...), no entanto — como criar umnewda própria classe de dentro do construtor — compila normalmente e estoura emStackOverflowErrorem tempo de execução, já que o compilador não analisa esse tipo de ciclo.
Um construtor também pode ser private — um padrão comum para forçar a criação de
objetos só através de um método estático (factory method) da própria classe, em vez de
new direto:
Um uso comum de construtor é inicializar um atributo com um valor padrão, em vez de
deixá-lo null:
Isso importa porque, diferente dos tipos primitivos — que sempre têm um valor padrão
(0 para números, false para boolean) — atributos de tipo objeto (como String)
começam como null quando não são explicitamente inicializados.
Vantagens do paradigma¶
Comparado com escrever tudo dentro de um único método main (estilo procedural), isolar
comportamento em classes e objetos traz:
- Menos repetição — o comportamento vive em um único lugar (o método), não espalhado em toda parte do código que precisa dele.
- Manutenção mais fácil — mudar uma regra de negócio significa mudar em um lugar só, não em todos os pontos onde ela foi repetida.
- Conexão natural entre dado e comportamento — em vez de dados soltos manipulados por funções externas, o objeto sabe o que ele é e o que sabe fazer.
Esses ganhos ficam ainda mais evidentes com os próximos pilares do paradigma: herança e polimorfismo.
Relações entre objetos: associação, agregação e composição¶
Objetos colaboram. Escolher a relação certa depende de propriedade e ciclo de vida: o objeto apenas conhece o outro, compartilha-o ou o tem como parte inseparável.
| Relação | Pergunta | Ciclo de vida | UML simplificada |
|---|---|---|---|
| Associação | Um objeto conhece ou usa outro? | Independentes | A -> B |
| Agregação | O todo agrupa partes que existem sozinhas? | A parte pode existir sem o todo | A o-- B |
| Composição | A parte pertence ao todo e não faz sentido sem ele? | A parte acompanha o ciclo de vida do todo | A *-- B |
Definição: Relação 'tem-um' (has-a)
Quando um objeto contém ou referencia outro como atributo (um Pedido tem itens;
um Carro tem um motor). Contrasta com a relação "é-um" (is-a), que é a
herança (um Gerente é um Funcionario). Para "tem-um", prefira composição;
veja Herança ou composição?.
class Pedido {
private final List<ItemPedido> itens = new ArrayList<>();
public void adicionar(Produto produto, int quantidade) {
itens.add(new ItemPedido(produto, quantidade)); // Pedido cria e possui os itens
}
public double total() {
return itens.stream().mapToDouble(ItemPedido::subtotal).sum();
}
}
Aqui Pedido ⟷ ItemPedido é composição (o item nasce e morre com o pedido), e
ItemPedido → Produto é associação (o produto continua existindo no catálogo).
Exemplo: Turma, Aluno e Matrícula
Aluno existe sozinho (se a turma for encerrada, ele continua existindo); a
Matricula depende da ligação entre Aluno e Turma e só faz sentido enquanto as
duas existem.
Erros comuns: usar herança quando a relação é "tem-um"; criar dependência circular sem necessidade; deixar qualquer classe modificar a lista interna (devolva uma cópia ou uma visão imutável — veja Encapsulamento).
Herança e polimorfismo¶
Herança permite que uma classe reaproveite atributos e métodos de outra, declarando
que é um tipo dela. Em Java, isso se expressa com extends:
public class Ebook extends Livro {
private String waterMark;
public Ebook(Autor autor) {
super(autor);
}
}
Ebook (a subclasse) herda tudo que Livro (a superclasse) tem — mesmo sem
declarar nada além do que é específico dela (waterMark). A palavra super delega para
o construtor (ou método) da superclasse, evitando repetir uma lógica que já existe lá.
Definição: Herança única (Java)
Diferente de C++, uma classe em Java só pode estender uma superclasse diretamente (sem herança múltipla). Uma classe pode, porém, herdar de uma classe que já herda de outra, formando uma cadeia — mas cada elo dessa cadeia aumenta o acoplamento entre as classes, então vale a pena usar com moderação.
Duas restrições que o compilador garante:
- Uma classe
finalnão pode ser estendida —extendssobre uma classefinalé erro de compilação. (Uma classe pode, ao mesmo tempo, ser ela mesmafinale estender outra classe normalmente — a restrição é só sobre quem tenta herdar dela.) - Se a superclasse não tiver um construtor sem argumentos, toda subclasse é
obrigada a declarar pelo menos um construtor que chame explicitamente
super(argumentos)compatível com algum construtor da mãe — o construtor implícito do compilador só chamasuper()sem argumentos, e isso não compila se esse construtor não existir na superclasse.
Sobrescrevendo métodos (@Override)¶
Uma subclasse pode redefinir o comportamento de um método herdado — isso é sobrescrita (override), diferente de sobrecarga (que é ter o mesmo nome com parâmetros diferentes):
@Override
public boolean aplicaDescontoDe(double porcentagem) {
if (porcentagem > 0.15) {
return false;
}
return super.aplicaDescontoDe(porcentagem);
}
A anotação @Override é opcional, mas recomendada: ela não muda o comportamento em
tempo de execução, só faz o compilador confirmar que aquele método realmente existe na
superclasse e está sendo sobrescrito corretamente — evitando o erro sutil de "criar sem
querer um método novo" por errar a assinatura.
Repare que, dentro do método sobrescrito, dá pra chamar a versão original com
super.metodo(...) — útil quando você quer complementar o comportamento da
superclasse, não substituí-lo por completo.
Definição: Regras completas para uma sobrescrita válida
Para o compilador aceitar um método como sobrescrita real (e não um método novo, ou um erro), todas essas regras precisam valer ao mesmo tempo:
- Mesmo nome, exatamente.
- Mesmos tipos de parâmetro, na mesma ordem (o nome dos parâmetros pode mudar).
- Retorno igual, ou covariante — a classe filha pode devolver um tipo mais específico (um subtipo) do que a mãe declarou, mas só para tipos de referência (retorno covariante não existe para tipos primitivos):
- Visibilidade igual ou mais aberta que a da mãe — nunca mais restrita. Métodos
de interface são implicitamente
public, então a implementação também precisa serpublic(não pode "afrouxar para baixo"). - Exceptions checked iguais ou menos abrangentes que as da mãe (pode lançar
menos, ou versões mais específicas na hierarquia de
Exception— nunca uma mais genérica nem uma nova sem relação). Exceptions unchecked (RuntimeExceptione suas filhas) não têm essa restrição — podem ser adicionadas livremente. - Não é possível sobrescrever um método
finalda superclasse. - O método sobrescrito pode ser declarado
abstract— empurrando a obrigação de implementar adiante na hierarquia (ver Classe abstrata).
O nome técnico completo para "qual versão de um método sobrescrito roda" é virtual method invocation — o binding (qual código realmente executa) só é resolvido em tempo de execução; em tempo de compilação, só se sabe a assinatura.
Definição: Quando um método 'sobrescrito' não é sobrescrita de verdade
Se o método da superclasse for private, ou tiver visibilidade default e a
subclasse estiver em outro pacote, a subclasse simplesmente não enxerga esse
método herdado — então declarar um método de mesmo nome/assinatura na subclasse não
é sobrescrita nem hiding: é um método novo, totalmente independente, sem
nenhuma relação com o da mãe. Consequência prática: qual método roda passa a
depender de qual referência/pacote faz a chamada — o mesmo tipo de binding
"errado" que costuma pegar quem assume que todo método de mesmo nome está
relacionado por herança.
Polimorfismo¶
Polimorfismo é a capacidade de tratar objetos de tipos diferentes (mas relacionados por herança) de forma uniforme, através do tipo da superclasse:
public void adiciona(Livro livro) { // aceita Livro, Ebook, LivroFisico, ...
livro.aplicaDescontoDe(0.05);
}
O ponto-chave: qual versão do método é executada é decidido em tempo de execução
(runtime), com base no tipo real do objeto — não no tipo da variável/parâmetro usado
para referenciá-lo. Mesmo adiciona recebendo um Livro, se o objeto passado for
de fato um Ebook, é o aplicaDescontoDe do Ebook que roda.
graph TD
A["adiciona(Livro livro)"] -->|"objeto real é Ebook"| B["Ebook.aplicaDescontoDe()"]
A -->|"objeto real é LivroFisico"| C["LivroFisico (herda de Livro)"]
Isso é o que permite CarrinhoDeCompras ter um único método adiciona(Livro livro) em
vez de um método sobrecarregado para cada subtipo de Livro — cada objeto sabe se
comportar como o seu próprio tipo, mesmo referenciado de forma mais genérica.
Se for necessário o comportamento específico de uma subclasse (que não existe na superclasse), é preciso um casting explícito de volta para o tipo concreto — e isso é arriscado: só funciona se o objeto realmente for daquele tipo em tempo de execução.
Definição: O tipo da referência limita quais métodos podem ser chamados
Polimorfismo decide qual implementação roda, mas não muda quais métodos você pode chamar — isso continua limitado ao que o tipo declarado da referência expõe, mesmo que o objeto real tenha mais métodos:
class Vehicle { void turnOn() { ... } }
class Car extends Vehicle { void turnOn() { ... } void turnOff() { ... } }
Vehicle v = new Car(); // referência do tipo Vehicle, objeto real é Car
v.turnOn(); // ok — Vehicle declara turnOn (roda a versão de Car)
v.turnOff(); // erro de compilação — Vehicle não declara turnOff
((Car) v).turnOff(); // ok, com casting explícito de volta para Car
Casting entre tipos relacionados por herança¶
Considere três classes: Vehicle (mãe), Car e Motorcycle (as duas filhas,
"irmãs" entre si, sem relação direta uma com a outra):
- Upcasting (filha → mãe) é sempre automático, nunca precisa de casting explícito —
é justamente o polimorfismo (
Vehicle v = new Car();). - Downcasting (mãe → filha) sempre precisa de casting explícito, e o compilador só aceita se existir um caminho possível na hierarquia — mesmo que não seja garantido:
- Casting entre tipos sem relação nenhuma na hierarquia (ex.:
CarparaMotorcyclediretamente, sabendo que uma classe não pode herdar de duas) é erro de compilação — o compilador consegue provar que é impossível, então nem tenta em tempo de execução:
Definição: Casting de classe para interface quase sempre compila
Diferente do casting entre duas classes, um casting de uma classe para uma interface que ela não implementa geralmente compila mesmo assim — porque o compilador não pode descartar a possibilidade de existir, em algum lugar, uma subclasse que implemente essa interface também:
class Car extends Vehicle { }
Car c = new Car();
Runnable r = (Runnable) c; // compila (Car não é final, poderia ter uma subclasse
// que implementasse Runnable) — ClassCastException em runtime
final (não pode ter subclasses) e ela mesma
não implementar a interface, não existe caminho possível nenhum, e o casting não
compila — igual ao casting entre classes sem relação.
Definição: instanceof
Operador que testa se uma referência aponta para um objeto de um tipo (ou subtipo)
específico, devolvendo boolean — sem lançar exceção, mesmo quando dá false. Só
não compila se os tipos envolvidos forem obviamente incompatíveis (ex.: uma
String não pode ser instanceof List, o compilador já sabe isso estaticamente).
Muito usado logo antes de um casting explícito, para evitar ClassCastException.
Prático, mas use com moderação: código cheio de if (x instanceof Y) em cadeia,
decidindo comportamento pelo tipo concreto, geralmente é sinal de uma modelagem
fraca — é exatamente o tipo de decisão que o polimorfismo (sobrescrita de método)
deveria estar resolvendo sozinho, sem precisar perguntar "que tipo é você?" em
tempo de execução.
Definição: O que não é polimórfico: static e atributos
Polimorfismo (resolução em tempo de execução, pelo tipo real do objeto) vale só para métodos de instância sobrescritos. Dois casos parecidos, mas que funcionam diferente:
- Métodos
static: não existe herança de método estático de verdade — uma subclasse pode declarar um métodostaticde mesmo nome, mas isso é redefinição (method hiding), não sobrescrita. A chamada é resolvida em tempo de compilação, pelo tipo da referência (ver Java): - Atributos: também não são polimórficos. Se a subclasse declara um atributo com
o mesmo nome de um da superclasse, os dois coexistem como campos diferentes —
não é sobrescrita, é shadowing de atributo. Qual dos dois você enxerga depende
do tipo da referência usada para acessá-lo, exatamente como em
static, não do tipo real do objeto. Dentro da própria subclasse,this.atributoacessa o campo dela mesma esuper.atributoacessa explicitamente o da superclasse:
Consequência prática: abstract não é permitido em método static (não existiria
"subclasse obrigada a implementar" sem herança real de método), e super.metodo()
não pode ser usado dentro de um método static (não há um this/objeto atual para
servir de ponto de partida da busca na hierarquia).
Herança ou composição?¶
Preferir composição (uma classe tem outra, como atributo) a herança (uma classe
é outra) quando possível — herança aumenta o acoplamento entre superclasse e subclasses
e tende a comprometer o encapsulamento (para dar acesso a atributos da superclasse às
subclasses, é comum precisar afrouxar private para protected — ver Modificadores de
acesso, na seção de Encapsulamento acima). Herança faz mais sentido
quando existe uma relação genuína de "é um" bem estável; composição é geralmente mais
flexível para "tem um"/"usa um".
Classe abstrata¶
Às vezes uma superclasse existe só para ser herdada — nunca deveria virar objeto sozinha.
No exemplo deste guia, Livro é sempre LivroFisico, Ebook ou outro tipo concreto;
"um Livro genérico, sem mais nada" não representa nada de real no domínio.
Uma classe abstrata (abstract class) impede exatamente isso: o compilador não deixa
instanciá-la diretamente com new, mesmo que ela continue funcionando normalmente como
tipo para variáveis, parâmetros e polimorfismo:
Além de impedir instanciação direta, uma classe abstrata pode declarar métodos abstratos — um método sem corpo, que toda subclasse concreta é obrigada a implementar:
public abstract class Livro {
public abstract boolean aplicaDescontoDe(double porcentagem);
}
public class LivroFisico extends Livro {
@Override
public boolean aplicaDescontoDe(double porcentagem) {
// implementação obrigatória
return true;
}
}
Isso resolve um problema real de manter várias subclasses: sem método abstrato, é fácil esquecer de sobrescrever um comportamento numa subclasse nova e ela acabar herdando um padrão que não faz sentido para ela. Com o método abstrato, o compilador garante que nenhuma subclasse concreta escapa dessa responsabilidade.
Definição: Regras de classe/método abstrato
- Uma classe pode ser abstrata sem ter nenhum método abstrato (só impede instanciação).
- Um método abstrato só pode existir dentro de uma classe abstrata.
- Uma classe abstrata pode ter métodos abstratos e concretos ao mesmo tempo.
- Toda subclasse concreta (não abstrata) é obrigada a implementar todos os métodos
abstratos herdados — a menos que ela também seja declarada
abstract, empurrando essa obrigação adiante na hierarquia.
Interface¶
Uma interface é um contrato: define quais métodos uma classe deve ter, sem dizer como eles são implementados. É parecida com uma classe abstrata que só tem métodos abstratos — mas resolve um problema que classe abstrata não resolve, o de acoplar classes que não têm relação de herança genuína entre si.
public interface Produto {
double getValor();
}
public class Livro implements Produto {
// é obrigado a implementar getValor()
}
public class Revista implements Produto {
// é obrigado a implementar getValor()
}
Diferenças-chave em relação a herdar de uma classe abstrata:
- Uma classe usa
implements(em vez deextends) para uma interface, e pode implementar várias interfaces ao mesmo tempo — diferente de herança de classe, que em Java é única. - Até o Java 7, uma interface não podia ter métodos com implementação — só assinaturas de
métodos, implicitamente
public abstract(não precisa escrever esses modificadores). - Uma interface pode
extendsmúltiplas outras interfaces ao mesmo tempo — só para classes concretas essa restrição de herança única existe:Mas uma interface nunca usainterface A extends Runnable {} interface B extends Serializable {} interface C extends Runnable, Serializable {} // ok — várias de uma vezimplements— sóextends, mesmo estendendo várias.
Definição: Constantes em interface
Uma interface pode declarar campos, mas eles são sempre implicitamente public
static final — ou seja, são constantes compartilhadas, não atributos de
instância (interface não guarda estado de objeto):
graph TD
P["«interface» Produto"] --- Livro
P --- Revista
Pr["«interface» Promocional"] --- Livro
Pr --- Revista
Isso permite reduzir acoplamento: em vez de forçar Livro e Revista a compartilhar uma
superclasse comum só para ter polimorfismo, cada uma assina só os contratos
(interfaces) que realmente fazem sentido para ela — por exemplo, nem todo Produto
precisa ser Promocional.
Novidades do Java 8: default methods¶
Desde o Java 8, uma interface pode ter métodos com implementação — um default
method, marcado com a palavra default:
public interface Promocional {
boolean aplicaDescontoDe(double porcentagem);
default boolean aplicaDescontoDe10Porcento() {
return aplicaDescontoDe(0.1);
}
}
Isso permite evoluir uma interface (adicionar um método novo) sem quebrar todo mundo que
já a implementa — sem default, toda classe existente que implementasse Promocional
pararia de compilar até implementar o método novo.
Definição: Interface funcional
Interface com um único método abstrato (default methods não contam). Pode ser
marcada explicitamente com @FunctionalInterface — o compilador então valida que a
regra é respeitada e acusa erro se alguém adicionar um segundo método abstrato. É o
que viabiliza expressões lambda e method references em Java (um lambda é, por baixo
dos panos, uma implementação de uma interface funcional).
Definição: 'Herança múltipla' de interfaces
Default methods aproximam Java de herança múltipla, mas não é a mesma coisa: eles não podem acessar atributos de instância (interfaces não têm estado) — ou seja, não há compartilhamento de estado entre interfaces, só reaproveitamento de comportamento sem dependência de dados.
Abstração: modelar o essencial¶
Abstrair é representar só o que importa para o problema. Uma classe abstrata pode
reunir o contrato (métodos abstratos) e uma implementação compartilhada (métodos
concretos) — aplicando o padrão Template Method (ver
Padrões Arquiteturais):
o método final fixa o roteiro e as subclasses preenchem só os passos que variam.
abstract class Pagamento {
public final boolean executar(double valor) { // roteiro fixo
if (valor <= 0) return false;
validar(valor);
processar(valor);
return true;
}
protected void validar(double valor) { } // gancho opcional
protected abstract void processar(double valor); // cada subclasse implementa
}
class PagamentoPix extends Pagamento {
protected void processar(double valor) { System.out.println("PIX processado"); }
}
| Construção | Característica |
|---|---|
| Classe concreta | Pode ser instanciada |
| Classe abstrata | Não é instanciada; serve de base |
| Método abstrato | Declara o que deve ser implementado |
| Método concreto | Já tem comportamento |
Erros comuns: criar abstrações antes de existir variação real; colocar na classe base detalhes de todas as subclasses; confundir "abstrato" com "incompleto".
Interface x classe abstrata: como escolher¶
| Interface | Classe abstrata |
|---|---|
| Contrato de capacidade; várias implementações, mesmo sem hierarquia comum | Base com estado e comportamento comuns |
implements: assume a obrigação |
extends: herda a base |
- Prefira interfaces pequenas e coesas (evite dezenas de métodos).
- O serviço deve depender do contrato, não do banco ou de uma classe concreta
(
Servico → Repositorio←RepositorioEmMemoria/RepositorioBanco). - Uma interface sem contrato nem variação útil é ruído.
interface Repositorio<T> {
void salvar(T objeto);
Optional<T> buscarPorId(long id);
}
class RepositorioEmMemoria implements Repositorio<Produto> {
private final Map<Long, Produto> dados = new HashMap<>();
public void salvar(Produto p) { dados.put(p.getId(), p); }
public Optional<Produto> buscarPorId(long id) { return Optional.ofNullable(dados.get(id)); }
}
Estudo de caso: loja virtual (OO de ponta a ponta)¶
Missão: modelar catálogo, carrinho, pedido, pagamento e notificação sem concentrar tudo em uma classe gigante. Resultado esperado: classes coesas, relações explícitas e pontos de extensão.
1. Descubra o domínio¶
Comece pelos substantivos e regras do problema, não pelas telas. Um substantivo só vira classe quando tem identidade, estado ou comportamento relevante.
2. Distribua as responsabilidades¶
| Classe | Responsabilidade |
|---|---|
Produto |
Conhecer preço atual e disponibilidade |
Carrinho |
Adicionar, remover e calcular prévia |
Pedido |
Congelar itens e total da compra |
Pagamento |
Autorizar a cobrança |
RepositorioPedido |
Salvar e recuperar pedidos |
Notificador |
Comunicar a confirmação ao cliente |
Evite: um LojaService que faz catálogo, estoque, carrinho, cobrança, banco e e-mail.
3. Conecte os objetos por contratos¶
classDiagram
class FinalizadorPedido {
+finalizar(pedido)
}
class Pagamento {
<<interface>>
+cobrar(valor)
}
class Notificador {
<<interface>>
+enviar(msg)
}
FinalizadorPedido --> Pagamento : usa
FinalizadorPedido --> Notificador : usa
Pagamento <|.. PagamentoPix
Notificador <|.. Email
Esse desenho permite trocar PIX por cartão e e-mail por SMS sem alterar a regra do pedido, e testar o fluxo com implementações falsas e rápidas.
4. Modele o pedido (o objeto protege o próprio estado)¶
class Pedido {
private final List<ItemPedido> itens;
private StatusPedido status = StatusPedido.CRIADO;
Pedido(List<ItemPedido> itens) {
if (itens.isEmpty()) throw new IllegalArgumentException("Pedido vazio");
this.itens = List.copyOf(itens); // cópia defensiva: ninguém altera por fora
}
double total() {
return itens.stream().mapToDouble(ItemPedido::subtotal).sum();
}
void marcarComoPago() {
if (status != StatusPedido.CRIADO) throw new IllegalStateException();
status = StatusPedido.PAGO; // transição validada pelo próprio pedido
}
}
Decisões de design: List.copyOf impede alteração externa da coleção; o pedido nunca
nasce vazio; a transição de status é validada pelo próprio objeto.
5. Coordene por contratos¶
interface Pagamento { void cobrar(double valor); }
interface Notificador { void enviar(String mensagem); }
class FinalizadorPedido {
private final Pagamento pagamento;
private final Notificador notificador;
FinalizadorPedido(Pagamento pagamento, Notificador notificador) {
this.pagamento = pagamento;
this.notificador = notificador;
}
void finalizar(Pedido pedido) {
pagamento.cobrar(pedido.total());
pedido.marcarComoPago();
notificador.enviar("Pedido confirmado");
}
}
O finalizador coordena; o pedido protege o estado; pagamento e notificação variam por meio de interfaces (injeção de dependência pelo construtor).
6. Teste o comportamento¶
Teste regras observáveis, cada cenário com preparação, ação e resultado:
| Cenário | Esperado |
|---|---|
| Criar pedido sem itens | Rejeitar |
| Dois itens | Total soma os subtotais |
| Finalizar | Cobrar o valor total; depois marcar como pago |
| Sucesso | Enviar confirmação |
| Cobrança falha | Não confirmar nem notificar |
Use implementações falsas (fakes) de Pagamento e Notificador para verificar as
chamadas sem serviços externos (ver Qualidade).
7. Evolua sem desmontar¶
Uma boa estrutura recebe novas regras em pontos previsíveis: novo pagamento
(PagamentoCartao), novo canal (NotificadorSms), cupom (política de desconto), frete
(contrato CalculadoraFrete), persistência (RepositorioPedido no banco) e auditoria
(observador de eventos). Sinal de saúde: adicionar uma opção exige criar uma
classe pequena, não editar dezenas de condicionais.
Quadro de decisão e checklist¶
| Pergunta | Use |
|---|---|
| Preciso criar algo concreto? | Classe + objeto |
| Preciso proteger uma regra? | Encapsulamento |
| É uma relação "é-um"? | Herança (com cautela) |
| É uma relação "tem-um"? | Composição / associação |
| Várias respostas ao mesmo pedido? | Polimorfismo |
| Quero definir uma capacidade? | Interface |
| Há base comum e implementação parcial? | Classe abstrata |
| Um detalhe externo pode mudar? | Depender de contrato |
Regra de ouro
POO não é decorar palavras: é distribuir responsabilidades para que cada objeto
proteja suas próprias regras. A pergunta-guia é "qual objeto deveria conhecer esta
informação e proteger esta regra?" — se a resposta é sempre Main, as
responsabilidades ainda não foram modeladas.
Checklist final: cada classe tem um propósito claro; o estado importante está protegido; as relações representam o domínio; as variações dependem de contratos; os testes descrevem comportamento. Próximo passo: escolher um projeto real e desenhar os objetos antes de abrir a IDE.
Quinze boas práticas de OO¶
| # | Boa prática | Em resumo |
|---|---|---|
| 1 | Coesão e acoplamento | Cada classe com um propósito; dependa de uma abstração (Pagamento), não de cada forma concreta (Debito, Cartao) |
| 2 | Strings com parcimônia | Não guarde estrutura em texto (endereço inteiro numa String): crie Endereco com logradouro, numero, bairro; buscar dentro de texto é frágil e fere a coesão |
| 3 | Seja objetivo, não preveja o futuro | Não crie Pessoa → PessoaFisica se só existirá pessoa física (YAGNI); herança sem necessidade só acopla a subclasse à superclasse |
| 4 | Crie métodos com carinho | Poucos parâmetros (agrupe em objeto), nome claro, uma tarefa; parâmetros desassociados aumentam o acoplamento |
| 5 | Conheça e use coleções | Em vez de arrays manuais, List, Set, Map e suas garantias (ordem, unicidade, busca por chave) |
| 6 | Sobrescreva equals, hashCode e toString |
Os "três mosqueteiros": sem equals/hashCode, coleções e comparações se comportam de forma inesperada; sem toString, a exibição é ilegível; espalhar a lógica de igualdade pelo código fere o encapsulamento. equals e hashCode devem ser consistentes entre si |
| 7 | Às vezes, associe em vez de herdar | Reúso não exige herança: composição é mais flexível (Herança x composição) |
| 8 | Evite herança ou limite-a | final (ou sealed) em classes que não devem ser estendidas; final em métodos que não podem ser sobrescritos; final em atributos os torna constantes |
| 9 | Encapsulamento | Atributos privados; trate quatro situações: acesso de leitura, de escrita, e as coleções/objetos mutáveis expostos |
| 10 | Interface x classe abstrata no momento certo | Interface: contrato, múltiplas implementações, "herança múltipla"; abstrata: base comum com implementação parcial e estado (quadro) |
| 11 | Evite especializar o já especializado | Cadeias profundas de herança concreta → concretas herdando de concretas ficam frágeis; uma abstrata herdando de concreta não faz sentido |
| 12 | Membros estáticos com parcimônia | Métodos estáticos não participam de polimorfismo nem do estado do objeto ("quebra de interface"); evite-os nas entidades de negócio, e use-os em utilitários sem estado |
| 13 | Entenda a clonagem de objetos | Atribuir copia a referência, não o objeto; para copiar, use construtor de cópia ou clone() consciente da cópia rasa x profunda |
| 14 | Use as facilidades da linguagem | for-each em vez de índices, try-with-resources, Optional, StringBuilder... Quem chega do estruturado leva hábitos que a linguagem já resolve melhor |
| 15 | Siga as convenções | Java: camelCase para métodos e variáveis, PascalCase para classes, MAIUSCULAS_COM_UNDERSCORE para constantes (Clean Code) |
Aprofundamentos¶
- A classe
Object: raiz de todas as classes em Java; forneceequals,hashCode,toString,getClass,clone(efinalize, obsoleto). Qualquer objeto é umObject. - Classes internas: membro (dentro da classe, acessa seus atributos), estática aninhada (não depende de uma instância da externa), local (dentro de um método) e anônima (sem nome, criada na hora; hoje muitas viram lambdas).
- Ligação dinâmica (polimorfismo): o método executado é escolhido em tempo de execução pelo tipo real do objeto, e não pelo tipo da variável — é o que permite
pagamento.pagar()funcionar para qualquer subtipo. - Depois da OO: padrões de projeto (Padrões), refatoração (Boas práticas) e UML (Diagramas UML).