Java

Tutorial Java: Polimorfismo

Java polimorfismo: técnicas y ejemplos. Domina las técnicas de polimorfismo en Java con ejemplos prácticos y detallados.

Aprende Java y certifícate

Concepto de polimorfismo y sus tipos

El polimorfismo permite que objetos de diferentes clases respondan de manera distinta a la misma operación. La palabra proviene del griego "poly" (muchos) y "morphos" (formas), literalmente "muchas formas".

En esencia, el polimorfismo se refiere a la capacidad de un objeto para comportarse de diferentes maneras según el contexto en que se utilice. Se puede entender como la habilidad de un objeto para "transformarse" o "adaptarse" dependiendo de cómo se acceda a él.

En Java se identifican principalmente dos tipos de polimorfismo:

  • Polimorfismo estático (o en tiempo de compilación)
  • Polimorfismo dinámico (o en tiempo de ejecución)

Polimorfismo estático

El polimorfismo estático, también conocido como polimorfismo en tiempo de compilación, se determina durante la fase de compilación del programa. Este tipo de polimorfismo se implementa principalmente a través de la sobrecarga de métodos (method overloading).

La sobrecarga de métodos permite definir varios métodos con el mismo nombre pero con diferentes parámetros dentro de una misma clase. El compilador determina qué versión del método debe invocarse basándose en los argumentos proporcionados.

public class Calculadora {
    // Método para sumar dos enteros
    public int sumar(int a, int b) {
        return a + b;
    }
    
    // Método sobrecargado para sumar tres enteros
    public int sumar(int a, int b, int c) {
        return a + b + c;
    }
    
    // Método sobrecargado para sumar dos números de punto flotante
    public double sumar(double a, double b) {
        return a + b;
    }
}

En este ejemplo, la clase Calculadora implementa tres versiones del método sumar. El compilador selecciona automáticamente el método adecuado según los tipos y número de argumentos proporcionados cuando se llama al método.

Calculadora calc = new Calculadora();
int resultado1 = calc.sumar(5, 7);           // Llama al primer método
int resultado2 = calc.sumar(5, 7, 3);        // Llama al segundo método
double resultado3 = calc.sumar(5.5, 7.3);    // Llama al tercer método

Se denomina "estático" porque la decisión sobre qué método invocar se toma durante la compilación, no durante la ejecución del programa.

Polimorfismo dinámico

El polimorfismo dinámico, también conocido como polimorfismo en tiempo de ejecución, se determina durante la ejecución del programa. Este tipo de polimorfismo se implementa principalmente a través de la sobreescritura de métodos (method overriding) y está estrechamente relacionado con la herencia y las interfaces.

La sobreescritura de métodos permite que una clase hija proporcione una implementación específica de un método que ya está definido en su clase padre. Cuando se invoca un método sobreescrito a través de una referencia de la clase padre, Java determina en tiempo de ejecución qué versión del método debe ejecutarse basándose en el tipo real del objeto.

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

// Clase derivada
public class Perro extends Animal {
    @Override
    public void hacerSonido() {
        System.out.println("El perro ladra: Guau guau");
    }
}

// Clase derivada
public class Gato extends Animal {
    @Override
    public void hacerSonido() {
        System.out.println("El gato maulla: Miau miau");
    }
}

En este ejemplo, tanto Perro como Gato heredan de Animal y sobreescriben el método hacerSonido(). Cuando se utiliza una referencia de tipo Animal para invocar el método hacerSonido(), el comportamiento dependerá del tipo real del objeto al que apunta la referencia:

Animal miMascota1 = new Perro();
Animal miMascota2 = new Gato();

miMascota1.hacerSonido();  // Imprime: "El perro ladra: Guau guau"
miMascota2.hacerSonido();  // Imprime: "El gato maulla: Miau miau"

A pesar de que ambas variables son de tipo Animal, el método que se ejecuta es el de la clase real del objeto (Perro o Gato). Esta decisión se toma en tiempo de ejecución, no durante la compilación.

Polimorfismo a través de interfaces

Las interfaces definen un contrato que las clases deben cumplir, permitiendo que objetos de diferentes clases se comporten de manera similar si implementan la misma interfaz.

// Definición de la interfaz
public interface Dibujable {
    void dibujar();
}

// Implementaciones de la interfaz
public class Circulo implements Dibujable {
    @Override
    public void dibujar() {
        System.out.println("Dibujando un círculo");
    }
}

public class Rectangulo implements Dibujable {
    @Override
    public void dibujar() {
        System.out.println("Dibujando un rectángulo");
    }
}

Las clases que implementan la interfaz Dibujable deben proporcionar una implementación para el método dibujar(). Esto permite tratar objetos de diferentes clases de manera uniforme:

Dibujable figura1 = new Circulo();
Dibujable figura2 = new Rectangulo();

figura1.dibujar();  // Imprime: "Dibujando un círculo"
figura2.dibujar();  // Imprime: "Dibujando un rectángulo"

Polimorfismo paramétrico

Aunque es menos conocido en el contexto básico de polimorfismo, Java también soporta el polimorfismo paramétrico a través de los generics (genéricos). Este tipo de polimorfismo permite escribir código que puede trabajar con diferentes tipos de datos sin necesidad de duplicar el código.

