Abstract Classes

IB Syllabus: B3.2 (HL): Use abstract classes to define incomplete base types that subclasses must complete.

HL Only: This page covers content assessed at HL level only.


Key Concepts

The Problem with Concrete Parent Classes

In the Inheritance and Polymorphism pages, the Animal class has a speak() method that returns "...":

public class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    public String speak() {
        return "...";  // meaningless default
    }
}
class Animal():
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def get_name(self):
        return self._name

    def get_age(self):
        return self._age

    def speak(self):
        return "..."    # meaningless default

This works, but it has two problems:

  1. Nothing stops someone from writing new Animal("Generic", 1): a generic animal that says “…” is meaningless. There is no real animal that just says “…”.
  2. A subclass can forget to override speak() and silently inherit the useless default. The program compiles and runs, but produces wrong output.

An abstract class solves both problems at once: it prevents direct instantiation of the parent AND forces every subclass to provide its own version of key methods.

What is an Abstract Class?

An abstract class is a class declared with the abstract keyword. It cannot be instantiated directly with new. It exists only to be extended by subclasses.

public abstract class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    // Abstract method -- no body, just a declaration
    public abstract String speak();
}
# Python has no abstract keyword - raising an error if a child forgets
# to override is the closest simple equivalent
class Animal():
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def get_name(self):
        return self._name

    def get_age(self):
        return self._age

    def speak(self):
        raise NotImplementedError    # "abstract": every child MUST override this

Now Animal is abstract. Two things change:

Animal a = new Animal("Generic", 1);  // COMPILE ERROR -- cannot instantiate
a = Animal("Generic", 1)    # Python ALLOWS this - there is no abstract keyword to stop it
a.speak()                   # but the mistake surfaces here: NotImplementedError at runtime
Dog d = new Dog("Max", 3, "Labrador");  // OK -- Dog is concrete
Animal a = d;                            // OK -- polymorphism still works
d = Dog("Max", 3, "Labrador")    # OK -- Dog provides a real speak()
a = d                            # OK -- polymorphism still works

The abstract class can still be used as a type for variables, arrays, and parameters. You just cannot create an instance of it directly.

Abstract Methods

An abstract method has no body, just a signature followed by a semicolon:

public abstract String speak();   // abstract -- no { }
def speak(self):
    raise NotImplementedError    # the only "body" is an error - forces children to override

Any class that extends Animal must override speak() and provide a body. If it does not, the compiler reports an error. This is how abstract classes force subclasses to provide meaningful behaviour.

public class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);
        this.breed = breed;
    }

    public String getBreed() { return breed; }

    @Override
    public String speak() {
        return "Woof!";  // required -- Dog MUST implement speak()
    }
}
class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)    # call the parent constructor
        self._breed = breed

    def get_breed(self):
        return self._breed

    def speak(self):
        return "Woof!"    # overriding replaces the error-raising version

If you removed the speak() method from Dog, you would get:

Dog is not abstract and does not override abstract method speak() in Animal

(Java catches this at compile time. In Python the same mistake only appears at runtime, as a NotImplementedError the first time speak() is called.)

Rules for Abstract Classes

Rule Example
Declared with abstract keyword public abstract class Animal { }
Cannot be instantiated with new new Animal(...) is a compile error
Can have constructors Subclasses call them via super(...)
Can have concrete (normal) methods getName(), getAge() work as usual
Can have abstract methods public abstract String speak();
If a class has any abstract method, the class must be declared abstract Cannot have an abstract method in a concrete class
A concrete subclass must implement ALL abstract methods Or it must also be declared abstract
Can be used as a type Animal[] zoo = new Animal[5]; is valid

Abstract classes can mix concrete and abstract methods. The concrete methods provide shared behaviour that all subclasses inherit. The abstract methods define the “contract” that each subclass must fulfil.

Abstract Classes and Polymorphism

Abstract classes work perfectly with polymorphism. You can still create polymorphic arrays and loops:

