Skip to main content

Section 9.2 Working with Abstract Classes

Subsection 9.2.1 Creating Abstract Classes and Methods

An abstract class in Java is defined with the keyword abstract. Abstract classes can contain:
  • Fields (instance variables)
  • Constructors (though they can only be called by subclasses)
  • Regular (concrete) methods with implementations
  • Abstract methods - methods declared without implementations
An abstract method is a method declaration without a body. It specifies the method’s signature (name, parameters, and return type) but doesn’t provide an implementation:
// An abstract method - note the semicolon instead of a method body
public abstract void calculateTax();
This tells Java: "Any non-abstract subclass of this class MUST provide an implementation for this method." It’s like a contract that forces subclasses to fulfill certain responsibilities.
Here’s a simple abstract class example:
// Abstract class with both concrete and abstract methods
public abstract class Document {               // ← Note the 'abstract' keyword
    protected String filename;                 // ← Instance variable (state)
    
    // Constructor - can be called by subclasses
    public Document(String filename) {
        this.filename = filename;
    }
    
    // Concrete method with implementation
    public String getFilename() {
        return filename;
    }
    
    // Abstract methods - MUST be implemented by subclasses 
    public abstract void save();               // ← Note: no method body, ends with semicolon
    public abstract void print();
}

Note 9.2.1. Why Abstract Classes Can’t Be Instantiated.

You cannot create objects directly from abstract classes:
        // This won't compile:
Document doc = new Document("report.txt"); // Error
This makes sense because abstract classes contain "incomplete" methods (the abstract ones). Since abstract methods have no implementation, Java can’t allow you to create objects that would be unable to respond to method calls.
To use an abstract class, you must create a subclass that implements all the abstract methods:
// Concrete subclass implementing the abstract methods
public class PDFDocument extends Document {    // ← Extends the abstract class
    public PDFDocument(String filename) {
        super(filename);                       // ← Calls abstract class constructor
    }
    
    @Override                                  // ← Always use @Override annotation
    public void save() {
        System.out.println("Saving PDF file: " + filename);
        // PDF-specific saving code
    }
    
    @Override                                  // ← Required for all abstract methods 
    public void print() {
        System.out.println("Printing PDF file: " + filename);
        // PDF-specific printing code
    }
}
Key takeaways about abstract classes:
  • They use the abstract keyword in their class definition
  • They cannot be instantiated (no new AbstractClass())
  • They can contain a mix of abstract and concrete methods
  • They can have constructors, fields, and regular methods
  • Subclasses must implement all abstract methods or also be declared abstract

Subsection 9.2.2 Implementing Abstract Methods in Subclasses

Any concrete (non-abstract) subclass extending an abstract class must implement all its abstract methods. Let’s build a simple vehicle example step by step:
Step 1: First, we define our abstract Vehicle class with abstract methods:
// Abstract class for vehicles
public abstract class Vehicle {
    protected String model;      // ← Shared state for all vehicles
    
    public Vehicle(String model) {
        this.model = model;
    }
    
    // Abstract methods that all vehicles must implement
    public abstract void start();
    public abstract void stop();
    
    // Concrete method shared by all vehicles
    public String getModel() {
        return model;
    }
}
Step 2: Now, we create a concrete subclass that implements the abstract methods:
// Concrete subclass of Vehicle
public class Car extends Vehicle {
    private int fuelLevel;      // ← Car-specific state
    
    public Car(String model, int fuelLevel) {
        super(model);           // ← Call to parent constructor
        this.fuelLevel = fuelLevel;
    }
    
    @Override                   // ← Always use @Override annotation
    public void start() {
        if (fuelLevel > 0) {
            System.out.println("Starting car engine for " + model);
        } else {
            System.out.println("Cannot start - out of fuel!");
        }
    }
    
    @Override                   // ← Required for all abstract methods
    public void stop() {
        System.out.println("Stopping car engine for " + model);
    }
}
Notice how the Car class:
  • Extends the abstract Vehicle class
  • Calls the parent constructor using super(model)
  • Implements both abstract methods (start() and stop())
  • Uses the @Override annotation for clarity and compile-time checking

Note 9.2.2. 🚩 Important: Use @Override Annotation.

Always use the @Override annotation when implementing abstract methods. This annotation is technically optional but strongly recommended because:
  • It asks the compiler to verify you’re actually overriding a method (not creating a new one)
  • If you misspell the method name or use incorrect parameters, the compiler will immediately catch the error
  • It makes your code more readable by clearly showing which methods fulfill the contract
