Saltar al contenido principal

Polimorfismo: selección dinámica de métodos

La selección dinámica de métodos (dynamic method dispatch), también conocida como vinculación dinámica (dynamic binding), y es una de las bases del polimorfismo en tiempo de ejecución.

Cuando tienes una referencia de una clase padre que apunta a un objeto de una clase hija, el método que se ejecuta depende del tipo real del objeto, no del tipo de la referencia.

Esto se decide en tiempo de ejecución, no en compilación. Por eso se llama vinculación dinámica.

Cuando definimos una clase como subclase de otra, los objetos de la subclase son también objetos de la superclase.

Por ejemplo:

class Animal {
void hacerSonido() {
System.out.println("El animal hace un sonido");
}
}

class Perro extends Animal {
@Override
void hacerSonido() {
System.out.println("El perro ladra");
}
}

public class Main {
public static void main(String[] args) {
Animal a = new Perro(); // Referencia de tipo Animal, objeto de tipo Perro
a.hacerSonido(); // ¿Qué se ejecuta?
}
}

Aunque a es una referencia de tipo Animal, el método que se ejecuta es el de Perro, porque la selección del método se hace dinámicamente según el tipo real del objeto. Se mostraría El perro ladra.

Sin embargo, si Perro no tuviese el método hacerSonido(), se ejecutaría el de Animal:

class Animal {
void hacerSonido() {
System.out.println("El animal hace un sonido");
}
}

class Perro extends Animal {}

public class Main {
public static void main(String[] args) {
Animal a = new Perro(); // Referencia de tipo Animal, objeto de tipo Perro
a.hacerSonido(); // Salida: El animal hace un sonido
}
}

Los atributos no tienen selección dinámica

Cuando un método se sobrescribe (override) en una subclase, Java elige en tiempo de ejecución cuál método ejecutar según el tipo real del objeto. Eso es selección dinámica de métodos.

Los atributos (campos) en Java no se resuelven dinámicamente. Su acceso se decide en tiempo de compilación, según el tipo de la referencia, no del objeto real.

class Animal {
String tipo = "Animal";
}

class Perro extends Animal {
String tipo = "Perro";
}

public class Main {
public static void main(String[] args) {
Animal a = new Perro();
Perro b = new Perro();
System.out.println(a.tipo); // Salida: Animal
System.out.println(b.tipo); // Salida: Perro
}
}
class Animal {
String tipo = "Animal";
}

class Perro extends Animal {}

public class Main {
public static void main(String[] args) {
Animal a = new Perro();
Perro b = new Perro();
System.out.println(a.tipo); // Salida: Animal
System.out.println(b.tipo); // Salida: Animal
}
}

Los atributos accesibles dependen de la clase. Por lo tanto, no se produce ocultación.

ElementoMomento de decisiónDepende del tipo...Ejemplo
MétodosEjecuciónReal del objetoPolimorfismo dinámico
AtributosCompilaciónDe la referenciaNo hay polimorfismo

Ejemplo de polimorfismo en colecciones

En Java, el polimorfismo permite tratar objetos de diferentes clases derivadas (subclases) como si fueran objetos de una clase base (superclase). Gracias al polimorfismo, podemos llamar a métodos de una superclase, sabiendo que cada subclase puede proporcionar su propia implementación específica de esos métodos.

Vamos a crear un ejemplo de polimorfismo usando una clase abstracta llamada Animal con un método abstracto hacerSonido(). Luego, crearemos dos subclases (Perro y Gato) que heredan de Animal e implementan hacerSonido() de manera diferente. Finalmente, mostraremos cómo se puede usar el polimorfismo para tratar cada subclase como una instancia de la superclase Animal.

public abstract class Animal {
// Método abstracto que cada subclase implementará de manera diferente
abstract void hacerSonido();
}
public class Perro extends Animal {
@Override
public void hacerSonido() {
System.out.println("El perro ladra.");
}
}
public class Gato extends Animal {
@Override
public void hacerSonido() {
System.out.println("El gato maúlla.");
}
}
public class Main {
public static void main(String[] args) {
// Creamos un ArrayList de tipo Animal para almacenar diferentes tipos de animales
ArrayList<Animal> animales = new ArrayList<Animal>();

// Asignamos un Perro y un Gato al array
animales.add(new Perro());
animales.add(new Gato());

// Usamos polimorfismo para llamar a hacerSonido() en cada objeto
for (Animal animal : animales) {
animal.hacerSonido();
}
}
}

