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:
- Nothing stops someone from writing
new Animal("Generic", 1): a generic animal that says “…” is meaningless. There is no real animal that just says “…”. - 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:
Shapeis abstract: you cannot writenew Shape("Red")describe()is concrete and calls the abstractgetArea(): 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);| Index | Actual type | getArea() (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.
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
-
Make Animal abstract: Take the
Animalclass from the Inheritance page and make it abstract. Makespeak()an abstract method. Verify thatnew Animal(...)no longer compiles butnew Dog(...)andnew Cat(...)still work. -
Instrument hierarchy: Create an abstract
Instrumentclass with attributesnameandtype(e.g. “string”, “percussion”), and an abstract methodplay()that returns a String. Create concrete subclassesGuitar(returns “Strum!”) andDrum(returns “Boom!”). Store them in anInstrument[]array and print each instrument’s name and sound.
Extension
-
Payment system: Create an abstract
PaymentMethodclass with an abstract methodprocessPayment(double amount)that returns a boolean. Create concrete subclassesCreditCard(always succeeds) andCashPayment(fails if amount > balance). Test with a polymorphic array of payment methods. -
Abstract chain: Create an abstract class
Awith abstract methodgetValue(). Create an abstract classB extends Athat adds a new abstract methodgetLabel(). Create a concrete classC extends Bthat implements both. Explain whyBdoes not need to implementgetValue().
Challenge
- Tournament system: Extend the abstract
Racerclass to support three racer types:Hare(jumps 5 with 30% probability),Tortoise(always 1 step), and a newCheetah(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