Una referencia, muchos tipos
El polimorfismo es la recompensa de la herencia y las interfaces. Significa que una sola variable tipada como un padre (o una interfaz) puede contener un objeto de cualquier subtipo, y cuando llamas a un método sobre ella, Java ejecuta la versión que pertenece a la clase real del objeto, no la versión que sugiere el tipo declarado de la variable.
Ya viste el planteamiento en la página de herencia: una subclase sobrescribe un método de su padre. El polimorfismo es lo que hace que valga la pena esa sobrescritura: te permite escribir código contra el tipo general y dejar que cada objeto concreto se comporte a su manera.
Tanto a como b están declarados como Animal, pero cada uno imprime su propio sonido. Esa elección ocurre en tiempo de ejecución según el objeto real.
Despacho dinámico de métodos
El mecanismo detrás de esto es el despacho dinámico de métodos: para un método de instancia sobrescrito, la JVM mira la clase en tiempo de ejecución del objeto para decidir qué implementación llamar. El compilador solo comprueba que el método exista en el tipo declarado; la selección real se aplaza hasta que el programa se ejecuta.
Esto es lo que permite que un solo bucle maneje una mezcla completa de tipos sin preguntar nunca qué es cada uno:
El bucle solo conoce Shape. Si más adelante añades un Triangle extends Shape, este código sigue funcionando sin cambios: ese es el objetivo. El código depende de la abstracción, no de la lista concreta de tipos.
Upcasting y downcasting
Guardar un Dog en una variable Animal es upcasting: subir por la jerarquía hacia un tipo más general. Siempre es seguro y Java lo hace de forma implícita, porque todo Dog es un Animal.
Ir en la dirección contraria es downcasting: tomar una referencia padre y tratarla como un subtipo específico. Eso solo es válido si el objeto realmente es ese subtipo, así que debes escribir el cast de tipos explícitamente y corres el riesgo de un ClassCastException si te equivocas:
El último cast compila sin problemas (el compilador no puede demostrar que está mal), pero estalla al ejecutarse porque un Cat no es un Dog. Nunca hagas downcast a ciegas.
Protege los downcasts con instanceof
Antes de hacer downcasting, comprueba el tipo real con instanceof. El Java moderno te permite vincular el resultado en la misma expresión (pattern matching para instanceof), así te ahorras el cast por separado:
instanceof devuelve false para null, así que la comprobación también te protege de un NullPointerException. Dicho esto, si te encuentras escribiendo largas cadenas de instanceof, suele ser señal de que el comportamiento debería estar dentro de las clases como un método sobrescrito: deja que el polimorfismo haga las ramificaciones por ti.
Overriding frente a overloading
Estos dos suenan parecidos pero no tienen relación, y confundirlos es una fuente clásica de errores.
Overriding (sobrescritura) es una subclase que reemplaza un método del padre con la firma idéntica. Se resuelve en tiempo de ejecución por el tipo del objeto: este es el polimorfismo que hemos estado usando.
Overloading (sobrecarga) es una clase que tiene varios métodos con el mismo nombre pero distintas listas de parámetros. Se resuelve en tiempo de compilación por los tipos de los argumentos, sin que intervenga ningún despacho en tiempo de ejecución:
El compilador elige el describe que coincide puramente a partir del tipo estático del argumento. No hay ningún objeto padre/hijo involucrado, así que esto no es polimorfismo en tiempo de ejecución: solo reutiliza un nombre de método.
Un error común: los campos no son polimórficos
Solo los métodos de instancia se despachan en tiempo de ejecución. Los campos y los métodos estáticos se resuelven por el tipo declarado, lo que confunde a mucha gente:
p.name() ejecuta la versión de Child (polimorfismo), pero p.label lee el campo de Parent, porque los campos están ocultos, no sobrescritos. La solución es sencilla: mantén los campos private y accede a ellos solo a través de métodos, para que la llamada polimórfica siempre gane.
Siguiente: modificadores de acceso
El polimorfismo solo funciona limpiamente cuando las subclases pueden ver y sobrescribir los miembros correctos mientras el resto de tu código no puede entrometerse y romper invariantes. Ese equilibrio se controla con el acceso public, protected, private y de paquete: los modificadores de acceso, a continuación.
Preguntas frecuentes
¿Qué es el polimorfismo en Java?
El polimorfismo significa que un tipo de referencia puede apuntar a objetos de muchas clases distintas, y el método que realmente se ejecuta lo elige el tipo real del objeto en tiempo de ejecución, no el tipo declarado de la variable. Así, una variable Shape shape puede contener un Circle o un Square, y llamar a shape.area() ejecuta la versión correcta automáticamente.
¿Cuál es la diferencia entre overriding y overloading en Java?
Overriding (sobrescritura) ocurre cuando una subclase reemplaza un método de la superclase con la misma firma; esto es lo que impulsa el polimorfismo en tiempo de ejecución. Overloading (sobrecarga) ocurre cuando una clase tiene varios métodos con el mismo nombre pero distintas listas de parámetros; el compilador elige uno en tiempo de compilación según los argumentos. La sobrescritura se resuelve en tiempo de ejecución por el tipo del objeto; la sobrecarga se resuelve en tiempo de compilación por los tipos de los argumentos.
¿Cuál es la diferencia entre upcasting y downcasting en Java?
El upcasting trata a un objeto hijo como su tipo padre (Animal a = new Dog();): siempre es seguro y suele ser implícito. El downcasting va en sentido contrario (Dog d = (Dog) a;) y solo es seguro si el objeto realmente es ese subtipo; de lo contrario lanza ClassCastException. Protege todo downcast con instanceof primero.