Explicación:

  • Clase Animal: Es una clase abstracta que tiene un método abstracto hacerSonido(). Este método no tiene implementación aquí; es solo una "promesa" de que todas las subclases de Animal deberán implementarlo.
  • Subclase Perro: Esta clase hereda de Animal y proporciona su propia implementación del método hacerSonido(), haciendo que el perro ladre.
  • Subclase Gato: También hereda de Animal y proporciona su propia implementación de hacerSonido(), haciendo que el gato maúlle.
  • Clase Main: Creamos un ArrayList de Animal que almacena tanto objetos de tipo Perro como Gato. Al recorrer la lista, llamamos a hacerSonido() en cada elemento. Gracias al polimorfismo, Java llama automáticamente a la versión correcta de hacerSonido() según el tipo real del objeto (ya sea un Perro o un Gato), aunque lo tratemos como un Animal.

De hecho, la salida del programa sería:

El perro ladra.
El gato maúlla.

Aunque la clase Animal no fuera abstracta, funcionaría el polimorfismo de la misma manera.

Ejemplo de polimorfismo en constructores y métodos

Supongamos que tenemos la siguiente jerarquía de clases:

public class Coche {}
public class CocheElectrico extends Coche {}
public class CocheHibrido extends Coche {}

Podemos crear una clase Garaje para almacenar un coche, tanto si es eléctrico como híbrido utilizando la superclase Coche.

public class Garaje {
private Coche coche;

public Garaje(Coche coche) {
this.coche = coche;
}

public Coche getCoche() {
return this.coche;
}

public void setCoche(Coche coche) {
this.coche = coche;
}
}

Así, por ejemplo, podemos crear un coche híbrido y guardarlo en el garaje:

CocheHibrido coche = new CocheHibrido();
// En el constructor podemos pasar como parámetro una subclase de Coche
Garaje garaje = new Garaje(coche);

El problema es que si queremos obtener el coche del garaje mediante el método getCoche() obtendremos una instancia de la superclase, no la de la propia clase del objeto:

// Funciona
Coche cocheDelGaraje = garaje.getCoche();

// No funciona
CocheElectrico cocheElectrico = garaje.getCoche();

Una posible solución es trabajar siempre con la superclase. Para eso podemos definir métodos abstractos en la superclase y utilizarlos siendo cualquiera de las clases hijas. Si no, también se puede utilizar instanceof como veremos a continuación.

Obtener la clase de un objeto con instanceof

El operador instanceof en Java se usa para comprobar si un objeto es de una clase específica o de una subclase de esa clase.

Este operador es muy útil para validar tipos en tiempo de ejecución, especialmente cuando se trabaja con clases jerárquicas o con objetos de tipo Object.

objeto instanceof Clase
  • objeto: El objeto que quieres comprobar.
  • Clase: La clase (o interfaz) contra la que estás comprobando.

El operador devuelve:

  • true: Si el objeto es una instancia de la clase especificada o de una subclase.
  • false: En caso contrario.

Veamos un ejemplo:

public class EjemploInstanceOf {
public static void main(String[] args) {
String texto = "Hola mundo";

// Comprobar si el objeto es una instancia de String
if (texto instanceof String) {
System.out.println("El objeto es una cadena de texto."); // Imprime esto
} else {
System.out.println("El objeto no es una cadena de texto.");
}
}
}

Si tienes una jerarquía de clases, instanceof también verifica si el objeto pertenece a una subclase.

public class Animal {}
public class Perro extends Animal {}

public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Perro Perro = new Perro();

System.out.println(animal instanceof Animal); // true
System.out.println(Perro instanceof Animal); // true
System.out.println(animal instanceof Perro); // false
}
}

También puedes usar instanceof para evitar errores cuando quieres hacer un cast entre clases.

Animal animal = new Gato();

if (animal instanceof Gato) {
Gato gato = (Gato) animal; // Conversión segura
System.out.println("El objeto ha sido convertido a Gato.");
}

Pattern matching para instanceof

En Java 16 se mejoró el uso de instanceof con Pattern Matching, permitiendo eliminar el cast explícito:

Object obj = "Una cadena de texto";

// Se comprueba y se hace el cast al mismo tiempo
if (obj instanceof String str) {
System.out.println("La cadena tiene " + str.length() + " caracteres.");
}

En el código anterior se convierte obj (de tipo Object) a str (de tipo String).

Si no se puede realizar el cast, simplemente no se entra dentro del if.

Object obj = 2;

// No se puede realizar el cast de Integer a String, por lo tanto, no se muestra nada
if (obj instanceof String str) {
System.out.println("La cadena tiene " + str.length() + " caracteres.");
}