public class Contenedor<T> {
    private T contenido;
    
    public void guardar(T objeto) {
        this.contenido = objeto;
    }
    
    public T obtener() {
        return contenido;
    }
}

En este ejemplo, la clase Contenedor puede almacenar cualquier tipo de objeto. El tipo específico se determina cuando se crea una instancia de la clase:

Contenedor<String> contenedorTexto = new Contenedor<>();
contenedorTexto.guardar("Hola mundo");
String texto = contenedorTexto.obtener();

Contenedor<Integer> contenedorNumero = new Contenedor<>();
contenedorNumero.guardar(42);
Integer numero = contenedorNumero.obtener();

El polimorfismo paramétrico permite escribir algoritmos genéricos que funcionan con cualquier tipo que cumpla con ciertos requisitos, aumentando la reutilización del código y reduciendo la duplicación.

Beneficios del polimorfismo

El polimorfismo ofrece ventajas en el desarrollo de software:

  • Flexibilidad: Permite que el código se adapte a diferentes situaciones sin necesidad de modificarlo.
  • Extensibilidad: Facilita la adición de nuevas clases que se integran con el código existente.
  • Mantenibilidad: Reduce la duplicación de código y mejora la organización.
  • Abstracción: Permite trabajar con conceptos de alto nivel sin preocuparse por los detalles de implementación.

Polimorfismo en tiempo de compilación

El polimorfismo en tiempo de compilación, también conocido como polimorfismo estático, representa una de las formas más básicas de implementar múltiples comportamientos en Java. A diferencia del polimorfismo dinámico, todas las decisiones sobre qué método invocar se toman durante la fase de compilación, antes de que el programa se ejecute.

La implementación principal de este tipo de polimorfismo se realiza mediante la sobrecarga de métodos (method overloading). Esta técnica permite definir varios métodos con el mismo nombre dentro de una clase, diferenciándose únicamente por su lista de parámetros.

Características de la sobrecarga de métodos

Para que la sobrecarga de métodos funcione correctamente, se deben cumplir ciertas condiciones:

  • Los métodos deben tener el mismo nombre
  • Los métodos deben diferir en al menos uno de estos aspectos:
    • Número de parámetros
    • Tipo de parámetros
    • Orden de los parámetros (menos común)

El tipo de retorno no se considera para determinar la sobrecarga. Dos métodos que difieren solo en el tipo de retorno generarán un error de compilación.

public class EjemploIncorrecto {
    // Error de compilación: métodos con la misma firma
    public int calcular(int x, int y) { return x + y; }
    public double calcular(int x, int y) { return x + y; } // Error
}

Resolución de métodos sobrecargados

Cuando se invoca un método sobrecargado, el compilador sigue un proceso específico para determinar qué versión del método debe ejecutarse:

1. Coincidencia exacta: Busca un método cuya firma coincida exactamente con los tipos de argumentos proporcionados. 2. Promoción de tipos primitivos: Si no encuentra una coincidencia exacta, intenta aplicar promociones de tipos primitivos (por ejemplo, de int a long). 3. Autoboxing/unboxing: Si aún no hay coincidencia, intenta aplicar autoboxing (conversión de primitivo a wrapper) o unboxing (conversión de wrapper a primitivo). 4. Varargs: Finalmente, considera métodos con parámetros varargs.

public class ResolucionSobrecarga {
    public void mostrar(int valor) {
        System.out.println("Entero: " + valor);
    }
    
    public void mostrar(long valor) {
        System.out.println("Long: " + valor);
    }
    
    public void mostrar(Integer valor) {
        System.out.println("Integer: " + valor);
    }
    
    public void mostrar(Object valor) {
        System.out.println("Object: " + valor);
    }
    
    public void mostrar(int... valores) {
        System.out.println("Varargs: " + Arrays.toString(valores));
    }
}

Cuando se invoca este método con diferentes argumentos:

ResolucionSobrecarga demo = new ResolucionSobrecarga();
int numero = 10;

demo.mostrar(numero);       // Llama a mostrar(int)
demo.mostrar(10L);          // Llama a mostrar(long)
demo.mostrar(Integer.valueOf(10)); // Llama a mostrar(Integer)
demo.mostrar("texto");      // Llama a mostrar(Object)
demo.mostrar(1, 2, 3);      // Llama a mostrar(int...)

Ambigüedad en la sobrecarga

En algunos casos, el compilador puede encontrar ambigüedades al intentar resolver qué método sobrecargado debe invocar. Esto ocurre cuando hay múltiples métodos que podrían ser candidatos válidos para una llamada específica.

public class AmbiguedadSobrecarga {
    // Método 1
    public void procesar(int num, double valor) {
        System.out.println("Método 1");
    }
    
    // Método 2
    public void procesar(double valor, int num) {
        System.out.println("Método 2");
    }
}

// Uso que genera ambigüedad
AmbiguedadSobrecarga obj = new AmbiguedadSobrecarga();
obj.procesar(10, 10); // ¿Cuál método debería llamarse?

