← CC2008

Polimorfismo via Herencia

Semestre 02, 2026

Polimorfismo

Definición

Un mismo método se comporta de forma distinta según el tipo real del objeto que lo ejecuta.

  • El término proviene del griego poly, muchos, y morphé, formas.
  • En Java el mecanismo se conoce como invocación virtual de métodos.
  • La máquina virtual ejecuta el método del objeto real, no el del tipo con que se declaró la variable.

Las tres piezas

1Una superclase
  • Declara el método que todas las subclases responderán.
2Subclases que sobrescriben
  • Cada una redefine ese método con la misma firma y su propia lógica.
3Una referencia del tipo de la superclase
  • La variable, el parámetro o la colección se declara con el tipo general. Sin esa referencia no hay nada que resolver en tiempo de ejecución.

Nómina

Problema

Se construirá un sistema de nómina para una empresa que paga a su personal bajo tres esquemas distintos. El sistema calcula el pago del mes de cada empleado, imprime su recibo y totaliza el costo de la nómina.

  • El monto por pagar depende del esquema de contratación de cada empleado.
  • El recorrido de la nómina es idéntico para todos, sin importar el esquema.

Esquemas de pago

Asalariado: recibe un monto fijo cada mes, sin importar las horas
Por hora: cobra una tarifa por hora, y lo que pasa de 40 se paga al 150 por ciento
Comisionista: recibe un porcentaje de las ventas del mes
Todos aparecen en el mismo recibo y en el mismo total
Restricción

Agregar un esquema nuevo no debe obligar a modificar el código que recorre la nómina.

El diseño sin herencia

La salida inmediata es una sola clase con un campo que indica el esquema de cada empleado.


public class Empleado {
    private String nombre;
    private String tipo;        // asalariado...
    private double salario;
    private double tarifa;
    private int horas;
    private double ventas;
    private double porcentaje;
}

Una clase que carga con todo

classDiagram
    class Empleado {
        -String nombre
        -String tipo
        -double salario
        -double tarifa
        -int horas
        -double ventas
        -double porcentaje
    }

El cálculo por condicionales


// dentro de la clase Empleado
public double calcularPago() {
    if (this.tipo.equals("asalariado")) {
        return this.salario;
    } else if (this.tipo.equals("por_hora")) {
        return this.tarifa * this.horas;
    } else if (this.tipo.equals("comisionista")) {
        return this.ventas * this.porcentaje;
    }
    return 0;
}

Lo que cuesta este diseño

Agregar un esquema obliga a modificar el método
El mismo if se repite en recibo, reporte y bono
Un esquema mal escrito compila y devuelve cero
Nada obliga a un esquema nuevo a definir su fórmula
Cada objeto carga campos que no utiliza
La versión con polimorfismo elimina la cadena completa

La jerarquía de clases

classDiagram
    class Empleado {
        <<abstract>>
        #String nombre
        #String id
        +calcularPago() double
        +mostrarRecibo() void
    }
    class EmpleadoAsalariado {
        #double salario
        +calcularPago() double
    }
    class EmpleadoPorHora {
        #double tarifa
        #int horas
        +calcularPago() double
    }
    class EmpleadoComisionista {
        #double ventas
        #double porcentaje
        +calcularPago() double
    }
    Empleado <|-- EmpleadoAsalariado
    Empleado <|-- EmpleadoPorHora
    Empleado <|-- EmpleadoComisionista

Lectura del diagrama

El triángulo apunta a la superclase y se lee como una relación es-un
El signo numeral marca los atributos protected, visibles para la clase y sus subclases
La etiqueta abstract marca la clase que no se puede instanciar
El signo más marca los métodos públicos de la jerarquía
Clave

calcularPago aparece en los cuatro recuadros con la misma firma, y la fórmula vive en la clase que le corresponde.

La superclase Empleado


public abstract class Empleado {
    protected String nombre;
    protected String id;

    public Empleado(String nombre, String id) {
        this.nombre = nombre;
        this.id = id;
    }

