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.

Table of Contents

  1. Key Concepts
    1. The Problem with Concrete Parent Classes
    2. What is an Abstract Class?
    3. Abstract Methods
    4. Rules for Abstract Classes
    5. Abstract Classes and Polymorphism
    6. The Racer Example Revisited
  2. Worked Examples
    1. Example 1: Shape Hierarchy
    2. Example 2: Game Character Actions
  3. Quick Check
  4. Trace Exercise
  5. Spot the Error
  6. Predict the Output
  7. Practice Exercises
    1. Core
    2. Extension
    3. Challenge
  8. Connections

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
    }
}

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();
}

Now Animal is abstract. Two things change:

Animal a = new Animal("Generic", 1);  // COMPILE ERROR -- cannot instantiate
Dog d = new Dog("Max", 3, "Labrador");  // OK -- Dog is concrete
Animal 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 { }

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()
    }
}

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

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

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());
}

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();
}
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
    }
}

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;
    }
}
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);

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";
    }
}
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");
}

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 IB Computer Science

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