Without @Override, you might accidentally create a new method instead of implementing the required one. The compiler would then complain that you haven’t implemented the abstract method, but it wouldn’t tell you why.
Here are common errors when implementing abstract methods:
// COMMON MISTAKES when implementing abstract methods
public class BrokenCar extends Vehicle {
    // Mistake 1: Method signature doesn't match (wrong name)
    @Override
    public void startEngine() {  // Should be 'start()'
        // This won't satisfy the abstract method requirement
        System.out.println("Starting engine");
    }
    
    // Mistake 2: Wrong return type
    @Override
    public boolean stop() {  // Should return void, not boolean
        System.out.println("Stopping");
        return true;
    }
    
    // Mistake 3: Forgetting to implement an abstract method
    // 'stop()' is missing completely - compiler will detect this
}
The compiler will generate errors for each of these mistakes. Pay close attention to these error messages as they tell you exactly what’s wrong:
  • "Method does not override method from its superclass" (for startEngine)
  • "Return type boolean is not compatible with void" (for stop)
  • "BrokenCar must implement inherited abstract method Vehicle.stop()" (for the missing method)
Pro Tip: When implementing abstract classes or interfaces, your Java compiler is your most important debugging tool. Always carefully read compiler error messages as they contain precise information about what’s wrong and often suggest how to fix it. The compiler ensures you correctly fulfill the "contract" required by the abstract class or interface.

Insight 9.2.3. Debugging Abstract Method Implementations.

Compiler errors are your best guide when working with abstract classes and interfaces. Unlike runtime bugs that might only appear under specific conditions, the compiler immediately flags contract violations. Think of compiler messages as guardrails keeping your implementation aligned with the required contract.
When you see errors like:
  • "Class X must implement inherited abstract method Y": You forgot to implement a required method
  • "Method does not override a method from superclass": You likely misspelled the method name or used wrong parameters
  • "Return type Z is not compatible with Y": Your implementation returns the wrong type
Don’t just fix the errors mechanically; understand what contract you’re violating. This helps build a deeper comprehension of abstract class and interface design.
Let’s see why abstract classes are useful by comparing code with and without them:
Without Abstract Classes
// No shared structure
public class Car {
    private String model;
    
    public void start() {
        System.out.println("Starting car");
    }
    
    public void stop() {
        System.out.println("Stopping car");
    }
}

public class Motorcycle {
    private String model;
    
    public void start() {
        System.out.println("Starting motorcycle");
    }
    
    // Oops! Developer forgot to implement stop()
    // No compiler warning!
}
// Shared structure through abstraction
public abstract class Vehicle {
    protected String model;
    
    // All vehicles must implement:
    public abstract void start();
    public abstract void stop();
}

public class Motorcycle extends Vehicle {
    @Override
    public void start() {
        System.out.println("Starting motorcycle");
    }
    
    // Compiler error:
    // "Motorcycle must implement 
    // inherited abstract method Vehicle.stop()"
}
The approach with abstract classes ensures that developers can’t forget to implement required methods since the compiler enforces the contract.

Insight 9.2.4. The "Substitutability" Benefit of Abstract Classes.

Abstract classes enable polymorphism: the ability to treat objects of different concrete subclasses uniformly through their common abstract superclass. This is one of the most powerful features of object-oriented programming.
What this means in practice: You can use the abstract superclass type (Vehicle) as a variable type or parameter type to handle any subclass object, even when you don’t know its specific subclass at compile time.
// Working with different vehicles polymorphically
// Create an array of different vehicle types
Vehicle[] vehicles = new Vehicle[3];
vehicles[0] = new Car("Toyota Camry", 50);      // Car object stored in Vehicle variable
vehicles[1] = new Motorcycle("Honda CBR");       // Motorcycle object stored in Vehicle variable
vehicles[2] = new Truck("Ford F-150", 2000);     // Truck object stored in Vehicle variable

// Process all vehicles the same way - without knowing specific types
for (Vehicle v : vehicles) {
    System.out.println("Starting: " + v.getModel());
    v.start();  // Each vehicle type starts differently
    
    // Later...
    v.stop();   // Each vehicle type stops differently
}
This code doesn’t need to know which specific vehicle subclass it’s working with. It simply treats every vehicle through the common Vehicle interface, while at runtime, each object executes its specific implementation of start() and stop(). This allows for:
  • More flexible and extensible code (adding new vehicle types without changing existing code)
  • Reduced code duplication (shared logic in one place)
  • Better organization of related functionality