En este ejemplo, ambos métodos son candidatos válidos: el primero requeriría convertir el segundo argumento de int a double, mientras que el segundo requeriría convertir el primer argumento. Como ambas conversiones tienen la misma prioridad, el compilador no puede decidir y generará un error.

Sobrecarga de constructores

La sobrecarga no se limita a los métodos regulares; los constructores también pueden sobrecargarse, lo que permite crear objetos de diferentes maneras:

public class Producto {
    private String nombre;
    private double precio;
    private String categoria;
    
    // Constructor completo
    public Producto(String nombre, double precio, String categoria) {
        this.nombre = nombre;
        this.precio = precio;
        this.categoria = categoria;
    }
    
    // Constructor con valores predeterminados
    public Producto(String nombre, double precio) {
        this(nombre, precio, "General"); // Llama al constructor completo
    }
    
    // Constructor mínimo
    public Producto(String nombre) {
        this(nombre, 0.0); // Llama al segundo constructor
    }
}

Esta técnica de encadenamiento de constructores mediante this() es una práctica común que evita la duplicación de código.

Sobrecarga de operadores encubierta

Aunque Java no permite la sobrecarga explícita de operadores como en C++, el operador + tiene un comportamiento especial cuando se usa con cadenas:

String resultado = "Java" + 10; // Equivale a "Java".concat(String.valueOf(10))

Este comportamiento puede considerarse una forma de sobrecarga de operadores incorporada en el lenguaje.

Aplicaciones prácticas

El polimorfismo en tiempo de compilación se utiliza mucho en la biblioteca estándar de Java y en el desarrollo de APIs:

  • APIs fluidas: Proporcionar múltiples versiones de un método para diferentes tipos de datos.
public class StringBuilder {
    public StringBuilder append(String str) { /* ... */ }
    public StringBuilder append(int i) { /* ... */ }
    public StringBuilder append(char c) { /* ... */ }
    public StringBuilder append(boolean b) { /* ... */ }
    // Muchos más métodos append sobrecargados
}
  • Métodos de utilidad: Crear versiones especializadas de métodos para casos comunes.
public class Collections {
    public static <T> boolean addAll(Collection<? super T> c, T... elements) { /* ... */ }
    public static <T> boolean addAll(Collection<? super T> c, Collection<? extends T> coll) { /* ... */ }
}
  • Patrones de diseño: El patrón Builder utiliza a menudo la sobrecarga para crear interfaces fluidas.
public class PizzaBuilder {
    private Pizza pizza = new Pizza();
    
    public PizzaBuilder addTopping(String topping) {
        pizza.addTopping(topping);
        return this;
    }
    
    public PizzaBuilder addToppings(String... toppings) {
        for (String topping : toppings) {
            pizza.addTopping(topping);
        }
        return this;
    }
    
    public Pizza build() {
        return pizza;
    }
}

Limitaciones y consideraciones

El polimorfismo en tiempo de compilación, aunque útil, tiene algunas limitaciones:

  • Decisiones estáticas: Todas las decisiones se toman en tiempo de compilación, lo que limita la flexibilidad en comparación con el polimorfismo dinámico.
  • Complejidad potencial: El uso excesivo de sobrecarga puede hacer que el código sea difícil de entender y mantener.
  • Confusión con la sobreescritura: Los desarrolladores a veces confunden la sobrecarga (polimorfismo estático) con la sobreescritura (polimorfismo dinámico).
// Ejemplo de confusión común
class Base {
    void metodo(int x) { System.out.println("Base: " + x); }
}

class Derivada extends Base {
    // Esto es sobrecarga, NO sobreescritura
    void metodo(String x) { System.out.println("Derivada: " + x); }
}

En este ejemplo, metodo(String) en la clase Derivada no sobreescribe metodo(int) de la clase Base, sino que lo sobrecarga. Esto puede llevar a comportamientos inesperados si no se comprende bien la diferencia.

Buenas prácticas

Para utilizar eficazmente el polimorfismo en tiempo de compilación:

  • Mantener la coherencia: Los métodos sobrecargados deben realizar operaciones conceptualmente similares.
  • Documentar claramente: Especificar el propósito de cada versión sobrecargada.
  • Evitar ambigüedades: Diseñar las firmas de métodos para minimizar posibles confusiones.
  • No abusar: Utilizar la sobrecarga cuando realmente añada valor, no solo por conveniencia.

Polimorfismo en tiempo de ejecución

A diferencia del polimorfismo estático, donde las decisiones se toman durante la compilación, el polimorfismo en tiempo de ejecución, también conocido como polimorfismo dinámico, permite que el comportamiento de un objeto se determine en el momento de la ejecución del programa.

Este tipo de polimorfismo se basa principalmente en dos conceptos fundamentales: la herencia y la sobreescritura de métodos. La combinación de estos elementos permite que un objeto de una clase derivada pueda ser tratado como un objeto de su clase base, mientras mantiene su comportamiento específico.

Fundamentos del polimorfismo dinámico

El polimorfismo en tiempo de ejecución se implementa cuando:

  • Una clase hereda de otra o implementa una interfaz
  • La clase hija sobreescribe uno o más métodos de la clase padre
  • Se utiliza una referencia de tipo padre para apuntar a un objeto de tipo hijo
