Abstract Classes in Java

Interfaces define pure contracts with no implementation. Regular classes provide complete implementations. Abstract classes sit in between. They can define some methods completely while leaving others for subclasses to implement.

Use abstract classes when related classes share common code but also need class-specific behavior.

Defining an Abstract Class

The abstract keyword marks a class as abstract:

public abstract class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    // Concrete method - fully implemented
    public void sleep() {
        System.out.println(name + " is sleeping");
    }
    
    // Abstract method - no implementation
    public abstract void makeSound();
}

Animal has one concrete method (sleep) and one abstract method (makeSound). The abstract method has no body, just a signature ending with a semicolon.

Extending an Abstract Class

Subclasses must implement all abstract methods or be abstract themselves:

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " barks");
    }
}

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " meows");
    }
}

Both Dog and Cat inherit sleep() from Animal and provide their own makeSound().

Dog dog = new Dog("Rex");
dog.sleep();      // Rex is sleeping
dog.makeSound();  // Rex barks

Cat cat = new Cat("Whiskers");
cat.sleep();      // Whiskers is sleeping
cat.makeSound();  // Whiskers meows

You Cannot Instantiate Abstract Classes

Abstract classes cannot be instantiated directly:

// This won't compile
Animal animal = new Animal("Generic");  // Error!

An abstract class is incomplete. It may have abstract methods with no implementation. You must create instances of concrete subclasses instead.

However, you can use abstract class types for variables:

Animal pet = new Dog("Buddy");  // Valid - Dog is concrete
pet.makeSound();  // Buddy barks

This enables polymorphism just like with interfaces.

Abstract vs Concrete Methods

An abstract class can have any mix of abstract and concrete methods:

public abstract class Vehicle {
    protected String brand;
    protected int speed;
    
    public Vehicle(String brand) {
        this.brand = brand;
        this.speed = 0;
    }
    
    // Concrete methods
    public void accelerate(int amount) {
        speed += amount;
        System.out.println(brand + " accelerating to " + speed + " mph");
    }
    
    public void brake(int amount) {
        speed = Math.max(0, speed - amount);
        System.out.println(brand + " slowing to " + speed + " mph");
    }
    
    public int getSpeed() {
        return speed;
    }
    
    // Abstract methods
    public abstract void startEngine();
    public abstract int getWheelCount();
}
public class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }
    
    @Override
    public void startEngine() {
        System.out.println(brand + " car engine starting: vroom!");
    }
    
    @Override
    public int getWheelCount() {
        return 4;
    }
}

public class Motorcycle extends Vehicle {
    public Motorcycle(String brand) {
        super(brand);
    }
    
    @Override
    public void startEngine() {
        System.out.println(brand + " motorcycle engine starting: vrrrm!");
    }
    
    @Override
    public int getWheelCount() {
        return 2;
    }
}

Both share accelerate(), brake(), and getSpeed(). Each provides its own startEngine() and getWheelCount().

When to Use Abstract Classes

Use abstract classes when:

  • Related classes share common code (not just a contract)
  • You want to provide default implementations that subclasses can use or override
  • You need constructors or instance fields in the base type
  • You want to define non-public members (protected methods or fields)

Use interfaces when:

  • Unrelated classes need to share a capability
  • You want a class to adopt multiple contracts
  • You’re defining a pure API without implementation

Sometimes you use both. A class can extend an abstract class and implement interfaces:

public abstract class Animal {
    // Common animal code
}

public interface Trainable {
    void train(String command);
}

public class Dog extends Animal implements Trainable {
    @Override
    public void train(String command) {
        System.out.println("Learning: " + command);
    }
}

Abstract Classes with No Abstract Methods

An abstract class doesn’t require abstract methods. You might make a class abstract just to prevent instantiation:

public abstract class BaseController {
    protected void logRequest(String action) {
        System.out.println("Request: " + action);
    }
    
    protected void logResponse(String result) {
        System.out.println("Response: " + result);
    }
}

public class UserController extends BaseController {
    public void createUser(String name) {
        logRequest("Create user: " + name);
        // Create user logic
        logResponse("User created");
    }
}

BaseController provides utility methods but isn’t meant to be used directly. Making it abstract enforces this.

The Template Method Pattern

A powerful use of abstract classes is the template method pattern. The abstract class defines an algorithm’s structure while letting subclasses fill in specific steps:

public abstract class DataProcessor {
    // Template method - defines the algorithm
    public final void process() {
        readData();
        processData();
        writeData();
    }
    
    // Steps to be implemented by subclasses
    protected abstract void readData();
    protected abstract void processData();
    protected abstract void writeData();
}

public class CSVProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println("Reading CSV file");
    }
    
    @Override
    protected void processData() {
        System.out.println("Parsing CSV rows");
    }
    
    @Override
    protected void writeData() {
        System.out.println("Writing processed CSV");
    }
}