Key benefit: With polymorphism, you can add new types to your system without modifying existing code. For example, if you later create a Helicopter class extending Vehicle, all code that works with Vehicle objects will automatically work with Helicopter objects without any changes. This is known as the Open/Closed Principle: code should be open for extension but closed for modification.
However, an abstract class only ensures that methods exist, not that they work properly. A faulty implementation could still cause problems:
public class FaultyTruck extends Vehicle {
    @Override
    public void start() {
        // Doesn't actually do anything useful
        System.out.println("Not implemented yet!");
    }
    
    @Override
    public void stop() {
        // Creates unexpected behavior
        throw new RuntimeException("Brakes failed!");
    }
}
When creating abstract classes, consider not just what methods subclasses must provide, but also what the expected behavior should be. Document the contract clearly so subclass authors understand their responsibilities.
Key takeaways about implementing abstract methods:
  • Non-abstract subclasses must implement all abstract methods from their superclass
  • Always use @Override to catch signature mismatches
  • The implementation must match the method signature exactly (name, parameters, return type)
  • Abstract classes enforce a contract that the compiler checks
  • Abstract classes enable polymorphism (treating diverse objects uniformly)

Checkpoint 9.2.5.

Which of the following is true regarding abstract classes or abstract methods? (Select all that apply)
  • Abstract classes only become useful in programs when they are extended.
  • Abstract methods must be overriden.
  • Abstract classes only become useful when we create objects from them.
  • All methods in an abstract class must be overriden and implemented in any of its subclasses.

Checkpoint 9.2.6.

Given the following definitions:
public abstract class Manager extends Employee { ... }
public class Executive extends Manager { ... }
What, if anything, is wrong with the following code?
public class hrSystem {
   public void payRaise() {
      Manager m = new Manager();
   }
}
  • Compiler error because Manager is abstract so an instance cannot be created.
  • Compiler error because there is no Manager constructor.
  • Compiles fine but will generate a run-time error unless an Employee object is created first.
  • No problem. The code will compile without any errors.

Checkpoint 9.2.7.

Given the following definitions:
public abstract class Manager extends Employee { ... }
public class Executive extends Manager { ... }
What kind of classes are Manager and Executive, respectively?
  • Manager is abstract and Executive is concrete.
  • Manager is abstract and Executive is final.
  • Manager is abstract and Executive is overridden.
  • Manager is public and Executive is protected.

Checkpoint 9.2.8.

Why should you use the @Override annotation when implementing an abstract method?
  • It makes the method run faster.
  • Incorrect. @Override is for compile-time verification, not performance improvement.
  • It allows the method to be optional in the subclass.
  • Incorrect. Implementing abstract methods is mandatory for non-abstract subclasses.
  • It helps catch signature mismatches at compile time.
  • Correct! @Override ensures the method signature exactly matches the one in the superclass.
  • It prevents the superclass method from being inherited.
  • Incorrect. @Override does not block inheritance; it ensures proper implementation of inherited methods.

Checkpoint 9.2.9.

Which of the following statements about abstract methods is true?
  • They can have a method body in the abstract class.
  • Incorrect. Abstract methods cannot have a body; they must be implemented by subclasses.
  • Their implementation must match the method signature exactly.
  • Correct! The method name, parameters, and return type must match precisely.
  • They must be declared as private
  • Incorrect. Abstract methods must be public or protected to be accessible by subclasses.
  • They cannot be overridden in a subclass.
  • Incorrect. Implementing an abstract method in a subclass is a form of overriding.

Checkpoint 9.2.10.

What is one of the key benefits of using abstract classes?
  • They allow the creation of objects directly from the abstract class.
  • Incorrect. Abstract classes cannot be instantiated directly.
  • They enable polymorphism by enforcing a common contract for subclasses
  • Correct! Abstract classes ensure that all subclasses adhere to a consistent interface.
  • They can contain only abstract methods and no concrete methods
  • Incorrect. Abstract classes can have both abstract and concrete methods
  • They automatically generate implementations for abstract methods
  • Incorrect. The subclass must explicitly implement abstract methods.
You have attempted of activities on this page.