Watch the video (10 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is Method Overriding?
  5. 2. The @override Annotation
  6. 3. Which Version Runs?
  7. 4. Overriding a Getter
  8. 5. Overriding toString()
  9. 6. Overriding ==
  10. 7. Why == Alone Breaks Sets and Maps
  11. 8. Overriding hashCode
  12. 9. Signature Rules
  13. 10. Calling the Parent's Version
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 60 — Method Overriding

Category10 — Advanced OOP
Lecture060 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

A child class inherits every method of its parent, exactly as written. Overriding lets the child replace a method with its own version — including toString, == and hashCode.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 001–059:

  • classes, objects, constructors, and getters (Lectures 053–056)
  • extends, parent and child classes, and super parameters (Lecture 059)
  • == versus identical() (Lecture 018) and operator methods (Lecture 021)
  • is checks and Object (Lectures 010–011)
  • sets and maps (Lectures 047–049)

Connection With Previous Lectures

In Lecture 059, a Dog inherited everything from Animal. But what if the inherited behavior is wrong for the child?

dart
class Animal {
  void speak() {
    print('...');
  }
}

class Dog extends Animal {}

void main() {
  Dog().speak();
}
output
...

A dog should bark. Other inherited methods are quietly wrong too: print(student) shows Instance of 'Student', and two identical students are not ==. Lecture 018 promised we would teach our own classes what == means. This is that lecture.


Learning Objectives

By the end of this lecture you will be able to:

  • Understand what method overriding is and why it exists
  • Override methods and getters in a child class
  • Use the @override annotation and explain how it catches typos
  • Predict which version of a method runs
  • Override toString() to give objects readable output
  • Override == and hashCode together, and explain why both are needed
  • Recognize the errors Dart gives for an override with the wrong signature

1. What Is Method Overriding?

Overriding means a child class writes its own version of a method it inherited. Same name, same parameters — new body.

Analogy — you inherit your parent's recipe book. You keep most recipes, but on one page you paste your own version of the curry. Anyone who opens your book gets your curry.

dart
class Animal {
  void speak() {
    print('...');
  }
}

class Dog extends Animal {
  @override
  void speak() {
    print('Woof!');
  }
}

void main() {
  Animal().speak();
  Dog().speak();
}
output
...
Woof!

The parent keeps its version. Only Dog objects use the new one.


2. The @override Annotation

@override is a label that says "I mean to replace an inherited member". The code runs without it, but with it Dart checks your intention. Watch a typo:

dart
class Animal {
  void speak() {
    print('...');
  }
}

class Dog extends Animal {
  @override
  void speek() {
    print('Woof!');
  }
}

void main() {
  Dog().speak();
}
output
...

It prints the parent's text: speek is a new method, not an override. dart analyze (and your editor) catches it only because of @override:

output
warning - bin/main.dart:9:8 - The method doesn't override an inherited method. Try updating
this class to match the superclass, or removing the override annotation.
- override_on_non_overriding_member

Without the annotation there is no warning at all.


3. Which Version Runs?

The object decides, not the variable's type. A child that does not override keeps the inherited version.

dart
class Animal {
  void speak() {
    print('...');
  }
}

class Dog extends Animal {
  @override
  void speak() {
    print('Woof!');
  }
}

class Fish extends Animal {}

void main() {
  Animal pet = Dog();
  pet.speak();
  Fish().speak();
}
output
Woof!
...

pet is declared as an Animal, but the object inside is a Dog, so Dog's version runs. We will build on this idea when we cover Polymorphism (Lecture 065).


4. Overriding a Getter

Getters (Lecture 056) are overridden the same way.

dart
class Account {
  final String owner;
  Account(this.owner);

  String get type => 'Basic account';
}

class SavingsAccount extends Account {
  SavingsAccount(super.owner);

  @override
  String get type => 'Savings account';
}

void main() {
  print(Account('Ali').type);
  print(SavingsAccount('Sara').type);
}
output
Basic account
Savings account

5. Overriding toString()

Every class extends Object, even without extends, and inherits its toString(), which print and interpolation call. The inherited version is not helpful:

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);
}

void main() {
  print(Student('Ali', 85));
}
output
Instance of 'Student'

Override it to describe the object:

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);

  @override
  String toString() => 'Student($name, $marks)';
}

