Dart Inheritance and Polymorphism

Inheritance is one of the three major features of object-oriented programming. It allows a class to be extended based on another class.

This chapter introduces Dart's extends single inheritance, method overriding, the super keyword, and abstract classes.


extends single inheritance

Dart only supports single inheritance—each class can have only one direct parent class.

But the inheritance chain can be long: a subclass inherits from a parent class, the parent class inherits from a grandparent class, and so on.

Example

// Parent class (base class, superclass)
class Animal {
  String name;

  Animal(this.name);

  void eat() {
    print('$name is eating');
  }

  void sleep() {
    print('$name is sleeping');
  }
}

// Subclass: uses extends to inherit Animal
class Dog extends Animal {
  String breed;  // Breed

  // Subclass constructor needs to call the parent class constructor
  Dog(String name, this.breed) : super(name);

  // Methods newly added by the subclass
  void bark() {
    print('$name ($breed) is barking!');
  }
}

void main() {
  var dog = Dog('Wangcai', 'Golden Retriever');

  // Call a method inherited from the parent class
  dog.eat();
  dog.sleep();

  // Call the subclass's own method
  dog.bark();

  // A subclass object is also of the parent class type
  Animal animal = dog;  // Dog is an Animal, so it can be upcast
  animal.eat();
}
Wangcai is eating.
Wangcai is sleeping.
Wangcai (Golden Retriever) is barking!
Wangcai is eating.

A subclass can access all non-private member variables and methods in the parent class.

Private members (starting with an underscore) are not inherited.

Dart has no public, protected, or private keywords. Members starting with an underscore (_) are library-private and can only be accessed within the same file. This is different from Java/C++ access control.


Method Overriding @override

A subclass can override the parent class's methods to provide its own implementation.

Using the @override annotation clearly expresses the intent to override, and the compiler will help you check whether the override is correct.

Example

class Animal {
  String name;
  Animal(this.name);

  // Parent class method
  void makeSound() {
    print('$name made some sounds');
  }

  @override
  String toString() {
    return 'Animal: $name';
  }
}

class Cat extends Animal {
  Cat(String name) : super(name);

  // @override annotation: tells the compiler I am overriding a parent class method
  @override
  void makeSound() {
    print('$name meows~');
  }
}

class Duck extends Animal {
  Duck(String name) : super(name);

  @override
  void makeSound() {
    print('$name quacks!');
  }
}

void main() {
  var cat = Cat('Xiaohua');
  var duck = Duck('Donald Duck');

  cat.makeSound();
  duck.makeSound();

  // Polymorphism: the same method behaves differently for different objects
  List<Animal> animals = [
    Cat('Mimi'),
    Duck('Yaya'),
    Cat('Qiuqiu'),
  ];

  print('--- EXAMPLE Zoo ---');
  for (var animal in animals) {
    animal.makeSound();  // At runtime, the method of the actual type is called
  }
}
小花 喵喵叫~
唐老鸭 嘎嘎叫!
--- EXAMPLE 动物园 ---
咪咪 喵喵叫~
鸭鸭 嘎嘎叫!
球球 喵喵叫~

The example above demonstratesPolymorphism: For the same method call animal.makeSound(), the actual behavior executed depends on the real type of the object.

Using the @override annotation has two benefits: first, it makes the code intent clearer; second, if the parent class does not have a method with the same name (e.g., a spelling mistake), the compiler will report an error. It is recommended to always add @override.


super keyword

super represents the parent class object. A subclass uses super to call the parent class's constructor, methods, and properties.

Example

class Vehicle {
  String brand;
  int year;

  Vehicle(this.brand, this.year);

  void start() {
    print('$brand vehicle started');
  }

  void displayInfo() {
    print('Brand: $brand, Year: $year');
  }
}

class ElectricCar extends Vehicle {
  int batteryLevel;

  ElectricCar(String brand, int year, this.batteryLevel)
      : super(brand, year);  // Call the parent class constructor

  @override
  void start() {
    // Call a parent class method from a subclass method
    super.start();  // Execute the parent class startup logic first
    print('Battery level: $batteryLevel%');
    print('Electric motor started silently');
  }

  @override
  void displayInfo() {
    super.displayInfo();  // First display the basic information of the parent class
    print('Battery level: $batteryLevel%');  // Then supplement the subclass's specific information
  }
}

void main() {
  var car = ElectricCar('EXAMPLE EV', 2026, 85);
  car.start();
  print('---');
  car.displayInfo();
}
EXAMPLE EV 车辆启动了
电池电量: 85%
电动机无声启动完成
---
品牌: EXAMPLE EV, 年份: 2026
电池电量: 85%

Summary of super Usage Scenarios

ScenariosSyntaxDescription
Calling the Parent Class Constructor: super(args)Must be called in the initializer list
Calling parent class methodssuper.methodName()Can be called in overridden methods in the subclass
Accessing parent class propertiessuper.propertyNameAccessing member variables of the parent class

Abstract class abstract

An abstract class is a class that cannot be directly instantiated; it only defines the interface specification, with concrete logic implemented by subclasses.

Example

// The abstract keyword declares an abstract class
abstract class Shape {
  // Abstract method: no method body, subclasses must implement it
  double getArea();
  double getPerimeter();

  // Abstract classes can also have concrete methods
  void describe() {
    print('This is a shape, area: ${getArea()}, perimeter: ${getPerimeter()}');
  }
}

class Rectangle extends Shape {
  double width;
  double height;

  Rectangle(this.width, this.height);

  @override
  double getArea() => width * height;

  @override
  double getPerimeter() => 2 * (width + height);
}

class Circle extends Shape {
  double radius;

  Circle(this.radius);

  @override
  double getArea() => 3.14159 * radius * radius;

  @override
  double getPerimeter() => 2 * 3.14159 * radius;
}

void main() {
  // Shape shape = Shape(); // Error! Abstract classes cannot be instantiated

  var rect = Rectangle(10, 5);
  var circle = Circle(7);

  rect.describe();
  circle.describe();

  // Using an abstract class as a type
  List<Shape> shapes = [
    Rectangle(3, 4),
    Circle(5),
    Rectangle(6, 2),
  ];

  print('--- EXAMPLE Shape Statistics ---');
  double totalArea = 0;
  for (var shape in shapes) {
    totalArea += shape.getArea();
  }
  print('Total area: ${totalArea.toStringAsFixed(2)}');
}
这是一个形状,面积: 50.0, 周长: 30.0
这是一个形状,面积: 153.93791, 周长: 43.98226
--- EXAMPLE 形状统计 ---
总面积: 108.54

The core value of an abstract class is "defining a contract." It tells all subclasses: you must implement these methods, otherwise compilation fails. This makes team collaboration safer—implementers won't miss methods, and callers are confident that the methods definitely exist.

other extensions