Fundamentos de Polimorfismo en UML: Vinculación Dinámica y Sobrescritura
El polimorfismo es un concepto central de la programación orientada a objetos y el modelado UML. Permite que objetos de diferentes clases respondan de manera distinta al mismo mensaje.
¿Qué es el polimorfismo?
El polimorfismo (literalmente, múltiples formas) describe la capacidad de los objetos de usar la misma interfaz pero con implementaciones diferentes. En UML, esto se representa mediante jerarquías de herencia e interfaces.
Tipos de polimorfismo
- Sobrescritura (Override): Una método en la subclase reemplaza el de la clase base
- Sobrecarga (Overload): Múltiples métodos con el mismo nombre pero parámetros diferentes
- Polimorfismo paramétrico: Genéricos para reutilización type-safe
- Polimorfismo ad-hoc: Sobrecarga de métodos y conversión de tipos
Representación UML del polimorfismo
Diagrama de clases con polimorfismo
@startuml
abstract class Shape {
-color: String
-x: double
-y: double
+Shape(color: String, x: double, y: double)
+move(dx: double, dy: double): void
+area(): double {abstract}
+perimeter(): double {abstract}
+toString(): String
}
class Rectangle {
-width: double
-height: double
+Rectangle(color: String, x: double, y: double, width: double, height: double)
+area(): double
+perimeter(): double
+setDimensions(width: double, height: double): void
+toString(): String
}
class Circle {
-radius: double
+Circle(color: String, x: double, y: double, radius: double)
+area(): double
+perimeter(): double
+setRadius(radius: double): void
+toString(): String
}
class Triangle {
-base: double
-height: double
+Triangle(color: String, x: double, y: double, base: double, height: double)
+area(): double
+perimeter(): double
+toString(): String
}
Shape <|-- Rectangle
Shape <|-- Circle
Shape <|-- Triangle
@enduml
Diagrama de secuencia para vinculación dinámica
@startuml
actor User
User -> ShapeProcessor: processShapes(shapes)
activate ShapeProcessor
loop para cada forma
ShapeProcessor -> Shape: area()
activate Shape
alt Rectangle
Shape --> ShapeProcessor: Rectangle.area()
else Circle
Shape --> ShapeProcessor: Circle.area()
else Triangle
Shape --> ShapeProcessor: Triangle.area()
end
deactivate Shape
ShapeProcessor -> Shape: perimeter()
activate Shape
alt Rectangle
Shape --> ShapeProcessor: Rectangle.perimeter()
else Circle
Shape --> ShapeProcessor: Circle.perimeter()
else Triangle
Shape --> ShapeProcessor: Triangle.perimeter()
end
deactivate Shape
end
ShapeProcessor --> User: Resultados
deactivate ShapeProcessor
@enduml
Vinculación dinámica en Java
Sobrescritura y envío dinámico
public class PolymorphismDemo {
// Clase base abstracta
public abstract class Shape {
protected String color;
protected double x, y;
public Shape(String color, double x, double y) {
this.color = color;
this.x = x;
this.y = y;
}
// Método que puede ser sobrescrito
public void move(double dx, double dy) {
this.x += dx;
this.y += dy;
System.out.println(color + " forma movida a (" + x + ", " + y + ")");
}
// Métodos abstractos - deben ser sobrescritos
public abstract double area();
public abstract double perimeter();
// Método concreto que puede ser sobrescrito
public String getDescription() {
return "Una forma " + color + " en posición (" + x + ", " + y + ")";
}
// Getters
public String getColor() { return color; }
public double getX() { return x; }
public double getY() { return y; }
}
// Rectangle sobrescribe los métodos abstractos
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double x, double y, double width, double height) {
super(color, x, y);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public String getDescription() {
return super.getDescription() + " (rectángulo " + width + "x" + height + ")";
}
// Método adicional
public void setDimensions(double width, double height) {
this.width = width;
this.height = height;
}
}
// Circle sobrescribe los métodos abstractos
public class Circle extends Shape {
private double radius;
public Circle(String color, double x, double y, double radius) {
super(color, x, y);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
@Override
public String getDescription() {
return super.getDescription() + " (círculo con radio " + radius + ")";
}
public void setRadius(double radius) {
this.radius = radius;
}
}
// Demostración de vinculación dinámica
public void demonstrateDynamicBinding() {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle("rojo", 0, 0, 5, 3));
shapes.add(new Circle("azul", 10, 10, 2));
shapes.add(new Rectangle("verde", 5, 5, 2, 2));
// Procesamiento polimórfico - envío dinámico
for (Shape shape : shapes) {
System.out.println(shape.getDescription());
// Vinculación dinámica - se invoca el método correcto según el tipo del objeto
double area = shape.area(); // Elige Rectangle.area() o Circle.area()
double perimeter = shape.perimeter(); // Elige Rectangle.perimeter() o Circle.perimeter()
System.out.println(" Área: " + String.format("%.2f", area));
System.out.println(" Perímetro: " + String.format("%.2f", perimeter));
// move() también puede ser sobrescrito
shape.move(1, 1);
System.out.println();
}
}
}
Sobrecarga de métodos
Sobrecarga en Java
public class MethodOverloading {
// Métodos sobrecargados para diferentes tipos de parámetros
public class Calculator {
// Sobrecarga para int
public int add(int a, int b) {
System.out.println("int add(int, int) invocado");
return a + b;
}
// Sobrecarga para double
public double add(double a, double b) {
System.out.println("double add(double, double) invocado");
return a + b;
}
// Sobrecarga para tres parámetros
public int add(int a, int b, int c) {
System.out.println("int add(int, int, int) invocado");
return a + b + c;
}
// Sobrecarga para arrays
public int add(int[] numbers) {
System.out.println("int add(int[]) invocado");
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
// Sobrecarga con varargs
public int addVarargs(int... numbers) {
System.out.println("int addVarargs(int...) invocado");
return add(numbers);
}
// Sobrecarga para diferentes tipos de objetos
public String concatenate(String a, String b) {
System.out.println("String concatenate(String, String) invocado");
return a + b;
}
public String concatenate(String a, String b, String c) {
System.out.println("String concatenate(String, String, String) invocado");
return a + b + c;
}
}
// Demostración de sobrecarga
public void demonstrateOverloading() {
Calculator calc = new Calculator();
// Se invocan diferentes sobrecargas
System.out.println("5 + 3 = " + calc.add(5, 3));
System.out.println("5.5 + 3.3 = " + calc.add(5.5, 3.3));
System.out.println("1 + 2 + 3 = " + calc.add(1, 2, 3));
System.out.println("Array sum = " + calc.add(new int[]{1, 2, 3, 4, 5}));
System.out.println("Varargs sum = " + calc.addVarargs(1, 2, 3, 4, 5));
System.out.println("Hello + World = " + calc.concatenate("Hello", "World"));
System.out.println("A + B + C = " + calc.concatenate("A", "B", "C"));
}
}
Sobrecarga con herencia
public class OverloadingWithInheritance {
public class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
public void makeSound(String intensity) {
System.out.println("Animal makes a " + intensity + " sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
// Sobrecargado, no sobrescrito
public void makeSound(String intensity) {
System.out.println("Dog barks " + intensity);
}
// Sobrecarga adicional
public void makeSound(String intensity, int times) {
for (int i = 0; i < times; i++) {
System.out.println("Dog barks " + intensity);
}
}
}
public void demonstrateOverloadingInheritance() {
Animal animal = new Animal();
Dog dog = new Dog();
Animal animalDog = new Dog(); // Upcasting
// Vinculación estática en sobrecarga (Compile-Time)
animal.makeSound(); // Animal makes a sound
animal.makeSound("loud"); // Animal makes a loud sound
dog.makeSound(); // Dog barks (sobrescrito)
dog.makeSound("loud"); // Dog barks loud (sobrecargado)
dog.makeSound("loud", 3); // Dog barks loud (3x) (sobrecargado)
// Importante: vinculación estática en sobrecarga
animalDog.makeSound(); // Dog barks (vinculación dinámica)
animalDog.makeSound("loud"); // Animal makes a loud sound (vinculación estática)
}
}
Genéricos y polimorfismo paramétrico
Clases genéricas
public class GenericPolymorphism {
// Clase Container genérica
public class Container<T> {
private T content;
private String label;
public Container(String label, T content) {
this.label = label;
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
public String getLabel() {
return label;
}
// Método genérico
public <U> Container<U> transform(Function<T, U> transformer) {
U newContent = transformer.apply(content);
return new Container<>(label, newContent);
}
@Override
public String toString() {
return label + ": " + content;
}
}
// Processor genérico
public class Processor<T> {
public List<T> filter(List<T> items, Predicate<T> predicate) {
return items.stream()
.filter(predicate)
.collect(Collectors.toList());
}
public <R> List<R> map(List<T> items, Function<T, R> mapper) {
return items.stream()
.map(mapper)
.collect(Collectors.toList());
}
public T reduce(List<T> items, BinaryOperator<T> accumulator, T identity) {
return items.stream()
.reduce(identity, accumulator);
}
}
// Demostración
public void demonstrateGenerics() {
// Container con diferentes tipos
Container<String> stringContainer = new Container<>("Text", "Hello World");
Container<Integer> intContainer = new Container<>("Zahl", 42);
Container<List<String>> listContainer = new Container<>("Liste",
Arrays.asList("A", "B", "C"));
System.out.println(stringContainer);
System.out.println(intContainer);
System.out.println(listContainer);
// Transformación con método genérico
Container<Integer> lengthContainer = stringContainer.transform(String::length);
System.out.println("Länge: " + lengthContainer);
// Processor genérico
Processor<String> stringProcessor = new Processor<>();
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
// Filtrar
List<String> longWords = stringProcessor.filter(words, s -> s.length() > 5);
System.out.println("Lange Wörter: " + longWords);
// Mapear
List<Integer> lengths = stringProcessor.map(words, String::length);
System.out.println("Längen: " + lengths);
// Reducir
String concatenated = stringProcessor.reduce(words, String::concat, "");
System.out.println("Zusammengefügt: " + concatenated);
}
}
Métodos genéricos
public class GenericMethods {
// Método genérico para comparación
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Método genérico para intercambio
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Método genérico para conversión
public static <T, R> List<R> convertList(List<T> list, Function<T, R> converter) {
return list.stream()
.map(converter)
.collect(Collectors.toList());
}
// Método genérico con wildcards
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Upper Bounded Wildcard
public static double sumOfNumbers(List<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// Lower Bounded Wildcard
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Demostración
public static void demonstrateGenericMethods() {
// Método Max
System.out.println("Max von 5 und 3: " + max(5, 3));
System.out.println("Max von 'Hello' und 'World': " + max("Hello", "World"));
// Método Swap
String[] words = {"A", "B", "C"};
System.out.println("Vor Swap: " + Arrays.toString(words));
swap(words, 0, 2);
System.out.println("Nach Swap: " + Arrays.toString(words));
// Método Convert
List<String> strings = Arrays.asList("1", "2", "3", "4", "5");
List<Integer> integers = convertList(strings, Integer::parseInt);
System.out.println("Konvertiert: " + integers);
// Métodos con wildcards
List<String> stringList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);
System.out.println("String Liste:");
printList(stringList);
System.out.println("Integer Liste:");
printList(intList);
// Upper bounded wildcard
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Summe: " + sumOfNumbers(doubles));
// Lower bounded wildcard
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println("Zahlen hinzugefügt: " + numbers);
}
}
Interfaces y Polimorfismo
Polimorfismo basado en interfaces
public class InterfacePolymorphism {
// Interface para comportamiento polimórfico
public interface Drawable {
void draw();
double getArea();
String getType();
}
public interface Movable {
void move(double dx, double dy);
void setPosition(double x, double y);
double[] getPosition();
}
// Clase que implementa múltiples interfaces
public class Circle implements Drawable, Movable {
private double radius, x, y;
public Circle(double radius, double x, double y) {
this.radius = radius;
this.x = x;
this.y = y;
}
@Override
public void draw() {
System.out.println("Zeichne Kreis an (" + x + ", " + y + ") mit Radius " + radius);
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public String getType() {
return "Kreis";
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
System.out.println("Kreis verschoben nach (" + x + ", " + y + ")");
}
@Override
public void setPosition(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public double[] getPosition() {
return new double[]{x, y};
}
}
public class Rectangle implements Drawable, Movable {
private double width, height, x, y;
public Rectangle(double width, double height, double x, double y) {
this.width = width;
this.height = height;
this.x = x;
this.y = y;
}
@Override
public void draw() {
System.out.println("Zeichne Rechteck an (" + x + ", " + y + ") " + width + "x" + height);
}
@Override
public double getArea() {
return width * height;
}
@Override
public String getType() {
return "Rechteck";
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
System.out.println("Rechteck verschoben nach (" + x + ", " + y + ")");
}
@Override
public void setPosition(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public double[] getPosition() {
return new double[]{x, y};
}
}
// Procesamiento polimórfico
public void processShapes(List<Drawable> shapes) {
for (Drawable shape : shapes) {
shape.draw();
System.out.println("Tipo: " + shape.getType());
System.out.println("Área: " + shape.getArea());
System.out.println();
}
}
public void moveShapes(List<Movable> movables, double dx, double dy) {
for (Movable movable : movables) {
movable.move(dx, dy);
}
}
// Demostración
public void demonstrateInterfacePolymorphism() {
List<Drawable> shapes = new ArrayList<>();
List<Movable> movables = new ArrayList<>();
Circle circle = new Circle(2.0, 0, 0);
Rectangle rectangle = new Rectangle(3.0, 4.0, 5, 5);
shapes.add(circle);
shapes.add(rectangle);
movables.add(circle);
movables.add(rectangle);
System.out.println("=== Dibujar formas ===");
processShapes(shapes);
System.out.println("=== Mover formas ===");
moveShapes(movables, 10, 10);
System.out.println("=== Después de mover ===");
processShapes(shapes);
}
}
Notación UML para polimorfismo
Convenciones del diagrama de clases
@startuml
' Relación polimórfica en UML
abstract class PaymentProcessor {
+processPayment(amount: double): boolean {abstract}
+validatePayment(amount: double): boolean {abstract}
}
class CreditCardProcessor {
+processPayment(amount: double): boolean
+validatePayment(amount: double): boolean
+validateCardNumber(number: String): boolean
}
class PayPalProcessor {
+processPayment(amount: double): boolean
+validatePayment(amount: double): boolean
+validateEmail(email: String): boolean
}
class BankTransferProcessor {
+processPayment(amount: double): boolean
+validatePayment(amount: double): boolean
+validateBankDetails(iban: String): boolean
}
PaymentProcessor <|-- CreditCardProcessor
PaymentProcessor <|-- PayPalProcessor
PaymentProcessor <|-- BankTransferProcessor
' Interface para operaciones polimórficas
interface Refundable {
+processRefund(amount: double): boolean
+getRefundStatus(): String
}
CreditCardProcessor ..|> Refundable
PayPalProcessor ..|> Refundable
BankTransferProcessor ..|> Refundable
@enduml
Diagrama de secuencia para llamadas polimórficas
@startuml
actor Customer
Customer -> PaymentSystem: makePayment(amount, method)
activate PaymentSystem
PaymentSystem -> PaymentProcessorFactory: createProcessor(method)
activate PaymentProcessorFactory
PaymentProcessorFactory --> PaymentSystem: processor
deactivate PaymentProcessorFactory
PaymentSystem -> PaymentProcessor: processPayment(amount)
activate PaymentProcessor
alt Credit Card
PaymentProcessor -> CreditCardProcessor: processPayment(amount)
CreditCardProcessor --> PaymentProcessor: success
else PayPal
PaymentProcessor -> PayPalProcessor: processPayment(amount)
PayPalProcessor --> PaymentProcessor: success
else Bank Transfer
PaymentProcessor -> BankTransferProcessor: processPayment(amount)
BankTransferProcessor --> PaymentProcessor: success
end
PaymentProcessor --> PaymentSystem: result
deactivate PaymentProcessor
PaymentSystem --> Customer: payment result
deactivate PaymentSystem
@enduml
Mejores prácticas para polimorfismo
1. Liskov Substitution Principle
// Bien: Rectangle puede reemplazar Shape en cualquier lugar
public class GoodPolymorphism {
public interface Shape {
double area();
double perimeter();
void move(double dx, double dy);
}
public class Rectangle implements Shape {
private double width, height, x, y;
public Rectangle(double width, double height, double x, double y) {
this.width = width;
this.height = height;
this.x = x;
this.y = y;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
}
// Los métodos adicionales no violan LSP
public double getWidth() { return width; }
public double getHeight() { return height; }
}
public class Square implements Shape {
private double side, x, y;
public Square(double side, double x, double y) {
this.side = side;
this.x = x;
this.y = y;
}
@Override
public double area() {
return side * side;
}
@Override
public double perimeter() {
return 4 * side;
}
@Override
public void move(double dx, double dy) {
x += dx;
y += dy;
}
public double getSide() { return side; }
}
}
2. Segregación de Interfaces
// Malo: Interface demasiado grande
public interface BadShape {
double area();
double perimeter();
void move(double dx, double dy);
void rotate(double angle);
void resize(double factor);
Color getColor();
void setColor(Color color);
}
// Bien: Interfaces especializadas
public interface Drawable {
void draw(Graphics g);
}
public interface Movable {
void move(double dx, double dy);
void setPosition(double x, double y);
double[] getPosition();
}
public interface Resizable {
void resize(double factor);
void setSize(double width, double height);
}
public interface Rotatable {
void rotate(double angle);
double getRotation();
}
public interface Colored {
Color getColor();
void setColor(Color color);
}
// La clase implementa solo los interfaces necesarios
public class Circle implements Drawable, Movable, Resizable, Colored {
// Implementación...
}
3. Template Method Pattern
public abstract class DataProcessor {
// Template Method - define el algoritmo
public final void processData() {
loadData();
if (validateData()) {
transformData();
saveData();
onSuccess();
} else {
onError();
}
cleanup();
}
// Métodos abstractos - deben implementarse
protected abstract void loadData();
protected abstract boolean validateData();
protected abstract void transformData();
protected abstract void saveData();
// Hook methods - pueden sobrescribirse
protected void onSuccess() {
System.out.println("Procesamiento exitoso");
}
protected void onError() {
System.out.println("Procesamiento fallido");
}
protected void cleanup() {
System.out.println("Limpieza");
}
}
public class CSVProcessor extends DataProcessor {
@Override
protected void loadData() {
System.out.println("Cargando datos CSV");
}
@Override
protected boolean validateData() {
System.out.println("Validando datos CSV");
return true;
}
@Override
protected void transformData() {
System.out.println("Transformando datos CSV");
}
@Override
protected void saveData() {
System.out.println("Guardando datos CSV");
}
@Override
protected void onSuccess() {
System.out.println("¡Procesamiento CSV exitoso!");
}
}
Conceptos relevantes para el examen
Distinciones importantes
-
Sobrescritura vs Sobrecarga
- Sobrescritura: Misma firma en la subclase
- Sobrecarga: Mismo nombre, parámetros diferentes
-
Vinculación estática vs dinámica
- Estática: Sobrecarga (Compile-Time)
- Dinámica: Sobrescritura (Runtime)
-
Clase abstracta vs Interface
- Clase abstracta: Implementación compartida
- Interface: Contrato puro
-
Generics vs Herencia
- Generics: Seguridad de tipos en Compile-Time
- Herencia: Polimorfismo en Runtime
Tareas típicas de examen
- Dibuja diagramas UML para relaciones polimórficas
- Implementa métodos sobrescritos
- Explica la vinculación dinámica
- Compara diferentes tipos de polimorfismo
- Diseña jerarquías de clases polimórficas
Resumen
El polimorfismo es un concepto potente para arquitecturas de software flexibles:
- Sobrescritura permite vinculación dinámica y polimorfismo en Runtime
- Sobrecarga ofrece vinculación estática y polimorfismo en Compile-Time
- Generics permite reutilización con seguridad de tipos
- Interfaces definen contratos polimórficos
Un buen polimorfismo requiere cumplimiento del Liskov Substitution Principle y un diseño cuidadoso de interfaces para software mantenible y extensible.
Continúa en la ruta de aprendizaje de UML
El siguiente artículo en la ruta de aprendizaje de UML cubre UML Klassendiagramme: Beziehungen - Association, Aggregation, Komposition, Dependency, Inheritance — los detalles sobre los diferentes tipos de relaciones en diagramas de clases.

