Semestre 02, 2026
Un mismo método se comporta de forma distinta según el tipo real del objeto que lo ejecuta.
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.
Agregar un esquema nuevo no debe obligar a modificar el código que recorre la nómina.
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;
}
classDiagram
class Empleado {
-String nombre
-String tipo
-double salario
-double tarifa
-int horas
-double ventas
-double porcentaje
}
// 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;
}
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
calcularPago aparece en los cuatro recuadros con la misma firma, y la fórmula vive en la clase que le corresponde.
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());
}
}
public void mostrarRecibo() {
System.out.println(id + " " + nombre
+ " -> Q" + calcularPago());
}
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.
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;
}
}
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 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.
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;
}
}
La superclase no cambió ni una línea para que las tres subclases funcionen.
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);
E01 Ana -> Q9000.0
E02 Luis -> Q2205.0
E03 Sofía -> Q4000.0
Total: Q15205.0
40 horas normales a Q45 más 6 horas extra a Q67.5 dan los Q2205 de su recibo.
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.
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
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;
}
}
empleados.add(new Practicante("Diego", "E04", 2500));
El recorrido de la nómina no cambia ni una línea, y tampoco cambian Empleado ni las otras tres subclases.
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;
}
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;
}
Los parámetros, las variables y las colecciones se declaran con el tipo más general que sirva para el trabajo.
Empleado e = new EmpleadoPorHora(
"Luis", "E02", 45, 46);
e.calcularPago(); // corre la de EmpleadoPorHora
e.registrarHoras(8); // no compila
Suponiendo que EmpleadoPorHora incorpore un método propio.
public void registrarHoras(int extra) {
this.horas += extra;
}
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 implícito
Empleado e = new EmpleadoPorHora(
"Luis", "E02", 45, 46);
// downcasting explícito
EmpleadoPorHora h = (EmpleadoPorHora) e;
h.registrarHoras(8);
Empleado e = new EmpleadoAsalariado(
"Ana", "E01", 9000);
EmpleadoPorHora h = (EmpleadoPorHora) e; // falla
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.
for (Empleado e : empleados) {
if (e instanceof EmpleadoPorHora h) {
h.registrarHoras(8);
}
}
instanceof declara la variable en la misma línea, de modo que la verificación y la conversión ocurren juntas.
Toda clase hereda de Object, así que toString ya es un método sobrescribible.
@Override
public String toString() {
return this.nombre + " (" + this.id + ")";
}
System.out.println(empleados.get(0));
// Ana (E01)
println recibe un Object y desconoce el dominio de la nómina, y aun así termina ejecutando el toString de la subclase.
Solo los métodos de instancia sobrescritos se resuelven según el objeto real.
class Reporte {
public void imprimir(Empleado e) {
System.out.println("Recibo de empleado");
}
public void imprimir(EmpleadoPorHora e) {
System.out.println("Recibo con horas");
}
}
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.
Empleado e = new EmpleadoPorHora(
"Luis", "E02", 45, 46);
Reporte r = new Reporte();
r.imprimir(e); // imprime "Recibo de empleado"
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.
| Aspecto | Sobrecarga | Sobrescritura |
|---|---|---|
| Dónde se escriben | en la misma clase | en la subclase |
| Firma del método | distintos parámetros | la misma firma |
| Quién decide | el compilador | la máquina virtual |
| Con qué tipo decide | el declarado | el real |
| Cuándo se decide | al compilar | al ejecutar |
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"
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
}
}
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.