Inheritance and Overriding
What Is Inheritance, Really?
Imagine you are describing animals to a friend. You could say: a dog has four legs, breathes air, and barks. A cat has four legs, breathes air, and meows. Notice how much you repeated. Inheritance is a way to avoid that repetition in code. You describe what is shared once, and then only what is different for each specific kind.
In Dart, a class is a blueprint for objects — things that hold data and do work. Inheritance means one class (the child) gets the fields and methods of another class (the parent) for free. The child can then add its own, or change the ones it inherited.
Why does this matter? Because real programs are full of things that share most of their behavior. Without inheritance, you would copy the shared parts into many places. When something changes, you would have to fix it everywhere. Inheritance lets you fix it once.
A First Example
Let's model vehicles. Every vehicle can describe itself. A car adds doors. A bicycle adds a bell.
class Vehicle {
final String name;
Vehicle(this.name);
String describe() => '$name is a vehicle.';
}
class Car extends Vehicle {
final int doors;
Car(String name, this.doors) : super(name);
@override
String describe() => '$name is a car with $doors doors.';
}
void main() {
final car = Car('Bessie', 4);
print(car.describe()); // Bessie is a car with 4 doors.
}Three new things are happening here:
extends VehiclemakesCara child ofVehicle. Every car is a vehicle.super(name)passes the name up to the parent's constructor. A child must let the parent set up its part of the object.@overridemarks thatCarreplacesdescribe()with its own version. This marker is optional in Dart but strongly recommended — the compiler will warn you if your spelling does not actually match a real method.
Overriding: Changing What You Inherited
Overriding is when a child supplies its own version of a method the parent already has. The child's version wins when you call the method on a child object.
You can also call the parent's version from inside the override using super.methodName(). This is handy when you want to add to the parent's behavior rather than replace it.
class Bicycle extends Vehicle {
Bicycle(String name) : super(name);
@override
String describe() {
final base = super.describe(); // run the parent's version
return '$base Ring ring!';
}
}
void main() {
print(Bicycle('Pegasus').describe());
// Pegasus is a vehicle. Ring ring!
}What You Can and Cannot Override
Dart has firm rules here, and knowing them prevents real bugs:
- You cannot override a method with the wrong types. If the parent returns
String, you must returnString(or something compatible). - You cannot override a field declared
final— it can only be set once, in the constructor. - You can override a getter even if the parent has a field, because in Dart a field and a getter with the same name are the same thing as far as callers are concerned.
- Private members (names starting with
_) belong to the file where they are written. A child class in a different file cannot see or override them.
The Substitution Idea
Here is the mental model that makes inheritance click: wherever the parent type is expected, a child object can stand in. A function asking for a Vehicle will happily accept a Car or a Bicycle, because both are vehicles.
void printDescription(Vehicle v) {
print(v.describe());
}
void main() {
printDescription(Car('Bessie', 4));
printDescription(Bicycle('Pegasus'));
}When printDescription calls v.describe(), Dart looks at the actual object at runtime, not the declared type. This is called dynamic dispatch, and it is what makes overriding useful. The variable is typed as Vehicle, but the car's version of describe runs.
Adding to the Chain: Grandchildren
Inheritance can go deeper than one level. A SportsCar can extend Car, which extends Vehicle. Each level inherits from everything above it and may override again.
Show diagram source
classDiagram
Vehicle <|-- Car
Car <|-- SportsCar
Vehicle : +String name
Vehicle : +describe() String
Car : +int doors
Car : +describe() String
SportsCar : +int topSpeed
SportsCar : +describe() StringEach class down the chain must call its parent's constructor with super(...). Forgetting this is one of the most common beginner errors in Dart.
When Not to Inherit
Inheritance is powerful but easy to overuse. Two warnings worth remembering:
- Inheritance is for "is-a" relationships. A car is a vehicle, so extending is right. A car has an engine — that is a part, not a kind — so the engine should be a separate object stored in a field, not a parent class.
- Deep chains get fragile. If you change the middle of a chain, every class below it changes too. In later chapters you will meet mixins and abstract classes, which offer safer ways to share behavior.
A good rule of thumb: prefer sharing through composition (holding an object in a field) unless the "is-a" relationship is obvious.
Pulling It Together
extendsmakes one class a child of another.- The child inherits all visible fields and methods.
super(...)in the constructor passes work to the parent.@overridelets the child replace a method, andsuper.method()lets it extend it.- A child object can be used anywhere the parent type is expected.
- Dart picks the child's method at runtime — that is the point of it all.
Exercise
If SportsCar extends Car and you forget to write super(...) in its constructor, what happens?
Show answer
The program will not compile. Every subclass constructor must eventually call its parent's constructor, and super(...) is how you do it. Dart reports an error telling you the superclass constructor was never invoked.
Exercise
A function takes a Vehicle parameter and calls v.describe(). You pass it a Bicycle. Which describe runs, and why?
Show answer
Bicycle's version. Dart uses dynamic dispatch: it looks at the actual object at runtime, not the declared parameter type, so the child's override wins.
Spotted a mistake, or code that does not work?