// Clase base
public class Figura {
    public void dibujar() {
        System.out.println("Dibujando una figura genérica");
    }
}

// Clases derivadas
public class Circulo extends Figura {
    @Override
    public void dibujar() {
        System.out.println("Dibujando un círculo");
    }
}

public class Rectangulo extends Figura {
    @Override
    public void dibujar() {
        System.out.println("Dibujando un rectángulo");
    }
}

Cuando se utilizan estas clases de manera polimórfica:

Figura figura1 = new Circulo();
Figura figura2 = new Rectangulo();

figura1.dibujar(); // Imprime: "Dibujando un círculo"
figura2.dibujar(); // Imprime: "Dibujando un rectángulo"

Aunque ambas variables son de tipo Figura, el método que se ejecuta es el de la clase real del objeto. Esta decisión se toma durante la ejecución del programa, no durante la compilación.

Enlace dinámico (Dynamic Binding)

El enlace dinámico es el mecanismo que permite al sistema determinar en tiempo de ejecución qué método debe invocarse. En Java, este proceso se realiza automáticamente para métodos de instancia no estáticos, privados o finales.

Cuando se invoca un método en un objeto, la JVM sigue estos pasos:

1. Determina el tipo real del objeto (no el tipo de la referencia) 2. Busca la implementación del método en la clase del objeto 3. Si no encuentra el método, busca en la clase padre, y así sucesivamente 4. Ejecuta la primera implementación que encuentra

public class Animal {
    public void comer() {
        System.out.println("El animal come algo");
    }
}

public class Perro extends Animal {
    @Override
    public void comer() {
        System.out.println("El perro come croquetas");
    }
    
    public void ladrar() {
        System.out.println("Guau guau");
    }
}

// Uso
Animal miMascota = new Perro();
miMascota.comer();   // Imprime: "El perro come croquetas"
// miMascota.ladrar(); // Error de compilación

En este ejemplo, aunque miMascota es una referencia de tipo Animal, el método comer() que se ejecuta es el de la clase Perro. Sin embargo, no se puede llamar al método ladrar() a través de esta referencia, ya que no está definido en la clase Animal.

Casting de tipos en polimorfismo

Para acceder a los métodos específicos de la clase derivada a través de una referencia de la clase base, se puede realizar un casting de tipos:

Animal miMascota = new Perro();
// Para acceder a métodos específicos de Perro
if (miMascota instanceof Perro) {
    Perro miPerro = (Perro) miMascota;
    miPerro.ladrar(); // Ahora sí funciona
}

Con las mejoras de pattern matching en Java moderno, este código se puede simplificar:

Animal miMascota = new Perro();
if (miMascota instanceof Perro miPerro) {
    miPerro.ladrar(); // Más conciso
}

Polimorfismo con clases abstractas

Las clases abstractas proporcionan una forma de implementar polimorfismo, ya que pueden definir métodos abstractos que las clases hijas deben implementar:

public abstract class Empleado {
    protected String nombre;
    protected double salarioBase;
    
    public Empleado(String nombre, double salarioBase) {
        this.nombre = nombre;
        this.salarioBase = salarioBase;
    }
    
    // Método abstracto que debe ser implementado por las subclases
    public abstract double calcularSalario();
    
    // Método concreto que puede ser heredado o sobreescrito
    public void mostrarDetalles() {
        System.out.println("Nombre: " + nombre);
        System.out.println("Salario: " + calcularSalario());
    }
}

public class EmpleadoTiempoCompleto extends Empleado {
    private double bonificacion;
    
    public EmpleadoTiempoCompleto(String nombre, double salarioBase, double bonificacion) {
        super(nombre, salarioBase);
        this.bonificacion = bonificacion;
    }
    
    @Override
    public double calcularSalario() {
        return salarioBase + bonificacion;
    }
}

public class EmpleadoTiempoParcial extends Empleado {
    private int horasTrabajadas;
    private double tarifaPorHora;
    
    public EmpleadoTiempoParcial(String nombre, double salarioBase, 
                                int horasTrabajadas, double tarifaPorHora) {
        super(nombre, salarioBase);
        this.horasTrabajadas = horasTrabajadas;
        this.tarifaPorHora = tarifaPorHora;
    }
    
    @Override
    public double calcularSalario() {
        return salarioBase + (horasTrabajadas * tarifaPorHora);
    }
}

Este diseño permite tratar a todos los empleados de manera uniforme:

Empleado[] empleados = new Empleado[3];
empleados[0] = new EmpleadoTiempoCompleto("Ana", 2000, 500);
empleados[1] = new EmpleadoTiempoParcial("Carlos", 1000, 20, 15);
empleados[2] = new EmpleadoTiempoCompleto("Elena", 2500, 300);

// Procesamiento polimórfico
for (Empleado emp : empleados) {
    emp.mostrarDetalles();
    System.out.println("-------------------");
}

Polimorfismo con interfaces

Las interfaces proporcionan otra forma de implementar polimorfismo, muy útil en Java que no permite herencia múltiple de clases:

public interface Pagable {
    double calcularPago();
    void procesarPago();
}

public class Factura implements Pagable {
    private String numeroFactura;
    private double monto;
    
