W3docs

Polimorfismo en Java

Escribe código Java flexible con polimorfismo en tiempo de compilación (sobrecarga) y en tiempo de ejecución (sobreescritura).

El polimorfismo es "una interfaz, muchas implementaciones". En Java tiene dos variantes:

  • Polimorfismo en tiempo de compilación (sobrecarga): el compilador elige entre métodos que comparten un nombre según los tipos de argumento que se pasan.
  • Polimorfismo en tiempo de ejecución (sobreescritura): la JVM elige entre implementaciones de métodos según el objeto real sobre el que se realiza la llamada.

La variante en tiempo de ejecución es lo que la gente suele entender por "polimorfismo" en un contexto de POO, y es la que hace que la herencia valga la pena. Sin ella, una referencia Cat sería la única forma de llamar a Cat.speak() — no podrías escribir código que recorra una lista mixta de animales y le pida a cada uno que hable.

Polimorfismo en tiempo de compilación — sobrecarga

Dos métodos en la misma clase pueden compartir un nombre siempre que sus listas de parámetros difieran. El compilador elige cuál llamar según los tipos de argumento en el sitio de la llamada:

public class Printer {
  void print(int n)       { System.out.println("int: " + n); }
  void print(double d)    { System.out.println("double: " + d); }
  void print(String s)    { System.out.println("string: " + s); }
}

Printer p = new Printer();
p.print(5);          // int
p.print(5.0);        // double
p.print("hi");       // string

Esto se decide completamente en tiempo de compilación. El método elegido queda grabado en el bytecode; nada cambia en tiempo de ejecución. La sobrecarga de métodos se cubrió en detalle en sobrecarga de métodos en la Parte 5.

Polimorfismo en tiempo de ejecución — sobreescritura y despacho dinámico

La variante interesante. Cuando una subclase sobreescribe un método, las llamadas a través de una referencia de tipo padre igualmente se despachan a la versión de la subclase:

class Animal {
  String speak() { return "(noise)"; }
}
class Cat extends Animal {
  @Override String speak() { return "meow"; }
}
class Dog extends Animal {
  @Override String speak() { return "woof"; }
}

Animal[] zoo = { new Cat(), new Dog(), new Animal() };
for (Animal a : zoo) {
  System.out.println(a.speak());
}
// meow
// woof
// (noise)

En cada iteración a tiene el tipo Animal, pero el objeto real es un Cat, un Dog o un Animal. La llamada a.speak() no elige el método en tiempo de compilación — en tiempo de compilación, el compilador solo sabe que a es algún Animal. En tiempo de ejecución, la JVM mira el objeto real y despacha al speak de la clase de ese objeto.

Esto es el despacho dinámico (a veces llamado despacho virtual). Es lo que hace interesante el bucle anterior: está escrito de forma genérica contra Animal, y funciona para cualquier subclase — incluidas las que no existían cuando se escribió el bucle.

Por qué importa

El polimorfismo es la característica de la POO que hace que el código sea abierto a la extensión sin modificación. Una función que recibe un Shape y llama a area() sobre él funciona para cada forma que existe hoy y para cada forma que alguien añada mañana. La función no necesita una cadena de if (shape instanceof Circle).

double totalArea(List<Shape> shapes) {
  double sum = 0;
  for (Shape s : shapes) sum += s.area();    // dispatches to each subclass
  return sum;
}

Añade Triangle extends Shape, y totalArea funciona con listas de triángulos de forma gratuita. Esta es la esencia del Principio Abierto/Cerrado — abierto a la extensión, cerrado a la modificación.

Upcasting y downcasting

Pasar de un tipo subclase a un tipo padre es un upcast. Es implícito y siempre seguro:

Cat c    = new Cat();
Animal a = c;          // upcast — implicit

Ir en la dirección contraria — asignar una referencia de tipo padre de vuelta a un tipo subclase — es un downcast. Necesita una expresión de cast, y la JVM comprueba en tiempo de ejecución que el objeto es realmente de ese subtipo:

Animal a = new Cat();
Cat    c = (Cat) a;    // downcast — runtime check

Animal a2 = new Dog();
Cat    c2 = (Cat) a2;  // ClassCastException at runtime

La alternativa compatible con el tiempo de compilación es la comprobación instanceof, a menudo combinada con el reconocimiento de patrones en Java moderno:

if (a instanceof Cat c) {
  c.purr();
}

Los campos no son polimórficos

El despacho dinámico se aplica solo a los métodos de instancia. Los campos, los métodos static y los métodos private se vinculan en tiempo de compilación según el tipo declarado de la referencia:

class A {
  String label = "A";
  static String klass() { return "A"; }
}
class B extends A {
  String label = "B";
  static String klass() { return "B"; }
}

A a = new B();
System.out.println(a.label);     // "A"  — field, not polymorphic
System.out.println(a.klass());   // "A"  — static, not polymorphic

Esta es una de las razones por las que se mantienen los campos privados y se accede a ellos a través de métodos — los métodos participan en el polimorfismo; los campos no.

@Override y errores silenciosos

Siempre anota las sobreescrituras con @Override. La anotación le dice al compilador "esto pretende sobreescribir un método padre — falla si no lo hace". Sin ella, un pequeño error tipográfico crea un nuevo método que parece una sobreescritura pero no lo es:

class Animal {
  String speak() { return "(noise)"; }
}
class Cat extends Animal {
  String Speak() { return "meow"; }     // capital S — typo, new method
}

Animal a = new Cat();
System.out.println(a.speak());          // "(noise)" — Cat.Speak was never called

Añadir @Override hace que el compilador detecte esto de inmediato.

Polimorfismo con interfaces

La herencia no es la única forma. Una interfaz también es un tipo padre — diferentes clases concretas la implementan, y el código que toma el tipo de interfaz funciona con todas ellas:

interface Greeter {
  String greet();
}
class English implements Greeter {
  public String greet() { return "Hello"; }
}
class French implements Greeter {
  public String greet() { return "Bonjour"; }
}

Greeter g = new French();
System.out.println(g.greet());   // "Bonjour" — dispatched to French.greet

Misma idea — escribe código contra la abstracción, deja que el tiempo de ejecución elija la implementación. El capítulo de interfaces profundiza en los mecanismos.

Un ejemplo práctico

java— editable, runs on the server

Qué viene después

El polimorfismo se sustenta en un mecanismo: una subclase reemplazando un método que heredó. Ese mecanismo — qué está permitido, qué no, y la anotación @Override que te mantiene honesto — es el tema del siguiente capítulo. Continúa con sobreescritura de métodos.

Práctica

Práctica
Animal a = new Cat(); a.speak(); — ¿cuál speak() se ejecuta realmente?
Animal a = new Cat(); a.speak(); — ¿cuál speak() se ejecuta realmente?
Was this page helpful?