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.EnviarLa 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.enviarEn 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.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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(); // DerivadaNewImplicació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.
| Lenguaje | Contrato | Implementaciones |
|---|---|---|
| C# | clase abstracta o interfaz | override/implementación |
| Java | interfaz 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)); // 90Java
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)); // 90En 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
newen 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,
finalyprivateno se sobrescriben; en C#, métodos novirtualno se puedenoverride. - 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
- Inspecciona el tipo dinámico: en C# mira
obj.GetType(); en Javaobj.getClass(). Confirma qué instancia real hay detrás de la referencia. - Verifica si el método es virtual/override: en C# revisa que la base tenga
virtual/abstracty la derivadaoverride(nonew). En Java, confirma que el método no seafinal/privatey que la firma coincida. - 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.
- 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. - Busca “hiding” en C#: si el IDE muestra advertencias como “hides inherited member”, no lo ignores. Decide explícitamente:
override(polimorfismo) onew(ocultación). - 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
newsalvo 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) osealed 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.