    public abstract double calcularPago();

    public void mostrarRecibo() {
        System.out.println(this.id + " " + this.nombre
            + " -> Q" + calcularPago());
    }
}

Decisiones de diseño

La clase es abstract porque un empleado sin esquema no existe en la empresa
nombre e id son protected para que las subclases los usen de forma directa
calcularPago es abstracto y obliga a cada subclase a definir su fórmula
mostrarRecibo se hereda tal cual y ninguna subclase necesita reescribirlo

El método heredado


public void mostrarRecibo() {
    System.out.println(id + " " + nombre
        + " -> Q" + calcularPago());
}
Mecanismo

mostrarRecibo invoca a calcularPago sin conocer la versión que se ejecutará. Esa decisión la toma Java en tiempo de ejecución, según el objeto real.

EmpleadoAsalariado


public class EmpleadoAsalariado extends Empleado {
    protected double salario;

    public EmpleadoAsalariado(String nombre,
                              String id,
                              double salario) {
        super(nombre, id);
        this.salario = salario;
    }

    @Override
    public double calcularPago() {
        return this.salario;
    }
}

Lo que aporta la subclase

Su atributo propio se declara protected, igual que los de la superclase
super delega el nombre y el id en Empleado
La anotación @Override cumple el contrato con la misma firma
Sin ese método la clase no compila, porque el contrato queda pendiente

EmpleadoPorHora


public class EmpleadoPorHora extends Empleado {
    protected double tarifa;
    protected int horas;

    public EmpleadoPorHora(String nombre,
                           String id,
                           double tarifa,
                           int horas) {
        super(nombre, id);
        this.tarifa = tarifa;
        this.horas = horas;
    }

    @Override
    public double calcularPago() {
        if (this.horas <= 40) {
            return this.tarifa * this.horas;
        }
        return this.tarifa * 40
            + this.tarifa * 1.5 * (this.horas - 40);
    }
}

La regla de horas extra

Encapsulamiento

La fórmula de este esquema es la más complicada de las tres, y nada de esa complejidad se asoma fuera de la clase. Desde afuera la llamada sigue siendo calcularPago.

  • mostrarRecibo no se modificó y ahora imprime un pago con horas extra incluidas.
  • La subclase solo escribió aquello que la hace diferente.

EmpleadoComisionista


public class EmpleadoComisionista extends Empleado {
    protected double ventas;
    protected double porcentaje;

    public EmpleadoComisionista(String nombre,
                                String id,
                                double ventas,
                                double porcentaje) {
        super(nombre, id);
        this.ventas = ventas;
        this.porcentaje = porcentaje;
    }

    @Override
    public double calcularPago() {
        return this.ventas * this.porcentaje;
    }
}

Tres clases, una sola firma

Asalariado devuelve su salario del mes
Por hora multiplica tarifa y horas, con extras
Comisionista aplica un porcentaje a sus ventas
Las tres se invocan igual desde afuera
Clave

La superclase no cambió ni una línea para que las tres subclases funcionen.

Nómina


ArrayList<Empleado> empleados = new ArrayList<>();

empleados.add(new EmpleadoAsalariado(
    "Ana", "E01", 9000));
empleados.add(new EmpleadoPorHora(
    "Luis", "E02", 45, 46));
empleados.add(new EmpleadoComisionista(
    "Sofía", "E03", 80000, 0.05));

double total = 0;
for (Empleado e : empleados) {
    e.mostrarRecibo();
    total += e.calcularPago();
}
System.out.println("Total: Q" + total);

La salida


E01 Ana -> Q9000.0
E02 Luis -> Q2205.0
E03 Sofía -> Q4000.0
Total: Q15205.0
Cuenta de Luis

40 horas normales a Q45 más 6 horas extra a Q67.5 dan los Q2205 de su recibo.

Un requerimiento nuevo

La empresa empieza a contratar practicantes, que reciben una beca fija mensual. El sistema debe incluirlos en la nómina sin alterar el cálculo de los demás esquemas.

  • La prueba de un diseño es la cantidad de código que hay que tocar cuando llega un requerimiento.

