El polimorfismo es uno de los conceptos esenciales de la Programación Orientada a Objetos (POO), y se refiere a la capacidad de los objetos de diferentes clases para ser tratados como instancias de una misma clase base. En términos más simples, significa «muchas formas», ya que permite que un método o una función se comporte de manera diferente según el objeto que lo esté utilizando. Existen dos tipos principales de polimorfismo en POO: el polimorfismo en tiempo de compilación, también conocido como sobrecarga, y el polimorfismo en tiempo de ejecución, conocido como sobrescritura.
En este artículo, exploraremos ambos tipos de polimorfismo, cómo se implementan en lenguajes de programación como Java, Python y C++, y cómo nos ayudan a escribir código más flexible y reutilizable.
1. ¿Qué es el Polimorfismo en POO?
El polimorfismo en POO se refiere a la capacidad de un método o una función para tomar múltiples formas. Esto se puede lograr de dos maneras principales:
- Polimorfismo en tiempo de compilación (sobrecarga): Permite definir varios métodos con el mismo nombre pero con diferentes parámetros. La selección del método adecuado ocurre en el momento de la compilación.
- Polimorfismo en tiempo de ejecución (sobrescritura): Permite que una subclase implemente un método que ya ha sido definido en su superclase, y el método adecuado se selecciona en tiempo de ejecución, basándose en el tipo de objeto que está llamando al método.
2. Polimorfismo en Tiempo de Compilación (Sobrecarga)
El polimorfismo en tiempo de compilación, o sobrecarga de métodos, permite que un método tenga el mismo nombre pero acepte diferentes tipos o números de parámetros. En función de los argumentos que se pasen durante la invocación del método, el compilador decidirá cuál versión del método debe ejecutar.
Ventajas de la Sobrecarga:
- Claridad: Permite tener un conjunto de métodos con el mismo nombre, lo que mejora la legibilidad del código.
- Flexibilidad: Se pueden definir múltiples variantes de una operación en una misma clase, permitiendo que el mismo nombre de método funcione para diferentes tipos de datos.
Ejemplo de Sobrecarga en Java:
class Calculadora {
// Sobrecarga de método sumar con dos enteros
int sumar(int a, int b) {
return a + b;
}
// Sobrecarga de método sumar con tres enteros
int sumar(int a, int b, int c) {
return a + b + c;
}
// Sobrecarga de método sumar con dos números de punto flotante
double sumar(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculadora calc = new Calculadora();
System.out.println(calc.sumar(2, 3)); // Llama a sumar(int, int)
System.out.println(calc.sumar(1, 2, 3)); // Llama a sumar(int, int, int)
System.out.println(calc.sumar(2.5, 3.5)); // Llama a sumar(double, double)
}
}
En este ejemplo, el método sumar está sobrecargado tres veces para manejar diferentes tipos y cantidades de parámetros.
3. Polimorfismo en Tiempo de Ejecución (Sobrescritura)
El polimorfismo en tiempo de ejecución, o sobrescritura de métodos, permite que una subclase proporcione su propia implementación de un método que ya ha sido definido en su superclase. En este caso, el método adecuado se selecciona en tiempo de ejecución, según el tipo real del objeto que invoca el método, no el tipo de referencia.
Ventajas de la Sobrescritura:
- Reutilización del código: Las subclases pueden reutilizar el nombre del método de la superclase y modificar su comportamiento sin alterar el código de la superclase.
- Flexibilidad en la ejecución: El código en tiempo de ejecución selecciona la implementación del método basada en el objeto real, no en el tipo de referencia.
Ejemplo de Sobrescritura en Java:
// Clase base
class Animal {
void hacerSonido() {
System.out.println("El animal hace un sonido.");
}
}
// Subclase que sobrescribe el método hacerSonido
class Perro extends Animal {
@Override
void hacerSonido() {
System.out.println("El perro ladra.");
}
}
class Gato extends Animal {
@Override
void hacerSonido() {
System.out.println("El gato maúlla.");
}
}
public class Main {
public static void main(String[] args) {
Animal miAnimal = new Animal();
Animal miPerro = new Perro(); // Referencia de tipo Animal, pero objeto de tipo Perro
Animal miGato = new Gato(); // Referencia de tipo Animal, pero objeto de tipo Gato
miAnimal.hacerSonido(); // Llama a hacerSonido de Animal
miPerro.hacerSonido(); // Llama a hacerSonido de Perro (sobrescrito)
miGato.hacerSonido(); // Llama a hacerSonido de Gato (sobrescrito)
}
}
En este ejemplo, el método hacerSonido es sobrescrito por las subclases Perro y Gato. Aunque las referencias son de tipo Animal, en tiempo de ejecución se invocan los métodos correctos según el tipo del objeto real.
4. Diferencias Clave entre Sobrecarga y Sobrescritura
| Característica | Sobrecarga (Compilación) | Sobrescritura (Ejecución) |
| Momento de la decisión | En tiempo de compilación | En tiempo de ejecución |
| Propósito | Proveer múltiples implementaciones de un método con el mismo nombre pero diferentes parámetros | Modificar el comportamiento de un método heredado |
| Relación entre métodos | No requiere herencia, los métodos sobrecargados pueden estar en la misma clase | Requiere herencia, el método debe estar en una clase derivada |
| Firmas de métodos | Los métodos sobrecargados deben diferir en el número o tipo de parámetros | Los métodos sobrescritos deben tener la misma firma |
5. Polimorfismo en Otros Lenguajes de Programación
Polimorfismo en Python
En Python, la sobrecarga de métodos no está soportada de manera nativa como en Java o C++, pero es posible simularla con argumentos por defecto o usando *args y **kwargs para manejar parámetros variables. Sin embargo, el polimorfismo en tiempo de ejecución a través de la sobrescritura de métodos es soportado de manera completa.
Ejemplo de Sobrescritura en Python:
# Clase base
class Animal:
def hacer_sonido(self):
print("El animal hace un sonido.")
# Subclase que sobrescribe el método
class Perro(Animal):
def hacer_sonido(self):
print("El perro ladra.")
# Subclase que sobrescribe el método
class Gato(Animal):
def hacer_sonido(self):
print("El gato maúlla.")
# Uso del polimorfismo en tiempo de ejecución
animales = [Animal(), Perro(), Gato()]
for animal in animales:
animal.hacer_sonido()
Este código demuestra cómo la sobrescritura de métodos funciona en Python, donde el método hacer_sonido se comporta de manera diferente dependiendo del objeto que lo invoque.
Polimorfismo en C++
En C++, la sobrecarga de métodos es completamente soportada y funciona de manera similar a Java. La sobrescritura también es posible, pero para que un método sea sobrescrito, debe ser declarado como virtual en la clase base, de lo contrario, no se seleccionará la versión de la subclase en tiempo de ejecución.
Ejemplo de Sobrescritura en C++:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void hacerSonido() {
cout << "El animal hace un sonido." << endl;
}
};
class Perro : public Animal {
public:
void hacerSonido() override {
cout << "El perro ladra." << endl;
}
};
class Gato : public Animal {
public:
void hacerSonido() override {
cout << "El gato maúlla." << endl;
}
};
int main() {
Animal* animal = new Animal();
Animal* perro = new Perro();
Animal* gato = new Gato();
animal->hacerSonido(); // Llama a hacerSonido de Animal
perro->hacerSonido(); // Llama a hacerSonido de Perro
gato->hacerSonido(); // Llama a hacerSonido de Gato
delete animal;
delete perro;
delete gato;
return 0;
}
Los Dos Grandes Tipos de Polimorfismo
El polimorfismo se manifiesta de dos formas principales en la programación orientada a objetos:
Polimorfismo en Tiempo de Compilación (Estático)
Se resuelve durante la compilación mediante:
Sobrecarga de Métodos (Method Overloading)
public class Calculadora {
// Mismo nombre, diferentes parámetros
public int sumar(int a, int b) {
return a + b;
}
public double sumar(double a, double b) {
return a + b;
}
public int sumar(int a, int b, int c) {
return a + b + c;
}
}
// Uso
Calculadora calc = new Calculadora();
calc.sumar(2, 3); // Llama al primer método
calc.sumar(2.5, 3.7); // Llama al segundo método
calc.sumar(1, 2, 3); // Llama al tercer método
Sobrecarga de Operadores (en lenguajes que lo soportan)
class Vector {
public:
int x, y;
// Sobrecarga del operador +
Vector operator+(const Vector& otro) {
return Vector{x + otro.x, y + otro.y};
}
};
// Uso
Vector v1{1, 2};
Vector v2{3, 4};
Vector resultado = v1 + v2; // Usa el operador sobrecargado
Polimorfismo en Tiempo de Ejecución (Dinámico)
Se resuelve durante la ejecución del programa mediante:
Sobre escritura de Métodos (Method Overriding)
class Animal {
public void hacerSonido() {
System.out.println("Sonido genérico");
}
}
class Perro extends Animal {
@Override
public void hacerSonido() {
System.out.println("Guau guau");
}
}
class Gato extends Animal {
@Override
public void hacerSonido() {
System.out.println("Miau");
}
}
// Uso polimórfico
Animal miAnimal = new Perro();
miAnimal.hacerSonido(); // Output: "Guau guau"
miAnimal = new Gato();
miAnimal.hacerSonido(); // Output: "Miau"
¿Cómo Funciona el Polimorfismo Internamente?
Detrás del polimorfismo hay mecanismos ingeniosos que vale la pena entender:
Tabla de Métodos Virtuales (VTable)
En lenguajes como Java y C++, el polimorfismo se implementa mediante tablas de métodos virtuales:
// Ejemplo técnico de implementación
class Base {
public:
virtual void metodo1() { cout << "Base::metodo1" << endl; }
virtual void metodo2() { cout << "Base::metodo2" << endl; }
};
class Derivada : public Base {
public:
void metodo1() override { cout << "Derivada::metodo1" << endl; }
void metodo3() { cout << "Derivada::metodo3" << endl; }
};
// Internamente, cada objeto tiene un puntero a su VTable
Late Binding vs Early Binding
- Early Binding: La resolución de métodos ocurre en tiempo de compilación (sobrecarga).
- Late Binding: La resolución ocurre en tiempo de ejecución (sobreescritura).
Implementaciones de Polimorfismo en Varios Lenguajes
Java
// Interfaces para polimorfismo
interface Figura {
double calcularArea();
}
class Circulo implements Figura {
private double radio;
public Circulo(double radio) {
this.radio = radio;
}
@Override
public double calcularArea() {
return Math.PI * radio * radio;
}
}
class Cuadrado implements Figura {
private double lado;
public Cuadrado(double lado) {
this.lado = lado;
}
@Override
public double calcularArea() {
return lado * lado;
}
}
// Uso polimórfico
List<Figura> figuras = Arrays.asList(new Circulo(5), new Cuadrado(4));
for (Figura figura : figuras) {
System.out.println("Área: " + figura.calcularArea());
}
Python (Duck Typing)
# Polimorfismo mediante duck typing
class Pato:
def hacer_sonido(self):
return "Cuac cuac"
class Perro:
def hacer_sonido(self):
return "Guau guau"
def hacer_sonar(animal):
print(animal.hacer_sonido())
# Cualquier objeto con el método hacer_sonido() funciona
hacer_sonar(Pato()) # Output: Cuac cuac
hacer_sonar(Perro()) # Output: Guau guau
JavaScript
// Polimorfismo en JavaScript
class Animal {
hacerSonido() {
console.log("Sonido genérico");
}
}
class Gato extends Animal {
hacerSonido() {
console.log("Miau");
}
}
// Prototipado para herencia
function Perro() {}
Perro.prototype.hacerSonido = function() {
console.log("Guau");
};
// Uso polimórfico
const animales = [new Animal(), new Gato(), new Perro()];
animales.forEach(animal => animal.hacerSonido());
Patrones de Diseño Basados en Polimorfismo
Strategy Pattern
Permite cambiar el comportamiento de un objeto en tiempo de ejecución:
interface EstrategiaPago {
void pagar(double cantidad);
}
class PagoPayPal implements EstrategiaPago {
public void pagar(double cantidad) {
System.out.println("Pagando $" + cantidad + " con PayPal");
}
}
class PagoTarjeta implements EstrategiaPago {
public void pagar(double cantidad) {
System.out.println("Pagando $" + cantidad + " con Tarjeta");
}
}
class CarritoCompra {
private EstrategiaPago estrategia;
public void setEstrategia(EstrategiaPago estrategia) {
this.estrategia = estrategia;
}
public void checkout(double total) {
estrategia.pagar(total);
}
}
// Uso
CarritoCompra carrito = new CarritoCompra();
carrito.setEstrategia(new PagoPayPal());
carrito.checkout(100.50);
Factory Method Pattern
abstract class CreadorDocumento {
public abstract Documento crearDocumento();
public void nuevoDocumento() {
Documento doc = crearDocumento();
doc.abrir();
}
}
class CreadorWord extends CreadorDocumento {
public Documento crearDocumento() {
return new DocumentoWord();
}
}
class CreadorPDF extends CreadorDocumento {
public Documento crearDocumento() {
return new DocumentoPDF();
}
}
Buenas Prácticas y Consideraciones de Rendimiento
Principio de Sustitución de Liskov (LSP)
Las subclases deben ser sustituibles por sus clases base sin alterar el comportamiento correcto del programa:
// Cumpliendo con LSP
class Rectangulo {
protected int ancho, alto;
public void setAncho(int ancho) { this.ancho = ancho; }
public void setAlto(int alto) { this.alto = alto; }
public int getArea() { return ancho * alto; }
}
class Cuadrado extends Rectangulo {
// Para cumplir con LSP, evitamos cambiar el comportamiento esperado
public void setLado(int lado) {
super.setAncho(lado);
super.setAlto(lado);
}
}
Consideraciones de Rendimiento
- Overhead de la VTable: El polimorfismo dinámico tiene un costo mínimo en rendimiento
- Cache Misses: Las llamadas virtuales pueden causar misses en la cache de CPU
- Inlining: Los métodos virtuales no pueden ser inlineados fácilmente por el compilador
Cuándo Evitar el Polimorfismo
- En sistemas de tiempo real con restricciones extremas de rendimiento
- Cuando la simplicidad es más importante que la flexibilidad
- En código donde la previsibilidad del comportamiento es crítica
Ejemplo Práctico: Sistema de Notificaciones Polimórfico
// Interface común para todas las notificaciones
interface Notificacion {
void enviar(String mensaje);
boolean verificarConfiguracion();
}
// Implementaciones concretas
class EmailNotificacion implements Notificacion {
private String direccionEmail;
public EmailNotificacion(String email) {
this.direccionEmail = email;
}
@Override
public void enviar(String mensaje) {
System.out.println("Enviando email a " + direccionEmail + ": " + mensaje);
// Lógica real de envío de email
}
@Override
public boolean verificarConfiguracion() {
return direccionEmail != null && direccionEmail.contains("@");
}
}
class SMSNotificacion implements Notificacion {
private String numeroTelefono;
public SMSNotificacion(String telefono) {
this.numeroTelefono = telefono;
}
@Override
public void enviar(String mensaje) {
System.out.println("Enviando SMS a " + numeroTelefono + ": " + mensaje);
// Lógica real de envío de SMS
}
@Override
public boolean verificarConfiguracion() {
return numeroTelefono != null && numeroTelefono.length() >= 10;
}
}
class PushNotificacion implements Notificacion {
private String dispositivoId;
public PushNotificacion(String dispositivoId) {
this.dispositivoId = dispositivoId;
}
@Override
public void enviar(String mensaje) {
System.out.println("Enviando push a " + dispositivoId + ": " + mensaje);
// Lógica real de notificación push
}
@Override
public boolean verificarConfiguracion() {
return dispositivoId != null && !dispositivoId.isEmpty();
}
}
// Servicio que usa polimorfismo
class ServicioNotificaciones {
private List<Notificacion> notificaciones;
public void agregarNotificacion(Notificacion notificacion) {
notificaciones.add(notificacion);
}
public void enviarTodas(String mensaje) {
for (Notificacion notificacion : notificaciones) {
if (notificacion.verificarConfiguracion()) {
notificacion.enviar(mensaje);
}
}
}
}
// Uso
ServicioNotificaciones servicio = new ServicioNotificaciones();
servicio.agregarNotificacion(new EmailNotificacion("usuario@ejemplo.com"));
servicio.agregarNotificacion(new SMSNotificacion("1234567890"));
servicio.agregarNotificacion(new PushNotificacion("device-12345"));
servicio.enviarTodas("¡Su pedido ha sido enviado!");
Conclusión
El polimorfismo es una herramienta poderosa en POO que permite que los objetos y métodos sean reutilizados de manera flexible. Ya sea a través de la sobrecarga de métodos en tiempo de compilación o la sobrescritura de métodos en tiempo de ejecución, el polimorfismo hace que el código sea más adaptable y escalable. Cada lenguaje de programación maneja el polimorfismo de manera ligeramente diferente, por lo que es fundamental entender cómo implementarlo correctamente para aprovechar sus beneficios.