void main() {
  var ali = Student('Ali', 85);
  print(ali);
  print('Top: $ali');
  print([ali, Student('Sara', 92)]);
}
output
Student(Ali, 85)
Top: Student(Ali, 85)
[Student(Ali, 85), Student(Sara, 92)]

Lists print their elements with toString() too.


6. Overriding ==

The == inherited from Object asks "are these the same object?" — exactly like identical() from Lecture 018. Two separate objects are never equal:

dart
class Point {
  final int x;
  final int y;
  Point(this.x, this.y);
}

void main() {
  print(Point(1, 2) == Point(1, 2));
}
output
false

To compare by values, override operator ==. Its parameter must be Object — that is the signature it inherits — so check the type with is first:

dart
class Point {
  final int x;
  final int y;
  Point(this.x, this.y);

  @override
  bool operator ==(Object other) =>
      other is Point && x == other.x && y == other.y;
}

void main() {
  print(Point(1, 2) == Point(1, 2));
  print(Point(1, 2) == Point(2, 1));
}
output
true
false

After other is Point, Dart promotes other to Point, so other.x is allowed.


7. Why == Alone Breaks Sets and Maps

That Point is only half finished. With the course's lint rules, dart analyze says:

output
info - bin/main.dart:7:17 - Missing a corresponding override of 'hashCode'. Try overriding
'hashCode' or removing '=='. - hash_and_equals

Why? A set or map (Lectures 047–049) doesn't compare a value with every element. It first asks for its hashCode — a number — and looks only among elements with that number. Object's hashCode differs for every object:

dart
class Student {
  final String name;
  final int roll;
  Student(this.name, this.roll);

  @override
  bool operator ==(Object other) =>
      other is Student && name == other.name && roll == other.roll;
}

void main() {
  var a = Student('Ali', 7);
  var b = Student('Ali', 7);
  print(a == b);
  print({a, b}.length);
  print({a: 85}[b]);
  print([a].contains(b));
}
output
true
2
null
true

a == b, yet the set keeps both and the map can't find b. The list works because it uses only ==. The rule: equal objects must have equal hash codes.


8. Overriding hashCode

hashCode is a getter. Build it from the same fields that == compares, using Object.hash:

dart
class Student {
  final String name;
  final int roll;
  Student(this.name, this.roll);

  @override
  bool operator ==(Object other) =>
      other is Student && name == other.name && roll == other.roll;

  @override
  int get hashCode => Object.hash(name, roll);
}

void main() {
  var a = Student('Ali', 7);
  var b = Student('Ali', 7);
  print({a, b}.length);
  print({a: 85}[b]);
}
output
1
85

Now the set keeps one student and the map finds the entry. Override == and hashCode together, always, from final fields so the hash code never changes.


9. Signature Rules

An override must accept the same calls as the method it replaces. Adding a required parameter breaks that:

dart
class Animal {
  void speak() {
    print('...');
  }
}

class Dog extends Animal {
  // @override
  // void speak(int times) {} // ✗
}
output
Error: The method 'Dog.speak' has more required arguments than those of overridden method
'Animal.speak'.

The return type must match too (or be a subtype of it):

dart
class Animal {
  String sound() => '...';
}

class Dog extends Animal {
  // @override
  // int sound() => 1; // ✗
}
output
Error: The return type of the method 'Dog.sound' is 'int', which does not match the return
type, 'String', of the overridden method, 'Animal.sound'.
Change to a subtype of 'String'.

The easiest rule: copy the parent's first line exactly, then change only the body.


10. Calling the Parent's Version

Sometimes you want to add to the parent's method rather than replace it. Inside an override, super.speak() runs the parent's version. We will study super fully in the next lecture, Lecture 061 — super.


11. A Real-World Example

A shopping cart where duplicate products are ignored and the receipt reads well:

dart
class Product {
  final String name;
  final int price;
  Product(this.name, this.price);

  String get label => name;

  @override
  String toString() => '$label: Rs $price';

  @override
  bool operator ==(Object other) =>
      other is Product && name == other.name && price == other.price;

  @override
  int get hashCode => Object.hash(name, price);
}

class SaleProduct extends Product {
  SaleProduct(super.name, super.price);

