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.
// 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();
}
Note9.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.
Subsection9.2.2Implementing 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:
// 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);
}
}
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.
// 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:
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.
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.
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.
// 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.
Insight9.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:
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.
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.