    public Factura(String numeroFactura, double monto) {
        this.numeroFactura = numeroFactura;
        this.monto = monto;
    }
    
    @Override
    public double calcularPago() {
        return monto * 1.21; // Incluye IVA
    }
    
    @Override
    public void procesarPago() {
        System.out.println("Procesando pago de factura: " + numeroFactura);
        System.out.println("Total a pagar: " + calcularPago());
    }
}

public class Salario implements Pagable {
    private String empleado;
    private double salarioBase;
    private double deducciones;
    
    public Salario(String empleado, double salarioBase, double deducciones) {
        this.empleado = empleado;
        this.salarioBase = salarioBase;
        this.deducciones = deducciones;
    }
    
    @Override
    public double calcularPago() {
        return salarioBase - deducciones;
    }
    
    @Override
    public void procesarPago() {
        System.out.println("Procesando salario para: " + empleado);
        System.out.println("Salario neto: " + calcularPago());
    }
}

El uso polimórfico de estas clases permite procesar diferentes tipos de pagos de manera uniforme:

Pagable[] pagos = new Pagable[3];
pagos[0] = new Factura("F001", 1000);
pagos[1] = new Salario("Juan Pérez", 2500, 500);
pagos[2] = new Factura("F002", 750);

// Sistema de procesamiento de pagos
for (Pagable pago : pagos) {
    pago.procesarPago();
    System.out.println("-------------------");
}

Ventajas del polimorfismo en tiempo de ejecución

El polimorfismo dinámico ofrece ventajas en el desarrollo de software:

  • Extensibilidad: Facilita la adición de nuevas clases sin modificar el código existente.
  • Desacoplamiento: Reduce la dependencia entre componentes del sistema.
  • Reutilización: Permite compartir comportamiento común en la jerarquía de clases.
  • Mantenibilidad: Centraliza la lógica común y facilita los cambios.

Consideraciones de rendimiento

Aunque el polimorfismo dinámico es extremadamente útil, implica un pequeño costo de rendimiento debido al enlace dinámico. La JVM debe determinar en tiempo de ejecución qué método invocar, lo que requiere búsquedas en tablas virtuales.

Sin embargo, las optimizaciones modernas de la JVM, como la compilación JIT (Just-In-Time) y la inlining de métodos, reducen significativamente este impacto, haciendo que el costo sea prácticamente imperceptible en la mayoría de las aplicaciones.

Patrones de diseño basados en polimorfismo

El polimorfismo en tiempo de ejecución es fundamental para muchos patrones de diseño:

  • Patrón Strategy: Permite seleccionar algoritmos en tiempo de ejecución.
// Interfaz Strategy
public interface EstrategiaOrdenamiento {
    void ordenar(int[] datos);
}

// Implementaciones concretas
public class OrdenamientoBurbuja implements EstrategiaOrdenamiento {
    @Override
    public void ordenar(int[] datos) {
        System.out.println("Ordenando con método burbuja");
        // Implementación del algoritmo
    }
}

public class OrdenamientoQuicksort implements EstrategiaOrdenamiento {
    @Override
    public void ordenar(int[] datos) {
        System.out.println("Ordenando con quicksort");
        // Implementación del algoritmo
    }
}

// Contexto que utiliza la estrategia
public class Ordenador {
    private EstrategiaOrdenamiento estrategia;
    
    public void setEstrategia(EstrategiaOrdenamiento estrategia) {
        this.estrategia = estrategia;
    }
    
    public void ejecutarOrdenamiento(int[] datos) {
        estrategia.ordenar(datos);
    }
}
  • Patrón Factory: Crea objetos sin especificar la clase exacta.
public abstract class Documento {
    public abstract void abrir();
    public abstract void cerrar();
    public abstract void imprimir();
}

public class DocumentoPDF extends Documento {
    @Override
    public void abrir() { System.out.println("Abriendo PDF"); }
    
    @Override
    public void cerrar() { System.out.println("Cerrando PDF"); }
    
    @Override
    public void imprimir() { System.out.println("Imprimiendo PDF"); }
}

public class DocumentoWord extends Documento {
    @Override
    public void abrir() { System.out.println("Abriendo Word"); }
    
    @Override
    public void cerrar() { System.out.println("Cerrando Word"); }
    
    @Override
    public void imprimir() { System.out.println("Imprimiendo Word"); }
}

// Factory
public class FabricaDocumentos {
    public static Documento crearDocumento(String tipo) {
        return switch (tipo.toLowerCase()) {
            case "pdf" -> new DocumentoPDF();
            case "word" -> new DocumentoWord();
            default -> throw new IllegalArgumentException("Tipo de documento no soportado");
        };
    }
}

Limitaciones del polimorfismo en tiempo de ejecución

A pesar de sus ventajas, el polimorfismo dinámico tiene algunas limitaciones:

  • Solo funciona con métodos: No se aplica a atributos o variables de instancia.
  • Requiere herencia o interfaces: No es aplicable a clases no relacionadas.
  • No permite añadir nuevos métodos: A través de una referencia de la clase base, solo se puede acceder a los métodos declarados en esa clase.

Buenas prácticas