  @override
  String get label => '$name (SALE)';
}

void main() {
  var cart = <Product>{
    Product('Pen', 50),
    SaleProduct('Bag', 900),
    Product('Pen', 50),
  };
  for (var item in cart) {
    print(item);
  }
  print('Items: ${cart.length}');
}
output
Pen: Rs 50
Bag (SALE): Rs 900
Items: 2

SaleProduct overrides only label, yet the parent's toString() uses the new label — the object decides which version runs. The second Pen is equal to the first, so the set keeps one.


Code Examples

Example 1 — Overriding a Method

dart
class Vehicle {
  void start() {
    print('Starting...');
  }
}

class Car extends Vehicle {
  @override
  void start() {
    print('Engine on. Vroom!');
  }
}

void main() {
  Vehicle().start();
  Car().start();
}
output
Starting...
Engine on. Vroom!

Example 2 — Two Children, Two Versions

dart
class Animal {
  String sound() => '...';
}

class Cat extends Animal {
  @override
  String sound() => 'Meow';
}

class Cow extends Animal {
  @override
  String sound() => 'Moo';
}

void main() {
  var animals = [Cat(), Cow(), Animal()];
  for (var a in animals) {
    print(a.sound());
  }
}
output
Meow
Moo
...

Example 3 — Overriding a Getter

dart
class Shape {
  String get name => 'Shape';
}

class Circle extends Shape {
  @override
  String get name => 'Circle';
}

void main() {
  Shape s = Circle();
  print(s.name);
}
output
Circle

Example 4 — toString() in Interpolation

dart
class Book {
  final String title;
  final int pages;
  Book(this.title, this.pages);

  @override
  String toString() => '$title ($pages pages)';
}

void main() {
  var book = Book('Dart Basics', 120);
  print('Reading: $book');
}
output
Reading: Dart Basics (120 pages)

Example 5 — == by Value

dart
class Colour {
  final String name;
  Colour(this.name);

  @override
  bool operator ==(Object other) => other is Colour && name == other.name;

  @override
  int get hashCode => name.hashCode;
}

void main() {
  print(Colour('red') == Colour('red'));
  print(Colour('red') == Colour('blue'));
}
output
true
false

With only one field, that field's own hashCode is enough — no Object.hash needed.

Example 6 — Equal Objects in a Set

dart
class Point {
  final int x;
  final int y;
  Point(this.x, this.y);

  @override
  bool operator ==(Object other) =>
      other is Point && x == other.x && y == other.y;

  @override
  int get hashCode => Object.hash(x, y);
}

void main() {
  var visited = {Point(0, 0), Point(1, 2), Point(0, 0)};
  print(visited.length);
  print(visited.contains(Point(1, 2)));
}
output
2
true

Example 7 — Student Marks

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);

  String get grade => marks >= 80 ? 'A' : 'B';

  @override
  String toString() => '$name: $marks ($grade)';
}

class HonoursStudent extends Student {
  HonoursStudent(super.name, super.marks);

  @override
  String get grade => marks >= 90 ? 'A+' : 'A';
}

void main() {
  print(Student('Ali', 85));
  print(HonoursStudent('Sara', 95));
}
output
Ali: 85 (A)
Sara: 95 (A+)

Example 8 — Shopping Total

dart
class Item {
  final String name;
  final int price;
  Item(this.name, this.price);

  int get total => price;
}

class BulkItem extends Item {
  final int quantity;
  BulkItem(super.name, super.price, this.quantity);

  @override
  int get total => price * quantity;
}

void main() {
  var cart = [Item('Notebook', 200), BulkItem('Pencil', 30, 10)];
  var sum = 0;
  for (var item in cart) {
    print('${item.name}: ${item.total}');
    sum += item.total;
  }
  print('Total: $sum');
}
output
Notebook: 200
Pencil: 300
Total: 500

Example 9 — Bank Balance

dart
class Account {
  final String owner;
  int balance;
  Account(this.owner, this.balance);

  int get fee => 20;

  void withdraw(int amount) {
    balance -= amount + fee;
  }

  @override
  String toString() => '$owner: Rs $balance';
}

class PremiumAccount extends Account {
  PremiumAccount(super.owner, super.balance);

  @override
  int get fee => 0;
}