public class JSONProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println("Reading JSON file");
    }
    
    @Override
    protected void processData() {
        System.out.println("Parsing JSON objects");
    }
    
    @Override
    protected void writeData() {
        System.out.println("Writing processed JSON");
    }
}
DataProcessor csv = new CSVProcessor();
csv.process();
// Reading CSV file
// Parsing CSV rows
// Writing processed CSV

DataProcessor json = new JSONProcessor();
json.process();
// Reading JSON file
// Parsing JSON objects
// Writing processed JSON

The process() method is final so subclasses can’t change the order. They only fill in the specific steps.

Practical Example: Game Characters

public abstract class GameCharacter {
    protected String name;
    protected int health;
    protected int maxHealth;
    protected int level;
    
    public GameCharacter(String name, int maxHealth) {
        this.name = name;
        this.maxHealth = maxHealth;
        this.health = maxHealth;
        this.level = 1;
    }
    
    // Concrete methods shared by all characters
    public void takeDamage(int damage) {
        health = Math.max(0, health - damage);
        System.out.println(name + " takes " + damage + " damage. Health: " + health);
        if (health == 0) {
            System.out.println(name + " has been defeated!");
        }
    }
    
    public void heal(int amount) {
        health = Math.min(maxHealth, health + amount);
        System.out.println(name + " heals for " + amount + ". Health: " + health);
    }
    
    public void levelUp() {
        level++;
        maxHealth += 10;
        health = maxHealth;
        System.out.println(name + " reached level " + level + "!");
        onLevelUp();
    }
    
    public boolean isAlive() {
        return health > 0;
    }
    
    // Abstract methods for character-specific behavior
    public abstract void attack(GameCharacter target);
    public abstract void useSpecialAbility();
    protected abstract void onLevelUp();
    
    @Override
    public String toString() {
        return name + " (Level " + level + ", HP: " + health + "/" + maxHealth + ")";
    }
}

public class Warrior extends GameCharacter {
    private int strength;
    
    public Warrior(String name) {
        super(name, 120);
        this.strength = 15;
    }
    
    @Override
    public void attack(GameCharacter target) {
        System.out.println(name + " swings sword!");
        target.takeDamage(strength);
    }
    
    @Override
    public void useSpecialAbility() {
        System.out.println(name + " uses Shield Block! Defense increased.");
    }
    
    @Override
    protected void onLevelUp() {
        strength += 3;
        System.out.println("Strength increased to " + strength);
    }
}

public class Mage extends GameCharacter {
    private int magicPower;
    private int mana;
    
    public Mage(String name) {
        super(name, 80);
        this.magicPower = 20;
        this.mana = 100;
    }
    
    @Override
    public void attack(GameCharacter target) {
        if (mana >= 10) {
            System.out.println(name + " casts Fireball!");
            target.takeDamage(magicPower);
            mana -= 10;
        } else {
            System.out.println(name + " is out of mana! Weak attack.");
            target.takeDamage(5);
        }
    }
    
    @Override
    public void useSpecialAbility() {
        System.out.println(name + " casts Arcane Shield! Magic barrier active.");
        mana -= 20;
    }
    
    @Override
    protected void onLevelUp() {
        magicPower += 5;
        mana = 100;
        System.out.println("Magic power increased to " + magicPower);
    }
}

Using the game characters:

GameCharacter hero = new Warrior("Conan");
GameCharacter enemy = new Mage("Dark Wizard");

System.out.println(hero);
System.out.println(enemy);
System.out.println();

hero.attack(enemy);
enemy.attack(hero);
hero.useSpecialAbility();

System.out.println();
hero.levelUp();

Output:

Conan (Level 1, HP: 120/120)
Dark Wizard (Level 1, HP: 80/80)

Conan swings sword!
Dark Wizard takes 15 damage. Health: 65
Dark Wizard casts Fireball!
Conan takes 20 damage. Health: 100
Conan uses Shield Block! Defense increased.

Conan reached level 2!
Strength increased to 18

Common Mistakes

Forgetting to implement abstract methods. If your subclass doesn’t implement all abstract methods, it must also be declared abstract.

Trying to instantiate abstract classes. You can’t use new on an abstract class. Create instances of concrete subclasses.

Overusing abstract classes. Don’t force an inheritance hierarchy where composition would work better. “Has-a” relationships often beat “is-a” relationships.

Making too many methods abstract. If every method is abstract, you might want an interface instead.

What’s Next

Errors happen. Files don’t exist. Networks fail. User input is invalid. Exception handling lets you detect and respond to errors gracefully instead of crashing. The next tutorial covers try-catch blocks and Java’s exception system.


Related: Java Interfaces | Java Exception Handling

Sources

  • Oracle. “Abstract Methods and Classes.” The Java Tutorials. docs.oracle.com
  • Gamma, Erich et al. “Design Patterns.” Addison-Wesley, 1994.
Scroll to Top