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.

Você vai aprender
  • 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 Animal com Cachorro, Gato e Vaca, 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 ClassCastException e o instanceof.

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:

  1. Uma variável (ou lista, ou parâmetro) do tipo Animal pode guardar qualquer subclasse de Animal.
  2. 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 olhaO compilador, antes de rodarA JVM, enquanto roda
DecideQuais métodos você pode chamarQual 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:

Este código tem um erro de propósito
        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 array Animal[] 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.

A mensagem muda um pouco com 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ável c, do tipo Cachorro, já convertida. Ela só existe dentro do if.

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.

Muito 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.

"cannot find symbol … location: variable a of type Animal"

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.

"incompatible types: Animal cannot be converted to 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.

Roda a versão "errada" do método

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

EscritaO 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) aDowncasting: o compilador confia; a JVM confere (ClassCastException)
a instanceof Cachorrotrue 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:

bicho = new Gato();

2. Perguntar antes

Complete a condição que confere se bicho é um Gato:

if () { … }

3. O cast

Complete para converter bicho (do tipo Animal) para Gato:

Gato g = ;

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 Animal que 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: ClassCastException ao rodar.
  • Pergunte antes com instanceof; no Java 16+, if (a instanceof Cachorro c) pergunta e converte de uma vez.