void main() {
  var ali = Account('Ali', 1000);
  var sara = PremiumAccount('Sara', 1000);
  ali.withdraw(300);
  sara.withdraw(300);
  print(ali);
  print(sara);
}
output
Ali: Rs 680
Sara: Rs 700

Example 10 — Temperature Readings as Map Keys

dart
class City {
  final String name;
  final String country;
  City(this.name, this.country);

  @override
  bool operator ==(Object other) =>
      other is City && name == other.name && country == other.country;

  @override
  int get hashCode => Object.hash(name, country);

  @override
  String toString() => '$name, $country';
}

void main() {
  var temperatures = {City('Lahore', 'PK'): 34.5};
  temperatures[City('Karachi', 'PK')] = 31.0;
  temperatures[City('Lahore', 'PK')] = 36.0;

  for (var entry in temperatures.entries) {
    print('${entry.key}: ${entry.value}°C');
  }
}
output
Lahore, PK: 36.0°C
Karachi, PK: 31.0°C

The second Lahore equals the first key, so it replaces the value.


Common Beginner Mistakes

1. A typo with no @override

A misspelled name silently adds a new method and the parent's still runs (Section 2).

2. Giving == a parameter of your own type

dart
class Point {
  final int x;
  Point(this.x);

  // @override
  // bool operator ==(Point other) => x == other.x; // ✗
}
output
Error: The parameter 'other' of the method 'Point.==' has type 'Point', which does not match
the corresponding type, 'Object', in the overridden method, 'Object.=='.

The message also mentions a covariant keyword. You don't need it: take Object and check with is.

3. Overriding == without hashCode

Comparisons look right, but sets keep duplicates and map lookups return null (Section 7).

4. Using different fields in == and hashCode

Equal objects can then get different hash codes. Use the same fields in both.


Best Practices

  • Write @override on every override — methods, getters, toString, ==, hashCode.
  • Give classes you print a toString() override.
  • Override == and hashCode as a pair, from the same final fields.
  • Build hashCode with Object.hash(...).
  • Start == with an is check on an Object parameter.
  • Change only the body of an override, never its signature.

Mini Challenge

Create a class Temperature with a final double celsius. Override toString() to print 25.0°C, and override == and hashCode so equal readings count once. Put 25.0, 30.5 and 25.0 in a set and print the set and its length.

Your program should print exactly:

output
{25.0°C, 30.5°C}
2

Try it yourself before opening the solution.

Show solution
dart
class Temperature {
  final double celsius;
  Temperature(this.celsius);

  @override
  String toString() => '$celsius°C';

  @override
  bool operator ==(Object other) =>
      other is Temperature && celsius == other.celsius;

  @override
  int get hashCode => celsius.hashCode;
}

void main() {
  var readings = {Temperature(25.0), Temperature(30.5), Temperature(25.0)};
  print(readings);
  print(readings.length);
}
output
{25.0°C, 30.5°C}
2

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Class Bird has void fly() printing Flying. Create Penguin extends Bird that overrides fly() to print Penguins can't fly. Call fly() on both.
  2. Give class Car (fields brand, year) a toString() so print(Car('Honda', 2020)) shows Honda (2020).
  3. Class Employee has String get role => 'Employee'. Create Manager overriding it to return 'Manager'. Print the role of each.
  4. Without running it: what does print(Box(5) == Box(5)); print if Box does not override ==? Why?

Application

  1. Give class Book (field isbn) an == and hashCode based on isbn. Print Book('111') == Book('111'), then the length of the set {Book('111'), Book('222'), Book('111')}.
  2. Without running it: a class's == compares name and roll, but its hashCode is Object.hash(name, marks). What can go wrong when its objects go into a set?
  3. Class Ticket has int get price => 500. ChildTicket overrides it to 250. Store [Ticket(), ChildTicket(), ChildTicket()] and print the total price.
  4. Class Shape has double get area => 0 and toString() returning 'Area: $area'. Square (field side) overrides only area. Print Square(3).

Challenge

  1. Class Contact (name, phone) is equal when the phone matches, whatever the name. Override ==, hashCode and toString() (name: phone). Build a map {Contact('Ali', '0300'): 'friend'}, then look up Contact('Ali Khan', '0300').
  2. Class Student (name, marks) has bool get passed => marks >= 50. StrictStudent overrides it to require 70. Print passed for Student('Ali', 60) and StrictStudent('Sara', 60), both stored in a List<Student>.

