
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.