Para aprovechar al máximo el polimorfismo en tiempo de ejecución:

  • Programar hacia interfaces: Utilizar interfaces o clases abstractas como tipos de referencia.
  • Principio de sustitución de Liskov: Asegurar que las clases derivadas puedan sustituir a sus clases base sin alterar el comportamiento esperado.
  • Evitar comprobaciones de tipo: Minimizar el uso de instanceof y casting, ya que pueden indicar un diseño deficiente.
  • Favorecer la composición sobre la herencia: Cuando sea posible, utilizar composición para reutilizar código en lugar de crear jerarquías de herencia complejas.

El polimorfismo en tiempo de ejecución es una herramienta fundamental en el diseño orientado a objetos que permite crear sistemas flexibles, extensibles y mantenibles. Su comprensión y aplicación adecuada son esenciales para cualquier desarrollador Java que busque crear código de calidad profesional.

Aplicaciones prácticas del polimorfismo

Frameworks y bibliotecas

Los frameworks modernos de Java aprovechan el polimorfismo para proporcionar extensibilidad. Por ejemplo, en el desarrollo de aplicaciones web, los frameworks como Spring utilizan el polimorfismo para permitir diferentes implementaciones de servicios:

public interface UserService {
    User findById(Long id);
    List<User> findAll();
    void save(User user);
}

@Service
public class DatabaseUserService implements UserService {
    @Override
    public User findById(Long id) {
        // Implementación con base de datos
        return userRepository.findById(id).orElse(null);
    }
    
    @Override
    public List<User> findAll() {
        return userRepository.findAll();
    }
    
    @Override
    public void save(User user) {
        userRepository.save(user);
    }
}

Este diseño permite cambiar fácilmente la implementación sin modificar el código cliente:

@RestController
public class UserController {
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

Sistemas de plugins

El polimorfismo es la base de los sistemas de plugins, donde se pueden añadir nuevas funcionalidades sin modificar el código existente. Por ejemplo, un editor de texto podría definir una interfaz para plugins:

public interface EditorPlugin {
    String getName();
    void initialize(EditorContext context);
    void onDocumentOpen(Document document);
    void onDocumentClose(Document document);
}

Diferentes plugins implementarían esta interfaz:

public class SpellCheckerPlugin implements EditorPlugin {
    @Override
    public String getName() {
        return "Corrector ortográfico";
    }
    
    @Override
    public void initialize(EditorContext context) {
        // Inicialización del plugin
    }
    
    @Override
    public void onDocumentOpen(Document document) {
        // Verificar ortografía al abrir documento
    }
    
    @Override
    public void onDocumentClose(Document document) {
        // Limpiar recursos
    }
}

El editor cargaría dinámicamente estos plugins y los utilizaría de manera polimórfica:

public class TextEditor {
    private List<EditorPlugin> plugins = new ArrayList<>();
    
    public void loadPlugins() {
        // Cargar plugins dinámicamente
        ServiceLoader<EditorPlugin> loader = ServiceLoader.load(EditorPlugin.class);
        for (EditorPlugin plugin : loader) {
            plugins.add(plugin);
            plugin.initialize(new EditorContext(this));
        }
    }
    
    public void openDocument(String path) {
        Document doc = new Document(path);
        // Notificar a todos los plugins
        for (EditorPlugin plugin : plugins) {
            plugin.onDocumentOpen(doc);
        }
        // Resto del código para abrir el documento
    }
}

Patrones de diseño

El polimorfismo es esencial en varios patrones de diseño. Veamos algunos ejemplos prácticos:

Patrón Observer

Este patrón permite que un objeto notifique a múltiples observadores cuando cambia su estado:

public interface Observer {
    void update(String event, Object data);
}

public class StockMarket {
    private List<Observer> observers = new ArrayList<>();
    private Map<String, Double> stocks = new HashMap<>();
    
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    
    public void updateStock(String symbol, double price) {
        stocks.put(symbol, price);
        // Notificar a todos los observadores
        for (Observer observer : observers) {
            observer.update("STOCK_UPDATED", Map.of("symbol", symbol, "price", price));
        }
    }
}

// Implementaciones concretas
public class StockDisplay implements Observer {
    @Override
    public void update(String event, Object data) {
        if ("STOCK_UPDATED".equals(event)) {
            Map<String, Object> stockInfo = (Map<String, Object>) data;
            System.out.println("Actualización de acción: " + 
                              stockInfo.get("symbol") + " - " + 
                              stockInfo.get("price"));
        }
    }
}

public class TradingBot implements Observer {
    @Override
    public void update(String event, Object data) {
        if ("STOCK_UPDATED".equals(event)) {
            Map<String, Object> stockInfo = (Map<String, Object>) data;
            // Lógica para decidir si comprar o vender
            analyzeAndTrade((String)stockInfo.get("symbol"), (Double)stockInfo.get("price"));
        }
    }
    
    private void analyzeAndTrade(String symbol, double price) {
        // Implementación del algoritmo de trading
    }
}

Patrón Template Method

Este patrón define el esqueleto de un algoritmo, permitiendo que las subclases redefinan ciertos pasos:

public abstract class DataProcessor {
    // Método plantilla
    public final void processData(String filePath) {
        String data = readData(filePath);
        String processedData = process(data);
        saveResult(processedData);
        notifyCompletion();
    }
    
