Java Polimorfismo
Polimorfismo é a ideia que dá sentido à herança e à sobrescrita. Ela permite tratar um cachorro, um gato e uma vaca simplesmente como "animais": guardar todos numa mesma lista, passar qualquer um para o mesmo método e, mesmo assim, cada um se comportar do seu jeito. O nome assusta, mas a ideia é do dia a dia. Nesta aula você vai ver como ela funciona, qual método o Java escolhe e como "voltar" de Animal para Cachorro sem quebrar o programa.
- Como uma variável do tipo da superclasse pode guardar um objeto da subclasse.
- Qual método roda quando há sobrescrita: o despacho dinâmico.
- Como fazer uma lista de
AnimalcomCachorro,GatoeVaca, e métodos que aceitam qualquer animal. - O que o tipo da variável deixa (e não deixa) chamar.
- Como converter de volta (casting), a
ClassCastExceptione oinstanceof.
Esta aula usa Herança e Sobrescrita. Os exemplos usam a família de animais dessas aulas.
A ideia: "chame o veterinário para o animal"
A Ana tem uma clínica veterinária. Na agenda está escrito "atender um animal às 10h". A recepcionista não precisa saber se é cachorro ou gato para marcar a consulta: qualquer animal serve. Mas, na hora da consulta, se a veterinária pede "faça um som", o cachorro late e o gato mia. A agenda trata todos como "animal"; cada um continua sendo o que é.
Polimorfismo vem do grego: poli = muitas, morphé = formas. "Muitas formas": uma mesma chamada (emitirSom()) assume a forma do objeto que a recebe. Em Java, ele aparece em duas peças:
- Uma variável (ou lista, ou parâmetro) do tipo
Animalpode guardar qualquer subclasse deAnimal. - Quando você chama um método sobrescrito por essa variável, roda a versão do objeto, e não a do tipo da variável.
Uma variável Animal guardando um Cachorro
Até agora, você sempre escreveu Cachorro rex = new Cachorro();, com o mesmo tipo dos dois lados. Mas, como todo cachorro é um animal, isto também vale:
public class Main {
public static void main(String[] args) {
Animal a = new Cachorro();
a.emitirSom();
a = new Gato();
a.emitirSom();
}
}
class Animal {
void emitirSom() {
System.out.println("(algum som de animal)");
}
}
class Cachorro extends Animal {
@Override
void emitirSom() {
System.out.println("Au au!");
}
void buscarBolinha() {
System.out.println("Buscando a bolinha!");
}
}
class Gato extends Animal {
@Override
void emitirSom() {
System.out.println("Miau!");
}
}
- Animal
- O tipo da variável (também chamado de tipo da referência): o que o compilador sabe sobre ela. "Aqui dentro há algum animal."
- a
- O nome da variável.
- new Cachorro()
- O objeto de verdade, criado na memória. Ele é, e sempre será, um
Cachorro. - a = new Gato();
- A mesma variável passa a apontar para um gato. Permitido, porque gato também é animal.
$ java Main.java Au au! « a variável é Animal, mas o objeto é Cachorro Miau! « a mesma linha de código, agora com um Gato
A linha a.emitirSom(); aparece duas vezes, igual, e fez coisas diferentes. Guardar uma subclasse numa variável da superclasse se chama upcasting ("converter para cima"), e é automático: não precisa escrever nada.
Qual método roda? O despacho dinâmico
Há dois tipos envolvidos em Animal a = new Cachorro();, e cada um tem um papel:
Tipo da variável (Animal) | Tipo do objeto (Cachorro) | |
|---|---|---|
| Quem olha | O compilador, antes de rodar | A JVM, enquanto roda |
| Decide | Quais métodos você pode chamar | Qual versão do método roda |
Em a.emitirSom() | "Animal tem emitirSom()? Tem. Pode chamar." | "O objeto é um Cachorro, que sobrescreveu. Roda a do Cachorro." |
Essa escolha da versão na hora de rodar, olhando o objeto, se chama despacho dinâmico (dynamic dispatch) ou ligação tardia (late binding). "Dinâmico" porque acontece durante a execução: o compilador não tem como saber, por exemplo, que animal o usuário vai escolher num menu.
O que o tipo da variável não deixa fazer
O outro lado da tabela: o compilador só deixa chamar o que o tipo da variável conhece. O objeto em a é um cachorro, mas a variável diz só "animal", e nem todo animal busca bolinha:
Animal a = new Cachorro();
a.emitirSom();
a.buscarBolinha();
$ javac Main.java
Main.java:5: error: cannot find symbol
a.buscarBolinha();
^
symbol: method buscarBolinha()
location: variable a of type Animal
1 error
"Não achei o método buscarBolinha() na variável a, do tipo Animal." O objeto tem o método, mas o compilador decide pelo tipo da variável. Mais abaixo você vai ver como recuperar o acesso, com casting e instanceof.
Onde o polimorfismo brilha: listas e parâmetros
Uma variável solta não mostra a vantagem. Ela aparece quando você tem vários animais diferentes e quer tratá-los do mesmo jeito. A fazenda da Ana:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Animal> fazenda = List.of(new Cachorro(), new Gato(), new Vaca(), new Gato());
for (Animal bicho : fazenda) {
bicho.emitirSom();
}
System.out.println("---");
acordar(new Vaca());
acordar(new Cachorro());
}
static void acordar(Animal bicho) {
System.out.print("Bom dia! ");
bicho.emitirSom();
}
}
class Animal {
void emitirSom() {
System.out.println("(algum som de animal)");
}
}
class Cachorro extends Animal {
@Override
void emitirSom() {
System.out.println("Au au!");
}
}
class Gato extends Animal {
@Override
void emitirSom() {
System.out.println("Miau!");
}
}
class Vaca extends Animal {
@Override
void emitirSom() {
System.out.println("Muu!");
}
}
- List<Animal>
- Uma lista de animais. Cabe qualquer subclasse de
Animal(listas são o assunto de ArrayList; um arrayAnimal[]funcionaria igual). - for (Animal bicho : fazenda)
- Percorre a lista (veja for-each). A cada volta,
bichoé um animal diferente. - static void acordar(Animal bicho)
- Um método que aceita qualquer animal como parâmetro.
$ java Main.java Au au! Miau! Muu! Miau! --- Bom dia! Muu! Bom dia! Au au!
Repare no que não existe nesse código: nenhum if perguntando "é cachorro? é gato?". O laço e o método acordar foram escritos uma vez, para Animal. Se amanhã a Ana criar a classe Galinha com o seu emitirSom(), o laço e o acordar funcionam com ela sem mudar uma linha. Esse é o grande ganho: o código que usa os animais não precisa conhecer todos os tipos de animal.
Missão: o programa soma as áreas de uma lista de formas, sem saber que forma é cada uma. Crie a classe Triangulo, que herda de Forma, recebe base e altura no construtor e sobrescreve area() com base * altura / 2. Acrescente new Triangulo(6, 2) na lista. O laço não precisa mudar! A saída deve ser Área total: 28.0.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Forma> formas = List.of(new Retangulo(2, 3), new Retangulo(4, 4));
double total = 0;
for (Forma f : formas) {
total = total + f.area();
}
System.out.println("Área total: " + total);
}
}
class Forma {
double area() {
return 0;
}
}
class Retangulo extends Forma {
double largura, altura;
Retangulo(double largura, double altura) {
this.largura = largura;
this.altura = altura;
}
@Override
double area() {
return largura * altura;
}
}
// crie aqui a classe Triangulo
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Forma> formas = List.of(new Retangulo(2, 3), new Retangulo(4, 4), new Triangulo(6, 2));
double total = 0;
for (Forma f : formas) {
total = total + f.area();
}
System.out.println("Área total: " + total);
}
}
class Forma {
double area() {
return 0;
}
}
class Retangulo extends Forma {
double largura, altura;
Retangulo(double largura, double altura) {
this.largura = largura;
this.altura = altura;
}
@Override
double area() {
return largura * altura;
}
}
class Triangulo extends Forma {
double base, altura;
Triangulo(double base, double altura) {
this.base = base;
this.altura = altura;
}
@Override
double area() {
return base * altura / 2;
}
}
Voltando para o tipo da subclasse: casting
Às vezes você tem um Animal nas mãos e sabe que ele é um cachorro. Para usar o que só o cachorro tem, é preciso converter a referência para Cachorro. Isso é o casting de objetos (ou downcasting, "converter para baixo"). Sem ele, o compilador recusa:
$ javac Main.java
Main.java:4: error: incompatible types: Animal cannot be converted to Cachorro
Cachorro c = a;
^
1 error
"Tipos incompatíveis: Animal não pode ser convertido em Cachorro." O compilador não sabe o que há dentro de a: poderia ser um gato. Com o cast, você assume a responsabilidade, escrevendo o tipo entre parênteses, como no casting de números:
Animal a = new Cachorro();
Cachorro c = (Cachorro) a;
c.buscarBolinha();
Animal outro = new Gato();
Cachorro falso = (Cachorro) outro;
falso.buscarBolinha();
- (Cachorro)
- O cast: "trate esta referência como
Cachorro". O objeto não muda; muda só o tipo pelo qual você o enxerga. - a
- A referência a converter.
- Cachorro c
- Agora, com uma variável do tipo
Cachorro,buscarBolinha()está liberado.
O primeiro cast é verdadeiro: o objeto é mesmo um cachorro. O segundo é uma mentira: outro guarda um gato. O compilador aceita os dois (ele não sabe o que há dentro), mas a JVM confere na hora de rodar:
$ javac Main.java && java Main Buscando a bolinha! Exception in thread "main" java.lang.ClassCastException: class Gato cannot be cast to class Cachorro (Gato and Cachorro are in unnamed module of loader 'app') at Main.main(Main.java:7)
ClassCastException = "exceção de conversão de classe": "a classe Gato não pode ser convertida para Cachorro". O resto da mensagem, entre parênteses, diz de onde as classes foram carregadas (unnamed module of loader 'app': sem módulo, carregadas pela aplicação); você pode ignorá-lo. A última linha aponta a linha 7, a do cast falso. O programa parou ali: o falso.buscarBolinha() nunca rodou.
java Main.java
No modo arquivo-fonte, as classes são carregadas por outro carregador, e o fim da mensagem fica (Gato and Cachorro are in unnamed module of loader com.sun.tools.javac.launcher.MemoryClassLoader @6166e06f). O começo, que é o que importa, é igual.
E se as classes nem são parentes, o compilador já recusa o cast, porque ele nunca poderia dar certo:
$ javac Main.java
Main.java:4: error: incompatible types: Animal cannot be converted to String
String texto = (String) a;
^
1 error
instanceof: pergunte antes de converter
Para não arriscar uma ClassCastException, pergunte primeiro. O operador instanceof ("é instância de") responde true ou false: "este objeto é um X (ou uma subclasse de X)?".
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Animal> fazenda = List.of(new Cachorro(), new Gato(), new Cachorro());
for (Animal bicho : fazenda) {
bicho.emitirSom();
if (bicho instanceof Cachorro) {
Cachorro c = (Cachorro) bicho;
c.buscarBolinha();
}
}
System.out.println("---");
Animal rex = new Cachorro();
System.out.println(rex instanceof Cachorro);
System.out.println(rex instanceof Animal);
System.out.println(rex instanceof Object);
System.out.println(rex instanceof Gato);
Animal ninguem = null;
System.out.println(ninguem instanceof Animal);
}
}
(As classes são as de antes; o buscarBolinha() agora mostra (buscando a bolinha).)
$ java Main.java Au au! (buscando a bolinha) « só os cachorros passaram no if Miau! Au au! (buscando a bolinha) --- true « rex é um Cachorro true « …e também um Animal true « …e também um Object false « mas não é um Gato false « null nunca é instância de nada
Repare nos três true: o instanceof segue a escada da herança. Um cachorro é um animal e é um Object. E o último: com null (variável que não aponta para objeto nenhum), o resultado é false, sem erro; por isso o instanceof também protege contra null.
O jeito moderno: instanceof com padrão
Java 16+ Perguntar e depois converter (if (bicho instanceof Cachorro) + (Cachorro) bicho) repete o nome da classe. Desde o Java 16, dá para fazer as duas coisas de uma vez, dando um nome à variável já convertida:
for (Animal bicho : fazenda) {
bicho.emitirSom();
if (bicho instanceof Cachorro c) {
c.buscarBolinha();
}
}
- bicho instanceof Cachorro
- A pergunta de sempre.
- c
- Se a resposta for
true, cria a variávelc, do tipoCachorro, já convertida. Ela só existe dentro doif.
A saída é a mesma da versão anterior (as três primeiras partes). Essa forma se chama pattern matching ("casamento de padrões") e ganhou uma aula própria: Pattern Matching, que mostra também o switch com tipos.
instanceof é sinal de alerta
Se o seu código tem uma escada de if (x instanceof Cachorro) … else if (x instanceof Gato) … else if … para decidir o que cada animal faz, provavelmente aquilo deveria ser um método sobrescrito em cada classe. Deixe o despacho dinâmico escolher. O instanceof é para os casos em que você precisa de algo que só um tipo tem.
Quando dá errado
ClassCastException
$ javac Main.java && java Main Buscando a bolinha! Exception in thread "main" java.lang.ClassCastException: class Gato cannot be cast to class Cachorro (Gato and Cachorro are in unnamed module of loader 'app') at Main.main(Main.java:7)
O que aconteceu: um cast afirmou que o objeto era de um tipo que ele não é. O compilador aceitou, a JVM recusou ao rodar.
Como resolver: confira com instanceof antes de converter (de preferência a forma if (x instanceof Cachorro c)). Ou repense: talvez o método devesse estar na superclasse e ser sobrescrito.
O que aconteceu: o método existe no objeto, mas não no tipo da variável. O compilador só olha o tipo da variável.
Como resolver: se todo animal deveria ter o método, declare-o em Animal. Se só o cachorro tem, use instanceof com padrão para obter uma variável Cachorro.
O que aconteceu: uma atribuição de "para baixo" (Cachorro c = a;) sem cast. Para cima é automático; para baixo, não.
Como resolver: if (a instanceof Cachorro c) { … }, ou o cast explícito (Cachorro) a se você tiver certeza.
O que aconteceu: quase sempre a subclasse não sobrescreveu de verdade (nome ou parâmetros diferentes), e roda a versão herdada.
Como resolver: coloque @Override no método da subclasse (veja Sobrescrita).
Referência
| Escrita | O que faz |
|---|---|
Animal a = new Cachorro(); | Upcasting: automático, sempre seguro |
a.emitirSom() | Roda a versão do objeto (despacho dinâmico) |
List<Animal>, void f(Animal a) | Aceitam qualquer subclasse de Animal |
(Cachorro) a | Downcasting: o compilador confia; a JVM confere (ClassCastException) |
a instanceof Cachorro | true se o objeto é Cachorro ou subclasse; false para null |
if (a instanceof Cachorro c) Java 16+ | Pergunta e já cria a variável convertida c |
Exercícios
1. Lista de qualquer animal
Complete o tipo da variável para ela aceitar um Gato e, depois, um Cachorro:
2. Perguntar antes
Complete a condição que confere se bicho é um Gato:
3. O cast
Complete para converter bicho (do tipo Animal) para Gato:
4. Animal a = new Gato(); e Gato sobrescreve emitirSom(). O que a.emitirSom() mostra?
5. Animal a = new Gato(); Cachorro c = (Cachorro) a; O que acontece?
6. Cachorro rex = new Cachorro(); Qual destas expressões vale false?
Missão final: a Ana quer que todos os cachorros da lista busquem a bolinha. O programa converte todo animal para Cachorro e quebra com ClassCastException ao chegar na Mimi. Rode e veja. Conserte com instanceof, para só os cachorros buscarem. A saída deve ter Rex buscou a bolinha e Thor buscou a bolinha, sem exceção.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Animal> animais = List.of(new Cachorro("Rex"), new Gato("Mimi"), new Cachorro("Thor"));
for (Animal bicho : animais) {
Cachorro c = (Cachorro) bicho;
c.buscarBolinha();
}
}
}
class Animal {
String nome;
Animal(String nome) {
this.nome = nome;
}
}
class Cachorro extends Animal {
Cachorro(String nome) {
super(nome);
}
void buscarBolinha() {
System.out.println(nome + " buscou a bolinha");
}
}
class Gato extends Animal {
Gato(String nome) {
super(nome);
}
}
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Animal> animais = List.of(new Cachorro("Rex"), new Gato("Mimi"), new Cachorro("Thor"));
for (Animal bicho : animais) {
if (bicho instanceof Cachorro c) {
c.buscarBolinha();
}
}
}
}
class Animal {
String nome;
Animal(String nome) {
this.nome = nome;
}
}
class Cachorro extends Animal {
Cachorro(String nome) {
super(nome);
}
void buscarBolinha() {
System.out.println(nome + " buscou a bolinha");
}
}
class Gato extends Animal {
Gato(String nome) {
super(nome);
}
}
Resumo
- Uma variável, lista ou parâmetro do tipo da superclasse aceita qualquer subclasse:
Animal a = new Cachorro();(upcasting, automático). - O tipo da variável decide o que pode ser chamado (compilador); o tipo do objeto decide qual versão roda (JVM). Isso é o despacho dinâmico.
- Polimorfismo permite escrever código para
Animalque funciona com animais que ainda nem existem. - Para usar o que só a subclasse tem, converta:
(Cachorro) a. Se o objeto não for daquele tipo:ClassCastExceptionao rodar. - Pergunte antes com
instanceof; no Java 16+,if (a instanceof Cachorro c)pergunta e converte de uma vez.