La jerarquía extendida

classDiagram
    class Empleado {
        <<abstract>>
        +calcularPago() double
        +mostrarRecibo() void
    }
    class EmpleadoAsalariado {
        +calcularPago() double
    }
    class EmpleadoPorHora {
        +calcularPago() double
    }
    class EmpleadoComisionista {
        +calcularPago() double
    }
    class Practicante {
        #double beca
        +calcularPago() double
    }
    Empleado <|-- EmpleadoAsalariado
    Empleado <|-- EmpleadoPorHora
    Empleado <|-- EmpleadoComisionista
    Empleado <|-- Practicante

La clase Practicante


public class Practicante extends Empleado {
    protected double beca;

    public Practicante(String nombre, String id,
                       double beca) {
        super(nombre, id);
        this.beca = beca;
    }

    @Override
    public double calcularPago() {
        return this.beca;
    }
}

Lo único que cambia


empleados.add(new Practicante("Diego", "E04", 2500));
Resultado

El recorrido de la nómina no cambia ni una línea, y tampoco cambian Empleado ni las otras tres subclases.

  • Se agregó una clase nueva en lugar de modificar código que ya funcionaba.

Métodos que reciben la superclase

El polimorfismo no aparece solo en las listas, también en los parámetros.


public static double totalNomina(
        ArrayList<Empleado> empleados) {
    double total = 0;
    for (Empleado e : empleados) {
        total += e.calcularPago();
    }
    return total;
}

El mismo método para cualquier subclase


public static Empleado mejorPagado(
        ArrayList<Empleado> empleados) {
    Empleado mejor = empleados.get(0);
    for (Empleado e : empleados) {
        if (e.calcularPago() > mejor.calcularPago()) {
            mejor = e;
        }
    }
    return mejor;
}
Regla práctica

Los parámetros, las variables y las colecciones se declaran con el tipo más general que sirva para el trabajo.

Tipo declarado y tipo real

Tipo declarado: el del código, decide qué métodos se pueden invocar
Tipo real: el objeto en memoria, decide qué versión se ejecuta

Empleado e = new EmpleadoPorHora(
    "Luis", "E02", 45, 46);

e.calcularPago();      // corre la de EmpleadoPorHora
e.registrarHoras(8);   // no compila

El compilador solo ve el tipo declarado

Suponiendo que EmpleadoPorHora incorpore un método propio.


public void registrarHoras(int extra) {
    this.horas += extra;
}
Por qué falla

La variable se declaró como Empleado, y Empleado no define ese método. El objeto sí lo tiene, pero no es visible a través de esa referencia.

Upcasting y downcasting


// upcasting implícito
Empleado e = new EmpleadoPorHora(
    "Luis", "E02", 45, 46);

// downcasting explícito
EmpleadoPorHora h = (EmpleadoPorHora) e;
h.registrarHoras(8);
Upcasting: subir al tipo de la superclase, siempre seguro y automático
Downcasting: bajar a la subclase, se escribe de forma explícita y puede fallar

ClassCastException


Empleado e = new EmpleadoAsalariado(
    "Ana", "E01", 9000);

EmpleadoPorHora h = (EmpleadoPorHora) e;   // falla
Qué ocurre

El compilador acepta la conversión porque ambas clases pertenecen a la misma jerarquía. En ejecución el objeto no es un EmpleadoPorHora y se lanza la excepción.

La forma segura con instanceof


for (Empleado e : empleados) {
    if (e instanceof EmpleadoPorHora h) {
        h.registrarHoras(8);
    }
}
Java 16

instanceof declara la variable en la misma línea, de modo que la verificación y la conversión ocurren juntas.

  • Para un caso puntual es aceptable, pero una cadena de instanceof devuelve el diseño al punto de partida.

toString y la clase Object

Toda clase hereda de Object, así que toString ya es un método sobrescribible.


@Override
public String toString() {
    return this.nombre + " (" + this.id + ")";
}

