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.
| Elemento | Momento de decisión | Depende del tipo... | Ejemplo |
|---|---|---|---|
| Métodos | Ejecución | Real del objeto | Polimorfismo dinámico |
| Atributos | Compilación | De la referencia | No 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 abstractohacerSonido(). Este método no tiene implementación aquí; es solo una "promesa" de que todas las subclases deAnimaldeberán implementarlo. - Subclase
Perro: Esta clase hereda deAnimaly proporciona su propia implementación del métodohacerSonido(), haciendo que el perro ladre. - Subclase
Gato: También hereda deAnimaly proporciona su propia implementación dehacerSonido(), haciendo que el gato maúlle. - Clase
Main: Creamos unArrayListdeAnimalque almacena tanto objetos de tipoPerrocomoGato. Al recorrer la lista, llamamos ahacerSonido()en cada elemento. Gracias al polimorfismo, Java llama automáticamente a la versión correcta dehacerSonido()según el tipo real del objeto (ya sea unPerroo unGato), aunque lo tratemos como unAnimal.
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.");
}