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
- Key Concepts
- Worked Examples
- Quick Check
- Trace Exercise
- Spot the Error
- Predict the Output
- Practice Exercises
- 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:
- 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();
}
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:
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";
}
}
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);| 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