Uma referência, muitos tipos
O polimorfismo é a recompensa da herança e das interfaces. Significa que uma única variável tipada como um pai (ou uma interface) pode conter um objeto de qualquer subtipo, e quando você chama um método nela, Java executa a versão que pertence à classe real do objeto - não a versão sugerida pelo tipo declarado da variável.
Você já viu a base na página de herança: uma subclasse sobrescreve um método do seu pai. O polimorfismo é o que torna essa sobrescrita valiosa - ele permite que você escreva código contra o tipo geral e deixe cada objeto concreto se comportar à sua maneira.
Tanto a quanto b são declarados como Animal, mas cada um imprime seu próprio som. Essa escolha acontece em tempo de execução com base no objeto real.
Despacho dinâmico de métodos
O mecanismo por trás disso é o despacho dinâmico de métodos: para um método de instância sobrescrito, a JVM olha para a classe em tempo de execução do objeto para decidir qual implementação chamar. O compilador apenas verifica se o método existe no tipo declarado; a seleção real é adiada até o programa rodar.
É isso que permite que um único laço lide com toda uma mistura de tipos sem nunca perguntar o que cada um é:
O laço conhece apenas Shape. Adicione um Triangle extends Shape mais tarde e este código continua funcionando sem alterações - esse é todo o objetivo. O código depende da abstração, não da lista concreta de tipos.
Upcasting e downcasting
Armazenar um Dog em uma variável Animal é upcasting - subir na hierarquia para um tipo mais geral. É sempre seguro e Java faz isso implicitamente, porque todo Dog é um Animal.
Ir na direção oposta é downcasting - pegar uma referência pai e tratá-la como um subtipo específico. Isso só é válido se o objeto realmente for aquele subtipo, então você precisa escrever o cast explicitamente, e corre o risco de um ClassCastException se estiver errado:
O último cast compila sem problemas - o compilador não consegue provar que está errado - mas estoura ao rodar porque um Cat não é um Dog. Nunca faça downcast por fé.
Proteja downcasts com instanceof
Antes de fazer downcasting, verifique o tipo real com instanceof. O Java moderno permite vincular o resultado na mesma expressão (pattern matching para instanceof), então você dispensa o cast separado:
instanceof retorna false para null, então a verificação também protege você de um NullPointerException. Dito isso, se você se pega escrevendo longas cadeias de instanceof, isso costuma ser sinal de que o comportamento pertence dentro das classes como um método sobrescrito - deixe o polimorfismo fazer a ramificação por você.
Overriding versus overloading
Esses dois parecem semelhantes, mas não têm relação, e confundi-los é uma fonte clássica de confusão.
Overriding (sobrescrita) é uma subclasse substituindo um método do pai com a assinatura idêntica. É resolvido em tempo de execução pelo tipo do objeto - este é o polimorfismo que viemos usando.
Overloading (sobrecarga) é uma classe que tem vários métodos com o mesmo nome, mas listas de parâmetros diferentes. É resolvido em tempo de compilação pelos tipos dos argumentos, sem nenhum despacho em tempo de execução envolvido:
O compilador escolhe o describe correspondente puramente a partir do tipo estático do argumento. Não há nenhum objeto pai/filho envolvido, então isso não é polimorfismo em tempo de execução - apenas reutiliza um nome de método.
Uma armadilha comum: campos não são polimórficos
Apenas métodos de instância são despachados em tempo de execução. Campos e métodos estáticos são resolvidos pelo tipo declarado, o que pega muita gente de surpresa:
p.name() executa a versão de Child (polimorfismo), mas p.label lê o campo de Parent, porque campos são ocultados, não sobrescritos. A correção é simples: mantenha os campos private e acesse-os apenas por meio de métodos, para que a chamada polimórfica sempre vença.
A seguir: modificadores de acesso
O polimorfismo só funciona de forma limpa quando as subclasses conseguem ver e sobrescrever os membros certos enquanto o resto do seu código não consegue se intrometer e quebrar invariantes. Esse equilíbrio é controlado pelo acesso public, protected, private e de pacote - os modificadores de acesso, a seguir.
Perguntas frequentes
O que é polimorfismo em Java?
Polimorfismo significa que um tipo de referência pode apontar para objetos de muitas classes diferentes, e o método que realmente executa é escolhido pelo tipo real do objeto em tempo de execução, não pelo tipo declarado da variável. Assim, uma variável Shape shape pode conter um Circle ou um Square, e chamar shape.area() executa a versão certa automaticamente.
Qual é a diferença entre overriding e overloading em Java?
Overriding (sobrescrita) é quando uma subclasse substitui um método da superclasse com a mesma assinatura - é isso que impulsiona o polimorfismo em tempo de execução. Overloading (sobrecarga) é quando uma classe tem vários métodos com o mesmo nome, mas listas de parâmetros diferentes; o compilador escolhe um em tempo de compilação com base nos argumentos. A sobrescrita é resolvida em tempo de execução pelo tipo do objeto; a sobrecarga é resolvida em tempo de compilação pelos tipos dos argumentos.
Qual é a diferença entre upcasting e downcasting em Java?
O upcasting trata um objeto filho como seu tipo pai (Animal a = new Dog();) - sempre seguro e geralmente implícito. O downcasting vai no sentido contrário (Dog d = (Dog) a;) e só é seguro se o objeto realmente for aquele subtipo; caso contrário, lança ClassCastException. Proteja todo downcast com instanceof antes.