Animal[] zoo = new Animal[3];
zoo[0] = new Dog("Max", 3, "Labrador");
zoo[1] = new Cat("Whiskers", 5);
zoo[2] = new Dog("Buddy", 2, "Beagle");

for (int i = 0; i < zoo.length; i++) {
    System.out.println(zoo[i].getName() + " says " + zoo[i].speak());
}
zoo = []
zoo.append(Dog("Max", 3, "Labrador"))
zoo.append(Cat("Whiskers", 5))
zoo.append(Dog("Buddy", 2, "Beagle"))

for i in range(len(zoo)):
    print(zoo[i].get_name(), "says", zoo[i].speak())

The difference from before: you can never accidentally add new Animal(...) to the array. Every element must be a concrete subclass that has a real speak() implementation. This makes your code safer.

The Racer Example Revisited

Recall the Racer hierarchy from Inheritance. The Racer class has a default move() that advances 1 step. But a generic Racer object has no meaning in the race: only Hare and Tortoise make sense.

Making Racer abstract and move() abstract forces every racer type to define its own movement strategy:

public abstract class Racer {
    private String name;
    private int position;

    public Racer(String name) {
        this.name = name;
        this.position = 0;
    }

    public String getName() { return name; }
    public int getPosition() { return position; }

    // Concrete helper -- subclasses can call this to advance
    protected void advance(int steps) {
        position = position + steps;
    }

    // Abstract -- each racer type defines HOW it moves
    public abstract void move();
}
class Racer():
    def __init__(self, name):
        self._name = name
        self._position = 0

    def get_name(self):
        return self._name

    def get_position(self):
        return self._position

    # Concrete helper -- subclasses call this to advance
    def advance(self, steps):
        self._position = self._position + steps

    # "Abstract" -- each racer type defines HOW it moves
    def move(self):
        raise NotImplementedError    # Python's closest simple stand-in for abstract
public class Hare extends Racer {
    public Hare(String name) {
        super(name);
    }

    @Override
    public void move() {
        if (Math.random() < 0.3) {
            advance(5);  // big jump
        }
        // else: sleep (do nothing)
    }
}

public class Tortoise extends Racer {
    public Tortoise(String name) {
        super(name);
    }

    @Override
    public void move() {
        advance(1);  // slow and steady
    }
}
import random

class Hare(Racer):
    def __init__(self, name):
        super().__init__(name)    # call the parent constructor

    def move(self):
        if random.random() < 0.3:
            self.advance(5)    # big jump
        # else: sleep (do nothing)

class Tortoise(Racer):
    def __init__(self, name):
        super().__init__(name)

    def move(self):
        self.advance(1)    # slow and steady

Notice the advance() helper method is protected and concrete: all racers share the same mechanism for updating position. Only the strategy (how far and when to move) is abstract. This is a common pattern: abstract classes provide shared infrastructure while leaving specific decisions to subclasses.


Worked Examples

Example 1: Shape Hierarchy

A drawing application needs different shapes, each with its own area calculation.

public abstract class Shape {
    private String colour;

    public Shape(String colour) {
        this.colour = colour;
    }

    public String getColour() { return colour; }

    // Every shape must calculate its own area
    public abstract double getArea();