Println elige la versión correcta


System.out.println(empleados.get(0));

// Ana (E01)
Mismo mecanismo

println recibe un Object y desconoce el dominio de la nómina, y aun así termina ejecutando el toString de la subclase.

Lo que no es polimorfismo dinámico

Regla

Solo los métodos de instancia sobrescritos se resuelven según el objeto real.

Sobrecarga: varios métodos con el mismo nombre y distintos parámetros
Métodos static: pertenecen a la clase y no al objeto
Atributos: el de la subclase oculta al de la superclase, no lo reemplaza
Los tres se deciden con el tipo declarado de la variable

Dos métodos con el mismo nombre


class Reporte {
    public void imprimir(Empleado e) {
        System.out.println("Recibo de empleado");
    }

    public void imprimir(EmpleadoPorHora e) {
        System.out.println("Recibo con horas");
    }
}
Sobrecarga

Aquí no hay herencia ni override. Son dos métodos distintos de la misma clase, y lo único que los diferencia es el tipo del parámetro.

La versión se elige al compilar


Empleado e = new EmpleadoPorHora(
    "Luis", "E02", 45, 46);

Reporte r = new Reporte();
r.imprimir(e);   // imprime "Recibo de empleado"
Qué ocurre

El objeto es un EmpleadoPorHora, pero la variable se declaró como Empleado. El compilador elige la versión con ese tipo declarado y el objeto real no participa en la decisión.

Sobrecarga y sobrescritura

AspectoSobrecargaSobrescritura
Dónde se escribenen la misma claseen la subclase
Firma del métododistintos parámetrosla misma firma
Quién decideel compiladorla máquina virtual
Con qué tipo decideel declaradoel real
Cuándo se decideal compilaral ejecutar

Los métodos static se ocultan


class Base {
    public static String quien() { return "Base"; }
}

class Hija extends Base {
    public static String quien() { return "Hija"; }
}

Base b = new Hija();
System.out.println(b.quien());   // imprime "Base"

Los atributos también se ocultan


class Base {
    protected String etiqueta = "base";
}

class Hija extends Base {
    protected String etiqueta = "hija";

    public void mostrar() {
        Base comoBase = this;

        System.out.println(this.etiqueta);      // hija
        System.out.println(comoBase.etiqueta);  // base
    }
}

Dos atributos en el mismo objeto

Para recordar

El objeto es uno solo y los dos atributos existen a la vez. El que se lee depende del tipo con que se llega a él, así que el de la subclase no reemplaza al de la superclase, lo oculta.

  • Ocultar no es sobrescribir.
  • Con métodos de instancia esto no pasa, porque ahí sí gana el objeto real.

Errores comunes

1Modificar la firma sin advertirlo
  • calcularPago(double bono) no sobrescribe a calcularPago(), es un método nuevo. La anotación @Override convierte ese error silencioso en error de compilación.
2Invocar un método de la subclase con una referencia de la superclase
  • El objeto lo tiene, pero el compilador no lo ve.
3Aplicar downcasting sin verificación
  • La ClassCastException aparece en ejecución, no al compilar.

Errores comunes que cuestan más

4Conservar una cadena de instanceof en el recorrido
  • Ese comportamiento pertenece a un método sobrescrito en cada subclase.
5Intentar sobrescribir métodos private, static o final
  • El primero no se hereda, el segundo se oculta y el tercero está cerrado de forma deliberada.
6Invocar un método sobrescribible desde el constructor de la superclase
  • La versión de la subclase se ejecuta antes de inicializar sus atributos, de modo que encuentra ceros y null.

Buenas prácticas

  • Declarar variables, parámetros y colecciones con el tipo más general que sirva.
  • Ubicar en la superclase lo que todos comparten y en cada subclase solo lo propio.
  • Usar abstract cuando la superclase no tenga una implementación razonable por defecto.
  • Escribir siempre @Override, porque no tiene costo y detecta errores de firma.
  • Sospechar de un if que pregunta por el tipo, casi siempre indica un método sobrescrito que falta.