Practice Answers

Question 1 — Solution

dart
class Bird {
  void fly() {
    print('Flying');
  }
}

class Penguin extends Bird {
  @override
  void fly() {
    print("Penguins can't fly");
  }
}

void main() {
  Bird().fly();
  Penguin().fly();
}
output
Flying
Penguins can't fly

Question 2 — Solution

dart
class Car {
  final String brand;
  final int year;
  Car(this.brand, this.year);

  @override
  String toString() => '$brand ($year)';
}

void main() {
  print(Car('Honda', 2020));
}
output
Honda (2020)

Question 3 — Solution

dart
class Employee {
  String get role => 'Employee';
}

class Manager extends Employee {
  @override
  String get role => 'Manager';
}

void main() {
  print(Employee().role);
  print(Manager().role);
}
output
Employee
Manager

Question 4 — Solution

It prints false. Without an override, == comes from Object and checks whether both sides are the same object. Box(5) twice creates two separate objects.

Question 5 — Solution

dart
class Book {
  final String isbn;
  Book(this.isbn);

  @override
  bool operator ==(Object other) => other is Book && isbn == other.isbn;

  @override
  int get hashCode => isbn.hashCode;
}

void main() {
  print(Book('111') == Book('111'));
  print({Book('111'), Book('222'), Book('111')}.length);
}
output
true
2

Question 6 — Solution

Two students with the same name and roll but different marks are ==, yet get different hash codes. The set looks in the wrong place and can keep both. hashCode must use the same fields as ==.

Question 7 — Solution

dart
class Ticket {
  int get price => 500;
}

class ChildTicket extends Ticket {
  @override
  int get price => 250;
}

void main() {
  var tickets = [Ticket(), ChildTicket(), ChildTicket()];
  var total = 0;
  for (var t in tickets) {
    total += t.price;
  }
  print(total);
}
output
1000

Question 8 — Solution

dart
class Shape {
  double get area => 0;

  @override
  String toString() => 'Area: $area';
}

class Square extends Shape {
  final double side;
  Square(this.side);

  @override
  double get area => side * side;
}

void main() {
  print(Square(3));
}
output
Area: 9.0

Question 9 — Solution

dart
class Contact {
  final String name;
  final String phone;
  Contact(this.name, this.phone);

  @override
  bool operator ==(Object other) => other is Contact && phone == other.phone;

  @override
  int get hashCode => phone.hashCode;

  @override
  String toString() => '$name: $phone';
}

void main() {
  var groups = {Contact('Ali', '0300'): 'friend'};
  print(groups[Contact('Ali Khan', '0300')]);
}
output
friend

Question 10 — Solution

dart
class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);

  bool get passed => marks >= 50;
}

class StrictStudent extends Student {
  StrictStudent(super.name, super.marks);

  @override
  bool get passed => marks >= 70;
}

void main() {
  List<Student> students = [Student('Ali', 60), StrictStudent('Sara', 60)];
  for (var s in students) {
    print('${s.name}: ${s.passed}');
  }
}
output
Ali: true
Sara: false

Quick Revision

  1. What does overriding a method mean?
  2. What does @override add if the code runs without it?
  3. Does the variable's type or the object decide which version runs?
  4. What does print show for an object whose class has no toString() override?
  5. What does the inherited == compare?
  6. Why must hashCode be overridden together with ==?
  7. What type must the parameter of operator == have?

Key Takeaways

  • Overriding replaces an inherited method or getter in a child class; the parent keeps its own version.
  • Write @override every time — it turns a misspelled name into a warning.
  • The object decides which version runs, even through a parent-typed variable.
  • Override toString() so print and interpolation show useful text.
  • The inherited == checks identity; override it as bool operator ==(Object other) with an is check to compare values.
  • Sets and maps use hashCode first: equal objects must have equal hash codes. Override both, from the same fields, using Object.hash.
  • An override keeps the parent's parameters and return type.

What's Next?

Lecture 61 — super. Overriding replaces a parent's method completely. Often you want to keep the parent's work and add a little more. Next we will use super to call the parent's methods, getters and constructors from inside the child.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.