Fundamentos de Herencia en POO: Inheritance y Polimorfismo
La herencia es un principio central de la programación orientada a objetos que permite definir propiedades y comportamientos comunes en una clase base y reutilizarlos en clases derivadas.
¿Qué es la herencia?
La herencia establece una relación “es un” entre tipos, donde una clase derivada hereda todos los miembros públicos y protegidos de la clase base, pudiendo extenderlos u override.
Conceptos clave de la herencia
- Relación es-un: la clase derivada es una especialización de la clase base
- Reutilización de código: el comportamiento común se define en un único lugar
- Polimorfismo: los objetos pueden tratarse como el tipo base
- Extensibilidad: se puede añadir nueva funcionalidad de forma controlada
Herencia básica en Java
Jerarquía de herencia simple
// Clase base abstracta
public abstract class Shape {
protected String color;
protected double x, y; // Posición
public Shape(String color, double x, double y) {
this.color = Objects.requireNonNull(color);
this.x = x;
this.y = y;
}
// Método abstracto - debe implementarse
public abstract double area();
public abstract double perimeter();
// Método concreto - puede overridearse
public void move(double dx, double dy) {
this.x += dx;
this.y += dy;
System.out.println("Forma movida a (" + x + ", " + y + ")");
}
// Getters
public String getColor() { return color; }
public double getX() { return x; }
public double getY() { return y; }
@Override
public String toString() {
return "Shape{color='" + color + "', x=" + x + ", y=" + y + "}";
}
}
// Clase derivada concreta
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); // Llamada al constructor de la clase base
setDimensions(width, height);
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public void move(double dx, double dy) {
super.move(dx, dy); // Invoca el método base
System.out.println("Rectángulo movido");
}
// Métodos adicionales
public void setDimensions(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Las dimensiones deben ser positivas");
}
this.width = width;
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
@Override
public String toString() {
return "Rectangle{" + super.toString() +
", width=" + width + ", height=" + height + "}";
}
}
// Otra clase derivada
public class Circle extends Shape {
private double radius;
public Circle(String color, double x, double y, double radius) {
super(color, x, y);
setRadius(radius);
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
public void setRadius(double radius) {
if (radius <= 0) {
throw new IllegalArgumentException("El radio debe ser positivo");
}
this.radius = radius;
}
public double getRadius() { return radius; }
@Override
public String toString() {
return "Circle{" + super.toString() + ", radius=" + radius + "}";
}
}
Uso polimórfico
public class ShapeDemo {
public static void main(String[] args) {
// Referencias polimórficas
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle("red", 0, 0, 5, 3));
shapes.add(new Circle("blue", 10, 10, 2));
shapes.add(new Rectangle("green", 5, 5, 2, 2));
// Procesamiento polimórfico
double totalArea = 0;
for (Shape shape : shapes) {
System.out.println(shape);
System.out.println("Area: " + shape.area());
System.out.println("Perimeter: " + shape.perimeter());
totalArea += shape.area();
System.out.println("---");
}
System.out.println("Total area: " + totalArea);
// Invocaciones dinámicas de métodos
processShapes(shapes);
}
public static void processShapes(List<Shape> shapes) {
for (Shape shape : shapes) {
// Dispatching dinámico: depende del tipo real del objeto
shape.move(1, 1);
// Verificación de tipo y casting
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
System.out.println("Rectangle: " + rect.getWidth() + "x" + rect.getHeight());
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
System.out.println("Circle with radius: " + circle.getRadius());
}
}
}
}
Herencia en C#
Herencia en C# con Interfaces
// Interface para definir el contrato común
public interface IShape
{
string Color { get; }
double Area { get; }
double Perimeter { get; }
void Move(double dx, double dy);
}
// Clase base abstracta
public abstract class Shape : IShape
{
protected string Color { get; set; }
protected double X { get; set; }
protected double Y { get; set; }
protected Shape(string color, double x, double y)
{
Color = color ?? throw new ArgumentNullException(nameof(color));
X = x;
Y = y;
}
// Implementación de interface
public string GetColor() => Color;
// Propiedades abstractas
public abstract double Area { get; }
public abstract double Perimeter { get; }
// Método virtual: puede sobreescribirse
public virtual void Move(double dx, double dy)
{
X += dx;
Y += dy;
Console.WriteLine($"Forma movida a ({X}, {Y})");
}
public override string ToString()
{
return $"Shape{{Color='{Color}', X={X}, Y={Y}}}";
}
}
// Clase concreta
public class Rectangle : Shape
{
public double Width { get; private set; }
public double Height { get; private set; }
public Rectangle(string color, double x, double y, double width, double height)
: base(color, x, y)
{
SetDimensions(width, height);
}
public override double Area => Width * Height;
public override double Perimeter => 2 * (Width + Height);
public override void Move(double dx, double dy)
{
base.Move(dx, dy);
Console.WriteLine("Rectángulo movido");
}
public void SetDimensions(double width, double height)
{
if (width <= 0 || height <= 0)
throw new ArgumentException("Las dimensiones deben ser positivas");
Width = width;
Height = height;
}
public override string ToString()
{
return $"Rectangle{{{base.ToString()}, Width={Width}, Height={Height}}}";
}
}
Herencia en Python
from abc import ABC, abstractmethod
from typing import List
# Clase base abstracta
class Shape(ABC):
def __init__(self, color: str, x: float, y: float):
if not color:
raise ValueError("Color darf nicht leer sein")
self.color = color
self.x = x
self.y = y
@abstractmethod
def area(self) -> float:
"""Berechnet die Fläche der Form"""
pass
@abstractmethod
def perimeter(self) -> float:
"""Berechnet den Umfang der Form"""
pass
def move(self, dx: float, dy: float):
"""Verschiebt die Form"""
self.x += dx
self.y += dy
print(f"Form verschoben nach ({self.x}, {self.y})")
def __str__(self):
return f"Shape{{color='{self.color}', x={self.x}, y={self.y}}}"
# Subclase concreta
class Rectangle(Shape):
def __init__(self, color: str, x: float, y: float, width: float, height: float):
super().__init__(color, x, y) # Aufruf des Basisklassen-Konstruktors
self.set_dimensions(width, height)
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def move(self, dx: float, dy: float):
super().move(dx, dy) # Basismethode aufrufen
print("Rechteck bewegt")
def set_dimensions(self, width: float, height: float):
if width <= 0 or height <= 0:
raise ValueError("Maße müssen positiv sein")
self.width = width
self.height = height
def __str__(self):
return f"Rectangle{{{super().__str__()}, width={self.width}, height={self.height}}}"
# Otra subclase
class Circle(Shape):
def __init__(self, color: str, x: float, y: float, radius: float):
super().__init__(color, x, y)
self.set_radius(radius)
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
def set_radius(self, radius: float):
if radius <= 0:
raise ValueError("Radius muss positiv sein")
self.radius = radius
def __str__(self):
return f"Circle{{{super().__str__()}, radius={self.radius}}}"
# Uso polimórfico
def process_shapes(shapes: List[Shape]):
total_area = 0
for shape in shapes:
print(shape)
print(f"Fläche: {shape.area():.2f}")
print(f"Umfang: {shape.perimeter():.2f}")
total_area += shape.area()
# Type Checking mit isinstance
if isinstance(shape, Rectangle):
print(f"Rechteck: {shape.width}x{shape.height}")
elif isinstance(shape, Circle):
print(f"Kreis mit Radius: {shape.radius}")
print("---")
print(f"Gesamtfläche: {total_area:.2f}")
# Verwendung
shapes = [
Rectangle("rot", 0, 0, 5, 3),
Circle("blau", 10, 10, 2),
Rectangle("grün", 5, 5, 2, 2)
]
process_shapes(shapes)
Principio de Sustitución de Liskov (LSP)
Entendiendo el principio
El Principio de Sustitución de Liskov establece que las subclases deben poder reemplazar a sus clases base sin que el comportamiento del programa cambie de forma inesperada.
Ejemplo de violación del LSP
// Mal diseño - Viola LSP
public class Rectangle {
protected double width, height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
public double area() {
return width * height;
}
}
// Subclase problemática
public class Square extends Rectangle {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width); // Quadrat muss gleiche Seiten haben
}
@Override
public void setHeight(double height) {
super.setWidth(height);
super.setHeight(height);
}
}
// Violación de LSP en la práctica
public void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
// Erwartung: area() == 20
// Bei Square: area() == 16 (unerwartet!)
assert rect.area() == 20 : "LSP verletzt!";
}
Diseño conforme con LSP
// Mejor: Clase base abstracta
public abstract class Shape {
public abstract double area();
public abstract double perimeter();
}
// Rectangle como clase independiente
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
setDimensions(width, height);
}
public void setDimensions(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Maße müssen positiv sein");
}
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
public double getWidth() { return width; }
public double getHeight() { return height; }
}
// Square como clase independiente
public class Square extends Shape {
private double side;
public Square(double side) {
setSide(side);
}
public void setSide(double side) {
if (side <= 0) {
throw new IllegalArgumentException("Seite muss positiv sein");
}
this.side = side;
}
@Override
public double area() {
return side * side;
}
@Override
public double perimeter() {
return 4 * side;
}
public double getSide() { return side; }
}
Composición antes que herencia
El problema de las jerarquías rígidas
// Mal: Jerarquía de herencia profunda
public class Animal {
public void eat() { System.out.println("Eating"); }
}
public class Mammal extends Animal {
public void walk() { System.out.println("Walking"); }
}
public class Dog extends Mammal {
public void bark() { System.out.println("Barking"); }
}
public class RobotDog extends Dog {
// Problema: RobotDog ist kein echtes Tier!
@Override
public void eat() {
throw new UnsupportedOperationException("Robots don't eat");
}
}
Mejor: Composición
// Interfaces para comportamientos
public interface Eater {
void eat();
}
public interface Walker {
void walk();
}
public interface Barker {
void bark();
}
// Clase base con comportamiento fundamental
public class Animal implements Eater {
protected String name;
public Animal(String name) {
this.name = name;
}
@Override
public void eat() {
System.out.println(name + " is eating");
}
}
// Composición para comportamientos
public class Dog implements Walker, Barker {
private Animal animal;
public Dog(String name) {
this.animal = new Animal(name);
}
@Override
public void walk() {
System.out.println(animal.name + " is walking");
}
@Override
public void bark() {
System.out.println(animal.name + " says: Woof!");
}
// Delegation an Animal
public void eat() {
animal.eat();
}
}
// Más flexible para RobotDog
public class RobotDog implements Walker, Barker {
private String name;
private int batteryLevel;
public RobotDog(String name) {
this.name = name;
this.batteryLevel = 100;
}
@Override
public void walk() {
if (batteryLevel > 10) {
System.out.println(name + " is walking on wheels");
batteryLevel -= 5;
} else {
System.out.println(name + " needs charging");
}
}
@Override
public void bark() {
System.out.println(name + " says: Electronic Woof!");
}
public void charge() {
batteryLevel = 100;
System.out.println(name + " is fully charged");
}
}
Clases abstractas vs Interfaces
Clase abstracta
// Clase abstracta con implementación común
public abstract class Vehicle {
protected String brand;
protected int year;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
// Método concreto
public void startEngine() {
System.out.println("Engine starting...");
}
// Métodos abstractos
public abstract void accelerate();
public abstract void brake();
// Template Method Pattern
public final void drive() {
startEngine();
accelerate();
System.out.println("Driving...");
brake();
}
// Getters
public String getBrand() { return brand; }
public int getYear() { return year; }
}
public class Car extends Vehicle {
public Car(String brand, int year) {
super(brand, year);
}
@Override
public void accelerate() {
System.out.println("Car accelerating");
}
@Override
public void brake() {
System.out.println("Car braking");
}
}
Interface
// Interface para comportamiento
public interface Electric {
void charge();
int getBatteryLevel();
}
public interface Autonomous {
void enableAutopilot();
boolean isAutopilotActive();
}
// Clase implementa múltiples interfaces
public class Tesla extends Vehicle implements Electric, Autonomous {
private int batteryLevel = 80;
private boolean autopilotActive = false;
public Tesla(String brand, int year) {
super(brand, year);
}
@Override
public void accelerate() {
System.out.println("Tesla accelerating silently");
}
@Override
public void brake() {
System.out.println("Tesla regenerative braking");
}
// Implementaciones de interfaces
@Override
public void charge() {
System.out.println("Tesla charging...");
batteryLevel = 100;
}
@Override
public int getBatteryLevel() {
return batteryLevel;
}
@Override
public void enableAutopilot() {
autopilotActive = true;
System.out.println("Autopilot enabled");
}
@Override
public boolean isAutopilotActive() {
return autopilotActive;
}
}
El problema del diamante
Herencia múltiple en C++
#include <iostream>
class Animal {
public:
void eat() { std::cout << "Animal eating" << std::endl; }
};
class Mammal : virtual public Animal {
public:
void walk() { std::cout << "Mammal walking" << std::endl; }
};
class Bird : virtual public Animal {
public:
void fly() { std::cout << "Bird flying" << std::endl; }
};
// Problema del diamante resuelto con herencia virtual
class Bat : public Mammal, public Bird {
public:
void echolocate() { std::cout << "Bat echolocating" << std::endl; }
};
int main() {
Bat bat;
bat.eat(); // Sin ambigüedad gracias a herencia virtual
bat.walk(); // De Mammal
bat.fly(); // De Bird
bat.echolocate(); // Método propio
return 0;
}
Solución basada en interfaces en Java/C#
// Interface para capacidades
public interface Flyable {
void fly();
}
public interface Walkable {
void walk();
}
// Clase base
public abstract class Animal {
public abstract void eat();
}
// Clase implementa múltiples interfaces
public class Bat extends Animal implements Flyable, Walkable {
@Override
public void eat() {
System.out.println("Bat eating insects");
}
@Override
public void fly() {
System.out.println("Bat flying");
}
@Override
public void walk() {
System.out.println("Bat walking");
}
public void echolocate() {
System.out.println("Bat echolocating");
}
}
Buenas prácticas en herencia
1. Evitar jerarquías profundas
// Mal: Demasiado profundo
class Animal -> Mammal -> Dog -> Labrador -> GoldenRetriever
// Mejor: Más plano
class Animal -> Dog
class Dog -> Labrador
class Dog -> GoldenRetriever
2. Usar final para clases estables
public final class ImmutablePoint {
private final double x, y;
public ImmutablePoint(double x, double y) {
this.x = x;
this.y = y;
}
// No puede heredarse, lo que garantiza estabilidad
}
3. Template Method Pattern
public abstract class DataProcessor {
// Template Method: define el flujo
public final void processData() {
loadData();
validateData();
transformData();
saveData();
cleanup();
}
// Métodos concretos
private void loadData() {
System.out.println("Loading data...");
}
private void cleanup() {
System.out.println("Cleaning up...");
}
// Métodos abstractos, implementados por subclases
protected abstract void validateData();
protected abstract void transformData();
protected abstract void saveData();
}
public class CSVProcessor extends DataProcessor {
@Override
protected void validateData() {
System.out.println("Validating CSV data");
}
@Override
protected void transformData() {
System.out.println("Transforming CSV data");
}
@Override
protected void saveData() {
System.out.println("Saving CSV data");
}
}
Conceptos relevantes para exámenes
Distinciones importantes
-
Sobrescritura vs Sobrecarga
- Sobrescritura: misma firma en la subclase
- Sobrecarga: mismo nombre, parámetros diferentes
-
Clase abstracta vs Interface
- Clase abstracta: implementación común
- Interface: contrato puro
-
Es-un vs Tiene-un
- Es-un: herencia
- Tiene-un: composición
-
Covarianza vs Contravarianza
- Covarianza: tipos de retorno más específicos
- Contravarianza: tipos de parámetros más generales
Tareas típicas de examen
// Ejemplo de polimorfismo
public class PolymorphismDemo {
public static void main(String[] args) {
Shape[] shapes = {
new Rectangle("rojo", 0, 0, 5, 3),
new Circle("azul", 10, 10, 2)
};
for (Shape shape : shapes) {
// Dispatch dinámico
System.out.println(shape.area());
// Casting de tipo
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
System.out.println("Width: " + rect.getWidth());
}
}
}
}
Conclusión
La herencia es una herramienta poderosa, pero requiere aplicación cuidadosa:
- Usa herencia para relaciones es-un reales
- Respeta el Liskov Substitution Principle
- Prefiere composición cuando solo necesitas reutilizar código
- Mantén las jerarquías planas y estables
- Utiliza interfaces para contratos flexibles
- Emplea clases abstractas para implementación compartida
Una herencia bien diseñada promueve reutilización y polimorfismo, mientras que una herencia deficiente conduce a acoplamiento fuerte y arquitecturas frágiles.
Continuamos con la ruta de aprendizaje de OOP
Ya hemos cubierto todos los artículos de OOP. Vuelve al primer artículo: Fundamentos de Programación Orientada a Objetos OOP.