    // Métodos que pueden ser sobrescritos
    protected abstract String process(String data);
    
    // Métodos con implementación por defecto
    protected String readData(String filePath) {
        System.out.println("Leyendo datos de: " + filePath);
        // Código para leer datos
        return "datos leídos";
    }
    
    protected void saveResult(String processedData) {
        System.out.println("Guardando resultado: " + processedData);
        // Código para guardar resultados
    }
    
    protected void notifyCompletion() {
        System.out.println("Procesamiento completado");
    }
}

// Implementaciones específicas
public class CSVProcessor extends DataProcessor {
    @Override
    protected String process(String data) {
        System.out.println("Procesando datos CSV");
        // Lógica específica para procesar CSV
        return "datos CSV procesados";
    }
}

public class XMLProcessor extends DataProcessor {
    @Override
    protected String process(String data) {
        System.out.println("Procesando datos XML");
        // Lógica específica para procesar XML
        return "datos XML procesados";
    }
    
    @Override
    protected void notifyCompletion() {
        super.notifyCompletion();
        System.out.println("Enviando notificación por email");
        // Código para enviar email
    }
}

Sistemas de renderizado gráfico

En aplicaciones gráficas, el polimorfismo permite manejar diferentes tipos de elementos visuales de manera uniforme:

public abstract class Shape {
    protected int x, y;
    protected Color color;
    
    public Shape(int x, int y, Color color) {
        this.x = x;
        this.y = y;
        this.color = color;
    }
    
    public abstract void draw(Graphics g);
    public abstract boolean contains(Point p);
    public abstract void move(int deltaX, int deltaY);
}

public class Circle extends Shape {
    private int radius;
    
    public Circle(int x, int y, int radius, Color color) {
        super(x, y, color);
        this.radius = radius;
    }
    
    @Override
    public void draw(Graphics g) {
        g.setColor(color);
        g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
    }
    
    @Override
    public boolean contains(Point p) {
        return Math.sqrt(Math.pow(p.x - x, 2) + Math.pow(p.y - y, 2)) <= radius;
    }
    
    @Override
    public void move(int deltaX, int deltaY) {
        x += deltaX;
        y += deltaY;
    }
}

public class Rectangle extends Shape {
    private int width, height;
    
    public Rectangle(int x, int y, int width, int height, Color color) {
        super(x, y, color);
        this.width = width;
        this.height = height;
    }
    
    @Override
    public void draw(Graphics g) {
        g.setColor(color);
        g.fillRect(x, y, width, height);
    }
    
    @Override
    public boolean contains(Point p) {
        return p.x >= x && p.x <= x + width && p.y >= y && p.y <= y + height;
    }
    
    @Override
    public void move(int deltaX, int deltaY) {
        x += deltaX;
        y += deltaY;
    }
}

El sistema de dibujo puede manejar cualquier forma sin conocer su tipo específico:

public class DrawingCanvas extends JPanel {
    private List<Shape> shapes = new ArrayList<>();
    
    public void addShape(Shape shape) {
        shapes.add(shape);
        repaint();
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        // Dibujar todas las formas
        for (Shape shape : shapes) {
            shape.draw(g);
        }
    }
    
    public Shape getShapeAt(Point p) {
        // Recorrer la lista en orden inverso (de arriba a abajo)
        for (int i = shapes.size() - 1; i >= 0; i--) {
            if (shapes.get(i).contains(p)) {
                return shapes.get(i);
            }
        }
        return null;
    }
}

Procesamiento de pagos

En sistemas de comercio electrónico, el polimorfismo permite manejar diferentes métodos de pago:

public interface PaymentMethod {
    boolean processPayment(double amount);
    String getPaymentDetails();
    boolean supportsRefund();
    double processRefund(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    private String cardNumber;
    private String cardholderName;
    private String expiryDate;
    
    public CreditCardPayment(String cardNumber, String cardholderName, String expiryDate) {
        this.cardNumber = cardNumber;
        this.cardholderName = cardholderName;
        this.expiryDate = expiryDate;
    }
    
    @Override
    public boolean processPayment(double amount) {
        // Conectar con pasarela de pago y procesar
        System.out.println("Procesando pago con tarjeta: " + amount + "€");
        return true; // Simulación de éxito
    }
    
    @Override
    public String getPaymentDetails() {
        return "Tarjeta terminada en " + cardNumber.substring(cardNumber.length() - 4);
    }
    
    @Override
    public boolean supportsRefund() {
        return true;
    }
    
    @Override
    public double processRefund(double amount) {
        System.out.println("Reembolsando " + amount + "€ a la tarjeta");
        return amount;
    }
}

public class PayPalPayment implements PaymentMethod {
    private String email;
    private String token;
    
    public PayPalPayment(String email, String token) {
        this.email = email;
        this.token = token;
    }
    
    @Override
    public boolean processPayment(double amount) {
        // Conectar con API de PayPal
        System.out.println("Procesando pago con PayPal: " + amount + "€");
        return true;
    }
    
    @Override
    public String getPaymentDetails() {
        return "PayPal: " + email;
    }
    
    @Override
    public boolean supportsRefund() {
        return true;
    }
    
