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ícateConcepto 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;
}
}
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
Listas
Métodos de la clase String
Streams: reduce()
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
CRUD en Java de modelo Customer sobre un ArrayList
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
Interfaces
Enumeraciones Enums
API java.nio 2
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
CRUD en Java de modelo Customer sobre un HashMap
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Streams: match
Gestión de errores y excepciones
Datos primitivos
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
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