Métodos virtuales, override y dispatch: cómo funciona el polimorfismo en la práctica

Capítulo 7

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Dispatch dinámico: qué pasa realmente cuando llamas a un método

El polimorfismo “de verdad” aparece cuando una variable de tipo base (clase padre o interfaz) apunta a un objeto concreto, y al invocar un método, el runtime decide qué implementación ejecutar según el tipo real del objeto (no según el tipo de la variable). A esto se le llama dispatch dinámico.

Idea clave: el tipo estático (el declarado en la variable) controla qué miembros son visibles/compilables; el tipo dinámico (el del objeto en memoria) controla qué implementación se ejecuta para métodos virtuales/override.

Ejemplo comparable (C#): referencia base, ejecución concreta

abstract class Notificador { public abstract void Enviar(string msg); } class EmailNotificador : Notificador { public override void Enviar(string msg) { Console.WriteLine($"Email: {msg}"); } } class SmsNotificador : Notificador { public override void Enviar(string msg) { Console.WriteLine($"SMS: {msg}"); } } Notificador n = new EmailNotificador(); n.Enviar("Hola"); // Ejecuta EmailNotificador.Enviar n = new SmsNotificador(); n.Enviar("Hola"); // Ejecuta SmsNotificador.Enviar

La llamada n.Enviar(...) se resuelve en runtime: el runtime mira el tipo real del objeto y salta a la implementación correspondiente.

Ejemplo comparable (Java): referencia de interfaz, ejecución concreta

interface Notificador { void enviar(String msg); } class EmailNotificador implements Notificador { @Override public void enviar(String msg) { System.out.println("Email: " + msg); } } class SmsNotificador implements Notificador { @Override public void enviar(String msg) { System.out.println("SMS: " + msg); } } Notificador n = new EmailNotificador(); n.enviar("Hola"); // EmailNotificador.enviar n = new SmsNotificador(); n.enviar("Hola"); // SmsNotificador.enviar

En Java, los métodos de instancia son virtuales por defecto (salvo final, private y algunos casos especiales). Por eso, el dispatch dinámico es el comportamiento habitual.

Overloading vs overriding: no son lo mismo (y se confunden mucho)

Overloading (sobrecarga): misma función, distinta firma

La sobrecarga se decide en tiempo de compilación. El compilador elige qué método llamar según el tipo estático de los argumentos.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

// C# (idéntico concepto en Java) class Logger { public void Log(string msg) { Console.WriteLine(msg); } public void Log(Exception ex) { Console.WriteLine(ex.Message); } }

Si llamas Log(x), el compilador decide cuál usar según el tipo de x en ese punto.

Overriding (sobrescritura): misma firma, implementación distinta en subclase

La sobrescritura participa en dispatch dinámico: se decide en tiempo de ejecución (para métodos virtuales/override).

// C# abstract class Parser { public abstract int Parse(string s); } class IntParser : Parser { public override int Parse(string s) => int.Parse(s); }

Error común: creer que una sobrecarga “es polimorfismo”

La sobrecarga puede parecer polimorfismo, pero no lo es en el sentido de dispatch dinámico. Un caso típico de sorpresa ocurre cuando el tipo estático “fuerza” una sobrecarga distinta.

// C# class Printer { public void Print(object o) => Console.WriteLine("object"); public void Print(string s) => Console.WriteLine("string"); } var p = new Printer(); object x = "hola"; p.Print(x); // imprime "object" (decisión en compilación)

Aunque el objeto real sea un string, la sobrecarga se eligió por el tipo estático object.

Particularidades del lenguaje: C# vs Java

C#: virtual/override/new y el caso del “hiding”

En C#, para que haya dispatch dinámico necesitas que el método sea virtual (o abstract) en la base y override en la derivada. Si usas new, no estás sobrescribiendo: estás ocultando el método (method hiding), y eso cambia el comportamiento según el tipo estático.

class Base { public virtual void M() => Console.WriteLine("Base"); } class DerivadaOverride : Base { public override void M() => Console.WriteLine("DerivadaOverride"); } class DerivadaNew : Base { public new void M() => Console.WriteLine("DerivadaNew"); } Base a = new DerivadaOverride(); a.M(); // DerivadaOverride (dispatch dinámico) Base b = new DerivadaNew(); b.M(); // Base (porque DerivadaNew ocultó, no override) DerivadaNew c = new DerivadaNew(); c.M(); // DerivadaNew

Implicación de diseño: si esperas polimorfismo, evita new para miembros que deberían ser parte del contrato virtual. Úsalo solo cuando realmente quieres un miembro distinto y aceptas que una referencia base no lo verá.

Java: @Override como red de seguridad

En Java, @Override no es obligatorio, pero es una práctica clave porque hace que el compilador verifique que realmente estás sobrescribiendo. Sin @Override, un error de firma puede convertir tu “override” en un método nuevo (y entonces el método de la base se seguirá ejecutando).

class Base { public void m(int x) { System.out.println("Base"); } } class HijaSinOverride extends Base { // Error típico: cambia la firma sin querer (long en vez de int) public void m(long x) { System.out.println("Hija"); } } Base b = new HijaSinOverride(); b.m(1); // imprime "Base" (no hubo override)

Con @Override el compilador te avisaría:

class HijaConOverride extends Base { @Override public void m(long x) { System.out.println("Hija"); } // <-- Esto no compila: no está sobrescribiendo nada }

final (Java) y sealed (C#): cuándo bloquear la sobrescritura

Java: métodos final

Un método final no puede ser sobrescrito. Útil cuando necesitas garantizar un comportamiento estable (por ejemplo, invariantes o pasos críticos de un algoritmo) y permitir extensión solo en puntos controlados.

class Base { public final void plantilla() { paso1(); paso2(); } protected void paso1() { } protected void paso2() { } }

C#: sealed override

En C#, puedes permitir override en un nivel y luego sellarlo para impedir overrides posteriores con sealed override.

class Base { public virtual void M() => Console.WriteLine("Base"); } class Intermedia : Base { public sealed override void M() => Console.WriteLine("Intermedia"); } class Final : Intermedia { // public override void M() { } // No compila }

Implicación de diseño: sellar un override reduce sorpresas en jerarquías profundas y hace más predecible el comportamiento, a costa de reducir extensibilidad.

Ejemplos prácticos: interfaz/base invocando implementaciones concretas

Escenario: cálculo de precio con estrategias

Vas a ver el mismo patrón en ambos lenguajes: una referencia de tipo contrato invoca una implementación concreta sin conocerla.

LenguajeContratoImplementaciones
C#clase abstracta o interfazoverride/implementación
Javainterfaz o clase@Override

C#

interface IDescuento { decimal Aplicar(decimal subtotal); } class DescuentoFijo : IDescuento { private readonly decimal _monto; public DescuentoFijo(decimal monto) { _monto = monto; } public decimal Aplicar(decimal subtotal) => Math.Max(0, subtotal - _monto); } class DescuentoPorcentaje : IDescuento { private readonly decimal _pct; public DescuentoPorcentaje(decimal pct) { _pct = pct; } public decimal Aplicar(decimal subtotal) => subtotal * (1 - _pct); } class Checkout { private readonly IDescuento _descuento; public Checkout(IDescuento descuento) { _descuento = descuento; } public decimal Total(decimal subtotal) => _descuento.Aplicar(subtotal); } var co = new Checkout(new DescuentoPorcentaje(0.10m)); Console.WriteLine(co.Total(100)); // 90

Java

interface Descuento { double aplicar(double subtotal); } class DescuentoFijo implements Descuento { private final double monto; DescuentoFijo(double monto) { this.monto = monto; } @Override public double aplicar(double subtotal) { return Math.max(0, subtotal - monto); } } class DescuentoPorcentaje implements Descuento { private final double pct; DescuentoPorcentaje(double pct) { this.pct = pct; } @Override public double aplicar(double subtotal) { return subtotal * (1 - pct); } } class Checkout { private final Descuento descuento; Checkout(Descuento descuento) { this.descuento = descuento; } double total(double subtotal) { return descuento.aplicar(subtotal); } } Checkout co = new Checkout(new DescuentoPorcentaje(0.10)); System.out.println(co.total(100)); // 90

En ambos casos, Checkout llama a aplicar sin saber si es fijo o porcentaje. El dispatch dinámico elige la implementación.

Errores comunes y cómo detectarlos rápido

  • Confundir sobrecarga con sobrescritura: si cambias parámetros, no estás sobrescribiendo; estás creando otro método.
  • C# usar new sin querer: si ves new en un método con el mismo nombre que la base, revisa si esperabas polimorfismo.
  • Java olvidar @Override: aumenta el riesgo de “no override” por firma distinta.
  • Métodos no virtualizables: en Java, final y private no se sobrescriben; en C#, métodos no virtual no se pueden override.
  • Llamadas desde constructores: llamar métodos virtuales desde constructores puede ejecutar código de la subclase antes de que esté inicializada (sorpresas difíciles de depurar). Evita depender de estado de la subclase en overrides que puedan ejecutarse durante construcción.

Guía práctica de depuración: rastrear qué implementación se ejecuta y evitar sorpresas

Paso a paso para rastrear el dispatch

  1. Inspecciona el tipo dinámico: en C# mira obj.GetType(); en Java obj.getClass(). Confirma qué instancia real hay detrás de la referencia.
  2. Verifica si el método es virtual/override: en C# revisa que la base tenga virtual/abstract y la derivada override (no new). En Java, confirma que el método no sea final/private y que la firma coincida.
  3. Coloca breakpoints en todas las implementaciones: pon un breakpoint en el método base y en cada override/implementación. Ejecuta y observa cuál se activa.
  4. Revisa la firma exacta: tipos, orden de parámetros, genéricos, ref/out (C#), boxing, varargs (Java). Un pequeño cambio puede convertir override en overload.
  5. Busca “hiding” en C#: si el IDE muestra advertencias como “hides inherited member”, no lo ignores. Decide explícitamente: override (polimorfismo) o new (ocultación).
  6. Confirma el punto de llamada: si la llamada se hace a través de una referencia base/interfaz, solo participarán miembros del contrato. Si se hace con el tipo concreto, podrías estar llamando a un método oculto o a una sobrecarga distinta.

Checklist para evitar sorpresas en diseño

  • En Java, usa siempre @Override en métodos que pretenden sobrescribir.
  • En C#, marca explícitamente virtual/override y evita new salvo que sea intencional.
  • Prefiere contratos pequeños y claros: cuanto más grande el contrato, más fácil introducir sobrecargas accidentales o firmas inconsistentes.
  • Si necesitas estabilidad, sella: final (Java) o sealed override (C#) cuando una variación posterior rompería invariantes o expectativas.
  • No dependas de overrides durante construcción: evita llamar métodos virtuales desde constructores o inicializadores que puedan disparar comportamiento incompleto.

Ahora responde el ejercicio sobre el contenido:

En un lenguaje OO, una variable declarada con un tipo base/interfaz apunta a un objeto concreto y se invoca un método que puede estar sobrescrito. ¿Qué afirmación describe correctamente qué decide el compilador y qué decide el runtime en este caso?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

El tipo estático determina qué llamadas son válidas/visibles al compilar. Para métodos virtuales o sobrescritos, la implementación que se ejecuta se resuelve en runtime según el tipo real del objeto (dispatch dinámico).

Siguiente capítulo

Diseño simple y robusto: acoplamiento, cohesión y principios aplicables

Arrow Right Icon
Portada de libro electrónico gratuitaPOO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)
70%

POO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.