    @Override
    public double processRefund(double amount) {
        System.out.println("Reembolsando " + amount + "€ a PayPal");
        return amount;
    }
}

El sistema de procesamiento de órdenes utiliza estos métodos de pago de forma polimórfica:

public class OrderProcessor {
    public boolean processOrder(Order order, PaymentMethod paymentMethod) {
        double total = calculateTotal(order);
        
        // Verificar inventario
        if (!checkInventory(order)) {
            return false;
        }
        
        // Procesar pago
        boolean paymentSuccess = paymentMethod.processPayment(total);
        if (!paymentSuccess) {
            return false;
        }
        
        // Registrar transacción
        recordTransaction(order, paymentMethod.getPaymentDetails(), total);
        
        // Enviar confirmación
        sendConfirmation(order);
        
        return true;
    }
    
    private double calculateTotal(Order order) {
        // Cálculo del total
        return order.getItems().stream()
                .mapToDouble(item -> item.getPrice() * item.getQuantity())
                .sum();
    }
    
    private boolean checkInventory(Order order) {
        // Verificación de inventario
        return true; // Simplificado
    }
    
    private void recordTransaction(Order order, String paymentDetails, double amount) {
        // Registro de la transacción
        System.out.println("Transacción registrada: " + order.getId() + 
                          " - Pago: " + paymentDetails + 
                          " - Total: " + amount + "€");
    }
    
    private void sendConfirmation(Order order) {
        // Envío de confirmación
        System.out.println("Confirmación enviada para el pedido: " + order.getId());
    }
}

Validación de datos

El polimorfismo facilita la implementación de diferentes estrategias de validación:

public interface Validator<T> {
    boolean validate(T value);
    String getErrorMessage();
}

public class EmailValidator implements Validator<String> {
    private String errorMessage = "";
    
    @Override
    public boolean validate(String email) {
        if (email == null || email.isEmpty()) {
            errorMessage = "El email no puede estar vacío";
            return false;
        }
        
        if (!email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
            errorMessage = "Formato de email inválido";
            return false;
        }
        
        return true;
    }
    
    @Override
    public String getErrorMessage() {
        return errorMessage;
    }
}

public class PasswordValidator implements Validator<String> {
    private String errorMessage = "";
    private int minLength;
    
    public PasswordValidator(int minLength) {
        this.minLength = minLength;
    }
    
    @Override
    public boolean validate(String password) {
        if (password == null || password.isEmpty()) {
            errorMessage = "La contraseña no puede estar vacía";
            return false;
        }
        
        if (password.length() < minLength) {
            errorMessage = "La contraseña debe tener al menos " + minLength + " caracteres";
            return false;
        }
        
        if (!password.matches(".*[A-Z].*")) {
            errorMessage = "La contraseña debe contener al menos una letra mayúscula";
            return false;
        }
        
        if (!password.matches(".*[0-9].*")) {
            errorMessage = "La contraseña debe contener al menos un número";
            return false;
        }
        
        return true;
    }
    
    @Override
    public String getErrorMessage() {
        return errorMessage;
    }
}

Estas validaciones se pueden utilizar en un formulario de registro:

public class RegistrationForm {
    private Map<String, Validator<?>> validators = new HashMap<>();
    private Map<String, Object> formData = new HashMap<>();
    
    public RegistrationForm() {
        // Configurar validadores
        validators.put("email", new EmailValidator());
        validators.put("password", new PasswordValidator(8));
    }
    
    public void setField(String fieldName, Object value) {
        formData.put(fieldName, value);
    }
    
    public boolean validate() {
        boolean isValid = true;
        
        for (Map.Entry<String, Validator<?>> entry : validators.entrySet()) {
            String fieldName = entry.getKey();
            Validator validator = entry.getValue();
            
            if (formData.containsKey(fieldName)) {
                if (!validator.validate(formData.get(fieldName))) {
                    System.out.println("Error en " + fieldName + ": " + validator.getErrorMessage());
                    isValid = false;
                }
            }
        }
        
        return isValid;
    }
}

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Java online

Ejercicios de esta lección Polimorfismo

Evalúa tus conocimientos de esta lección Polimorfismo con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

Interfaces

Código

Enumeraciones Enums

Código

API java.nio 2

Puzzle

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Streams: match

Test

Gestión de errores y excepciones

Código

Datos primitivos

Puzzle

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Recursión

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Records

Programación Orientada A Objetos

Pattern Matching

Programación Orientada A Objetos

Inferencia De Tipos Con Var

Programación Orientada A Objetos

Enumeraciones Enums

Programación Orientada A Objetos

Generics

Programación Orientada A Objetos

Clases Sealed

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Api Java.nio 2

Entrada Y Salida (Io)

Api Java.time

Api Java.time

Ecosistema Jakarta Ee De Java

Frameworks Para Java

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Entender el concepto de polimorfismo en programación orientada a objetos
  • Diferenciar entre polimorfismo estático y dinámico
  • Implementar polimorfismo estático usando sobrecarga de métodos
  • Usar sobreescritura de métodos para el polimorfismo dinámico
  • Aplicar polimorfismo con interfaces y clases abstractas
  • Comprender el papel del polimorfismo en frameworks y patrones de diseño