1. Introducción
El polimorfismo es uno de los tres conceptos fundamentales de la programación orientada a objetos (junto con la herencia y la encapsulación). Literalmente, la palabra proviene del griego «poly» (mucho) y «morph» (forma). En programación significa: una interfaz — muchas implementaciones.
Definición
Polimorfismo es la capacidad de objetos de distintas clases de responder de manera diferente a los mismos mensajes (llamadas a métodos).
Es decir, si tienes un método makeSound(), puedes llamarlo sobre cualquier animal; pero el gato maullará, el perro ladrará y la vaca mugirá. Para el programador es simplemente la llamada animal.makeSound(), y lo que suceda realmente depende de qué objeto exacto está detrás de esa variable.
Analogía de la vida cotidiana
Imagina que en casa tienes un mando a distancia del televisor, y con ese mismo mando puedes controlar los altavoces, el proyector e incluso la cafetera. Pulsas el botón «Encender» — turnOn(), y cada aparato reacciona a su manera. Lo importante es que todos tienen un «botón» de encendido, pero la implementación es distinta.
Ejemplo en Java
class Animal {
void makeSound() {
System.out.println("Algún sonido...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("¡Guau!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("¡Miau!");
}
}
class Cow extends Animal {
@Override
void makeSound() {
System.out.println("¡Muuu!");
}
}
Ahora podemos hacer lo siguiente:
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();
animal1.makeSound(); // ¡Guau!
animal2.makeSound(); // ¡Miau!
animal3.makeSound(); // ¡Muuu!
Observa que todas las variables son de tipo Animal, pero el resultado de la llamada depende del tipo real del objeto.
2. Tipos de polimorfismo
En Java (y en la mayoría de lenguajes POO) se distinguen dos tipos principales de polimorfismo:
Polimorfismo de compilación (estático) — sobrecarga de métodos (overloading)
Es cuando en una misma clase hay varios métodos con el mismo nombre pero con parámetros diferentes. El compilador decide qué método invocar en función de los argumentos proporcionados.
Ejemplo (nos adelantamos — más detalles en la siguiente lección):
class Printer {
void print(int x) {
System.out.println("Número: " + x);
}
void print(String s) {
System.out.println("Cadena: " + s);
}
}
Polimorfismo en ejecución (dinámico) — sobrescritura de métodos (overriding)
Es cuando un método se define en la clase base y luego se sobrescribe en las subclases. Qué método se invoca se decide en tiempo de ejecución (runtime), según el tipo real del objeto.
Ejemplo: véase arriba con los animales.
3. ¿Para qué sirve el polimorfismo?
El polimorfismo no es solo una palabra bonita para entrevistas. Es una herramienta que hace tu código flexible, extensible y fácil de mantener.
Universalidad del código
Puedes escribir código que trabaje con objetos del tipo base sin preocuparte por los detalles de su implementación. Por ejemplo, si tienes una lista de animales, puedes recorrerla e invocar en cada uno makeSound() sin pensar si es un gato o un perro.
Animal[] animals = { new Dog(), new Cat(), new Cow() };
for (Animal animal : animals) {
animal.makeSound(); // Cada vez se invocará el método «correcto»
}
Facilidad de extensión
Si mañana tu jefe viene y dice: «¡Añadamos un loro!», simplemente escribes una nueva clase Parrot extends Animal y la agregas al array. El resto del código permanecerá igual. Esto es abierto para extensión y cerrado para modificación (principio OCP de SOLID).
Simplificación de la arquitectura
Puedes construir sistemas complejos donde las partes interactúan entre sí a través de abstracciones (clases base o interfaces) sin preocuparte por las implementaciones concretas. Esto ahorra tiempo, nervios y café.
4. Conceptos clave: tipo de referencia y tipo real
Tipo de referencia de la variable
Cuando escribes Animal animal = new Dog();, la variable animal tiene tipo de referencia Animal, es decir, el compilador «cree» que es un Animal y solo permite los métodos que están declarados en la clase Animal.
Tipo efectivo (real) del objeto
Pero en memoria realmente tienes un objeto de tipo Dog. Es ese tipo el que determina qué método se invocará al llamar a makeSound().
Ilustración
Animal animal = new Dog();
animal.makeSound(); // Invocará Dog.makeSound(), no Animal.makeSound()
¡Importante! A través de una referencia del tipo base (Animal) no podrás invocar métodos que existan solo en Dog si no están declarados en la clase base.
Enlace tardío (dinámico)
Es la “magia” que ocurre en tiempo de ejecución: cuando llamas a un método mediante una referencia del tipo base, la JVM mira el tipo real del objeto e invoca la implementación «correcta». Eso es el polimorfismo en acción.
5. Ejemplo práctico: polimorfismo en una aplicación
Continuemos el desarrollo de nuestra aplicación didáctica. Supongamos que escribimos una sencilla simulación de un zoológico. Tenemos una clase base Animal y varios descendientes. Queremos que todos los animales puedan «emitir un sonido», pero no queremos escribir código aparte cada vez para cada tipo de animal.
Paso 1: Clase base y descendientes
class Animal {
void makeSound() {
System.out.println("Algún sonido...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("¡Guau!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("¡Miau!");
}
}
Paso 2: Array de animales
Animal[] zoo = { new Dog(), new Cat(), new Animal() };
Paso 3: Iteración e invocación del método
for (Animal animal : zoo) {
animal.makeSound();
}
Resultado de la ejecución:
¡Guau!
¡Miau!
Algún sonido...
Observa: no sabemos de antemano quién está en el array, pero el programa se encarga y llama al método correcto.
6. Representación esquemática del polimorfismo
. Animal (makeSound)
/ \
Dog Cat
(makeSound) (makeSound)
Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()
Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()
7. Otro ejemplo: polimorfismo en un caso real
Supón que escribes un programa para gestionar a los empleados de una empresa. Tienes una clase base Employee y dos descendientes: Manager y Developer. Todos los empleados pueden trabajar — work(), pero lo hacen de distinta manera.
class Employee {
void work() {
System.out.println("El empleado trabaja.");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("El gerente dirige una reunión.");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("El desarrollador escribe código.");
}
}
Ahora puedes hacer esto:
Employee[] staff = { new Manager(), new Developer(), new Employee() };
for (Employee emp : staff) {
emp.work();
}
Resultado:
El gerente dirige una reunión.
El desarrollador escribe código.
El empleado trabaja.
8. Cuándo NO funciona el polimorfismo
El polimorfismo funciona solo para métodos declarados en la clase base. Si una subclase tiene un método propio y único, a través de una referencia del tipo base no lo verás.
class Dog extends Animal {
void fetchStick() {
System.out.println("¡El perro trae el palo!");
}
}
Animal animal = new Dog();
// animal.fetchStick(); // ¡Error de compilación! Ese método no es visible a través de Animal
Para invocar un método específico, hay que convertir la variable al tipo adecuado:
if (animal instanceof Dog) {
((Dog) animal).fetchStick();
}
Pero esa es otra historia; lo importante: a través del polimorfismo solo están disponibles los métodos declarados en la clase base.
9. Errores típicos al trabajar con polimorfismo
Error n.º 1: Esperar que a través de una referencia del tipo base estén disponibles todos los métodos de la subclase. En realidad, solo están disponibles los que se declaran en la clase base.
Error n.º 2: No usar la anotación @Override al sobrescribir un método. Sin ella se puede, por accidente, escribir un método con una firma incorrecta y entonces el polimorfismo no funcionará (el método de la clase base no quedará sobrescrito).
Error n.º 3: Intentar invocar un método específico de la subclase sin convertir el tipo. El compilador no lo permitirá porque no sabe quién está exactamente detrás de la referencia del tipo base.
Error n.º 4: Confundir sobrecarga (overloading) y sobrescritura (overriding). La sobrecarga son varios métodos con el mismo nombre y parámetros distintos en una misma clase. La sobrescritura es cambiar el comportamiento de un método en una subclase.
GO TO FULL VERSION