OOP Concepts · Polymorphism in Depth

Method Overriding

Overriding is when a child class provides its own version of a method it inherited. The call stays the same; the behaviour depends on the actual object. It is runtime polymorphism.

Beginner 5 min Lesson 14 of 38 Runnable examplesExamples use modern JavaScript (ES2022 private fields) and C# 13.
Before you start:Inheritance
0/38 in OOP Concepts
On this page · 5 min · Beginner

In simple words#

A family recipe (parent method) that a child cooks their own way. The child keeps the name but replaces the steps.

Overriding
Child replaces a parent method.
virtual
Parent allows overriding.
override
Child provides the new version.

Learning objectives#

  • Override an inherited method
  • Extend parent behaviour with super / base
  • Explain overriding vs overloading
  • Override toString for readable output

Explanation#

A subclass overrides a method by declaring one with the same name and parameters. When the method is called, the version of the actual object runs — decided at runtime.

Override to replace behaviour completely, or call super.method() (JavaScript) / base.Method() (C#) inside the override to extend it.

In C#, the parent method must be marked virtual (or abstract) and the child must say override. In JavaScript every method can be overridden.

Overloading = same name, different parameters, chosen at compile time. Overriding = same name, same parameters, different class, chosen at runtime.

Overriding toString() (ToString() in C#) controls how an object prints — very useful in logs and template strings.

Syntax#

C# Illustrative
class Notification
{
    public virtual string Channel() => "generic";
    public override string ToString() => 
quot;Notification via
{Channel()}";
} class Sms : Notification { public override string Channel() => "sms"; }

Code example: Notifications that know their channel#

JavaScriptNotifications that know their channel Runnable
class Notification {
  constructor(to) {
    this.to = to;
  }
  channel() {
    return 'generic';
  }
  format(message) {
    return `To ${this.to}: ${message}`;
  }
  send(message) {
    return `[${this.channel()}] ${this.format(message)}`;
  }
}


class Email extends Notification {
  channel() { return 'email'; }                // replace
}


class Sms extends Notification {
  channel() { return 'sms'; }
  format(message) {                            // extend
    const text = super.format(message);
    return text.length > 30 ? text.slice(0, 27) + '...' : text;
  }
}


const list = [new Notification('admin'), new Email('asha@example.com'), new Sms('9876543210')];
for (const n of list) {
  console.log(n.send('Your order has shipped today'));
}


class Money {
  constructor(amount) { this.amount = amount; }
  toString() { return `₹${this.amount}`; }   // override Object.prototype.toString
}
console.log(`Total: ${new Money(250)}`);
Expected output
[generic] To admin: Your order has shipped today
[email] To asha@example.com: Your order has shipped today
[sms] To 9876543210: Your order h...
Total: ₹250
Opens this example in the full playground

Step-by-step explanation#

  1. send() is written once in Notification and calls this.channel() and this.format().
  2. Email replaces channel(); Sms replaces channel() and extends format().
  3. Sms.format calls super.format() to reuse the parent’s text, then shortens it to fit an SMS.
  4. The loop calls the same send() on each object, and each object’s overrides run.
  5. Money overrides toString, so the template string prints ₹250 instead of [object Object].

Real-world use case#

In ASP.NET Core, you override OnActionExecuting in a filter, ToString() in value objects for logging, and SaveChangesAsync in a DbContext to set audit fields before data is saved.

Common mistakes#

Changing the meaning of a method

An override of save() that deletes instead breaks every caller. Overrides must keep the parent’s promise (Liskov Substitution Principle).

Forgetting super / base when extending

If the parent does important work (validation, logging), skipping super.format() silently drops it.

C#: hiding with new instead of override

Without override, the parent version runs when the object is used through a parent-type variable.

Best practices#

  • Override to specialise, not to change the meaning
  • Call super / base when the parent’s work is still needed
  • Override toString / ToString for debugging output
  • In C#, mark only intended extension points virtual

Remember this#

Overriding = same signature, child class, chosen at RUNTIME.

super.method() / base.Method() = “do what my parent does, then more”.

Overloading ≠ overriding.

Practice exercise#

Your task: Create a Shape class with describe() returning "A shape", and a Circle subclass whose describe() returns the parent text plus " that is round". Use super.

Need a hint?
return super.describe() + ' that is round';
Show solution
JavaScript Runnable
class Shape { describe() { return 'A shape'; } }
class Circle extends Shape {
  describe() { return super.describe() + ' that is round'; }
}
console.log(new Circle().describe()); // A shape that is round
Opens this example in the full playground

Mini quiz#

Method Overriding quizPredict the output Question 1 of 3

What is logged?

class A { who() { return 'A'; } hello() { return 'I am ' + this.who(); } }
class B extends A { who() { return 'B'; } }
console.log(new B().hello());
Score so far: 0/0

Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.