    // Concrete method shared by all shapes
    public String describe() {
        return colour + " shape with area " + getArea();
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(String colour, double radius) {
        super(colour);
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(String colour, double width, double height) {
        super(colour);
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }
}
import math

class Shape():
    def __init__(self, colour):
        self._colour = colour

    def get_colour(self):
        return self._colour

    # Every shape must calculate its own area
    def get_area(self):
        raise NotImplementedError    # "abstract" - Python has no abstract keyword

    # Concrete method shared by all shapes
    def describe(self):
        return self._colour + " shape with area " + str(self.get_area())

class Circle(Shape):
    def __init__(self, colour, radius):
        super().__init__(colour)    # call the parent constructor
        self._radius = radius

    def get_area(self):
        return math.pi * self._radius * self._radius

class Rectangle(Shape):
    def __init__(self, colour, width, height):
        super().__init__(colour)
        self._width = width
        self._height = height

    def get_area(self):
        return self._width * self._height
Shape[] shapes = new Shape[3];
shapes[0] = new Circle("Red", 5);
shapes[1] = new Rectangle("Blue", 4, 6);
shapes[2] = new Circle("Green", 3);

double totalArea = 0;
for (int i = 0; i < shapes.length; i++) {
    System.out.println(shapes[i].describe());
    totalArea = totalArea + shapes[i].getArea();
}
System.out.println("Total area: " + totalArea);
shapes = []
shapes.append(Circle("Red", 5))
shapes.append(Rectangle("Blue", 4.0, 6.0))    # .0 mirrors Java's double, so 24.0 prints
shapes.append(Circle("Green", 3))

total_area = 0
for i in range(len(shapes)):
    print(shapes[i].describe())
    total_area = total_area + shapes[i].get_area()
print("Total area:", total_area)

Output:

Red shape with area 78.53981633974483
Blue shape with area 24.0
Green shape with area 28.274333882308138
Total area: 130.81415022205297

Key observations:

  • Shape is abstract: you cannot write new Shape("Red")
  • describe() is concrete and calls the abstract getArea(): the correct version runs at runtime
  • The polymorphic array and loop work exactly as before

Example 2: Game Character Actions

A game has different character types, each with a unique attack.

public abstract class GameCharacter {
    private String name;
    private int health;

    public GameCharacter(String name, int health) {
        this.name = name;
        this.health = health;
    }

    public String getName() { return name; }
    public int getHealth() { return health; }

    public void takeDamage(int amount) {
        health = health - amount;
        if (health < 0) {
            health = 0;
        }
    }

    public boolean isAlive() {
        return health > 0;
    }

    // Each character type attacks differently
    public abstract int attack();
    public abstract String getAttackName();
}

public class Warrior extends GameCharacter {
    public Warrior(String name) {
        super(name, 100);
    }

    @Override
    public int attack() {
        return 15;  // heavy consistent damage
    }

    @Override
    public String getAttackName() {
        return "Sword Strike";
    }
}

public class Mage extends GameCharacter {
    public Mage(String name) {
        super(name, 70);
    }

    @Override
    public int attack() {
        // 40% chance of critical spell (30 damage), otherwise 10
        if (Math.random() < 0.4) {
            return 30;
        }
        return 10;
    }

    @Override
    public String getAttackName() {
        return "Fireball";
    }
}
import random

class GameCharacter():
    def __init__(self, name, health):
        self._name = name
        self._health = health

    def get_name(self):
        return self._name

    def get_health(self):
        return self._health

    def take_damage(self, amount):
        self._health = self._health - amount
        if self._health < 0:
            self._health = 0

    def is_alive(self):
        return self._health > 0

    # Each character type attacks differently ("abstract" - children must override)
    def attack(self):
        raise NotImplementedError

    def get_attack_name(self):
        raise NotImplementedError

class Warrior(GameCharacter):
    def __init__(self, name):
        super().__init__(name, 100)    # call the parent constructor

    def attack(self):
        return 15    # heavy consistent damage

    def get_attack_name(self):
        return "Sword Strike"

class Mage(GameCharacter):
    def __init__(self, name):
        super().__init__(name, 70)

    def attack(self):
        # 40% chance of critical spell (30 damage), otherwise 10
        if random.random() < 0.4:
            return 30
        return 10

    def get_attack_name(self):
        return "Fireball"
GameCharacter[] team = new GameCharacter[2];
team[0] = new Warrior("Aria");
team[1] = new Mage("Zeph");

for (int i = 0; i < team.length; i++) {
    int damage = team[i].attack();
    System.out.println(team[i].getName() + " uses " +
        team[i].getAttackName() + " for " + damage + " damage");
}
team = []
team.append(Warrior("Aria"))
team.append(Mage("Zeph"))

for i in range(len(team)):
    damage = team[i].attack()
    print(team[i].get_name(), "uses", team[i].get_attack_name(), "for", damage, "damage")

Output (example run):

Aria uses Sword Strike for 15 damage
Zeph uses Fireball for 30 damage

Notice GameCharacter has two abstract methods (attack() and getAttackName()). Both Warrior and Mage must implement both. The concrete methods takeDamage(), isAlive(), getName(), and getHealth() are shared by all characters.


Quick Check

Q1. What happens when you write new Animal("Rex", 3) if Animal is abstract?

Q2. A class has one abstract method and three concrete methods. The class must be:

Q3. Shape is abstract with two abstract methods: getArea() and getPerimeter(). Circle extends Shape and only implements getArea(). What happens?

Q4. Can an abstract class have a constructor?

Q5. Animal is abstract. Is Animal[] zoo = new Animal[5]; valid?


Trace Exercise

Trace the method calls through the Shape hierarchy. Shape is abstract with abstract getArea().

Trace: Abstract Polymorphism

// Circle: getArea() returns Math.PI * r * r
// Rectangle: getArea() returns w * h
Shape[] shapes = new Shape[3];
shapes[0] = new Circle("Red", 5);
shapes[1] = new Rectangle("Blue", 4, 6);
shapes[2] = new Circle("Green", 3);
IndexActual typegetArea() (rounded)
0
1
2

Total area (rounded):

Circle area: PI * r^2. For r=5: 3.14159 * 25 = 78.54. For r=3: 3.14159 * 9 = 28.27.


Spot the Error

This code has a bug. The Vehicle class is intended to be abstract, but the programmer made a mistake.

Bug Hunt: A vehicle hierarchy where every vehicle type must report its fuel efficiency. Find and fix the error.

1public class Vehicle { 2 private String model; 3 4 public Vehicle(String model) { 5 this.model = model; 6 } 7 8 public String getModel() { return model; } 9 10 public abstract double getFuelEfficiency(); 11}

Pick the correct fix for line 1:


Predict the Output

The abstract Shape class has describe() that returns colour + " shape with area " + getArea(). What does this print?

Shape s1 = new Circle("Red", 4);
Shape s2 = new Rectangle("Blue", 3, 7);
System.out.println(s2.describe());

Practice Exercises

Core

  1. Make Animal abstract: Take the Animal class from the Inheritance page and make it abstract. Make speak() an abstract method. Verify that new Animal(...) no longer compiles but new Dog(...) and new Cat(...) still work.

  2. Instrument hierarchy: Create an abstract Instrument class with attributes name and type (e.g. “string”, “percussion”), and an abstract method play() that returns a String. Create concrete subclasses Guitar (returns “Strum!”) and Drum (returns “Boom!”). Store them in an Instrument[] array and print each instrument’s name and sound.

Extension

  1. Payment system: Create an abstract PaymentMethod class with an abstract method processPayment(double amount) that returns a boolean. Create concrete subclasses CreditCard (always succeeds) and CashPayment (fails if amount > balance). Test with a polymorphic array of payment methods.

  2. Abstract chain: Create an abstract class A with abstract method getValue(). Create an abstract class B extends A that adds a new abstract method getLabel(). Create a concrete class C extends B that implements both. Explain why B does not need to implement getValue().

Challenge

  1. Tournament system: Extend the abstract Racer class to support three racer types: Hare (jumps 5 with 30% probability), Tortoise (always 1 step), and a new Cheetah (always 3 steps, but stops completely after position 80). Run a 100-round race with one of each type and print the final standings sorted by position.

Connections

  • Prerequisites: Inheritance: abstract classes build on the extends/super pattern
  • Prerequisites: Method Overriding: abstract methods force overriding
  • Prerequisites: Polymorphism: abstract types work as polymorphic variable types
  • Next: Interfaces: pure contracts with no state, multiple implementation
  • Related: What is OOP?: abstraction is one of the four OOP principles

© EduCS.me — A resource hub for Computer Science education

This site uses Just the Docs, a documentation theme for Jekyll.