Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

published: reading time: 21 min read author: Geek Workbench
Quick Summary

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

Abstract Classes in Java

Abstract classes sit between interfaces and concrete classes — they provide a common implementation that subclasses inherit, while leaving certain methods undefined for subclasses to implement.

Introduction

Abstract classes sit between pure interfaces and fully-implemented concrete classes. They exist to solve a specific problem: you have related classes that share implementation code, but also require certain methods to be customized by each subclass. A plain interface cannot hold shared implementation, and a concrete class cannot leave methods undefined. Abstract classes bridge this gap by letting you declare abstract methods that subclasses must implement while providing concrete methods that subclasses inherit ready-made.

This matters in practice because inheritance hierarchies built on abstract classes tend to be more maintainable than those built on concrete parent classes. The Template Method pattern is the canonical use case — a final method in the abstract class defines an algorithm’s skeleton, calling abstract methods at key steps. Subclasses provide the implementations of those steps, and the algorithm structure is guaranteed across every subclass. Without this pattern, you’d either duplicate the algorithm in every subclass or use fragile inheritance chains that break when parent implementation changes.

This post covers when to use abstract classes versus interfaces, the Template Method pattern in detail, constructor behavior (including why you should never call abstract methods from constructors), sealed classes for controlled inheritance, and the failure modes that catch most developers — including the infamous NPE from calling overridable methods during construction. By the end, you’ll know exactly when abstract classes earn their place in your design versus when a plain interface or composition would serve better.

When to Use

Use abstract classes when:

  • Shared implementation exists — multiple related subclasses share common code
  • A common base type is needed — but some behavior cannot be fully specified
  • You need single inheritance — one class can extend only one abstract class
  • You want to provide a template — subclasses fill in the blanks following a defined pattern
public abstract class Notification {
    // Concrete method — shared implementation
    protected final void log(String message) {
        System.out.println("[NOTIFICATION] " + message);
    }

    // Abstract method — must be implemented by subclasses
    public abstract void send(String recipient, String content);

    // Concrete method using abstract method
    public void notifyUser(String recipient, String content) {
        log("Sending to " + recipient);
        send(recipient, content);  // Calls subclass implementation
        log("Sent successfully");
    }
}

public class EmailNotification extends Notification {
    @Override
    public void send(String recipient, String content) {
        System.out.println("EMAIL to " + recipient + ": " + content);
    }
}

public class SmsNotification extends Notification {
    @Override
    public void send(String recipient, String content) {
        System.out.println("SMS to " + recipient + ": " + content);
    }
}

When Not to Use

Avoid abstract classes when:

  • Multiple unrelated classes need the same contract — use interfaces
  • All methods should be abstract — interfaces express pure contracts better
  • You need multiple inheritance — a class can only extend one abstract class
  • Simplicity is preferred — if only one subclass exists, maybe a regular class is better
// Don't: abstract class for a single implementation
public abstract class SingletonBase {
    protected void commonMethod() { }
}

public class OnlyUse extends SingletonBase { }  // Just use a regular class

// Do: abstract class for shared behavior + template
public abstract class Parser {
    // Common structure all parsers follow
    public final ParseResult parse(String input) {
        validate(input);
        return doParse(input);
    }

    private void validate(String input) {
        if (input == null || input.isEmpty()) {
            throw new IllegalArgumentException("Input cannot be empty");
        }
    }

    // Subclasses implement this
    protected abstract ParseResult doParse(String input);
}

Abstract Class Architecture — Mermaid Diagram

classDiagram
    class Notification {
        +log(message) void
        +send(recipient, content) void$abstract
        +notifyUser(recipient, content) void
    }
    class EmailNotification {
        +send(recipient, content) void
    }
    class SmsNotification {
        +send(recipient, content) void
    }
    class PushNotification {
        +send(recipient, content) void
    }
    Notification <|-- EmailNotification
    Notification <|-- SmsNotification
    Notification <|-- PushNotification
    note for Notification "abstract — cannot be instantiated\nsend() is abstract — no implementation"

Failure Scenarios

1. Trying to Instantiate an Abstract Class

The compiler treats abstract as a hard constraint. You cannot create an instance of an abstract class, even if you try to assign it to a variable of the abstract type. This is not a runtime exception that could theoretically be caught and handled; it is a compile-time error that stops the build entirely.

The error message is direct: “Cannot instantiate abstract class.” Java enforces this because an abstract class contains at least one abstract method with no implementation. Creating an object of that type would leave a method call with nowhere to go, so Java prevents it rather than risking a runtime failure.

The right approach is to use the abstract type as a reference while instantiating the concrete subclass. This is polymorphism in action — you interact with the Shape interface without needing to know whether the underlying object is a Circle, Rectangle, or Triangle. The concrete subclass fulfills what the abstract type promises.

public abstract class Shape {
    public abstract double area();
}

// Does not compile
Shape s = new Shape();  // Error: Cannot instantiate abstract class

// Correct usage
Shape s = new Circle(5);  // Shape reference, Circle object

2. Forgetting to Implement Abstract Methods

Java treats abstract methods as a binding contract. When a class extends an abstract class, it agrees to implement every abstract method in the hierarchy. If it does not, the compiler refuses to build — the build stops with an error before you can even run the code.

This differs from languages where unimplemented methods silently default to no-op behavior or fail only at runtime. Java forces you to handle the contract at compile time, which prevents a situation where code tries to call a method that has no body. Discovering that at runtime, when users are affected, is far worse than a build failure.

There are two ways to handle this. You can implement the method properly with the @Override annotation and a concrete body, as the Complete class does. Or you can acknowledge that your subclass is also incomplete by declaring it abstract — this passes the requirement down the inheritance chain to however eventually extends your class. Neither approach is wrong; it depends on whether your subclass represents a real type you actually intend to instantiate.

One gotcha to watch for: the compiler checks abstract methods declared directly in the parent class. But if an abstract class implements an interface and skips implementing one of the interface methods, that unimplemented method also becomes a requirement for any concrete subclass of your abstract class.

public abstract class Base {
    public abstract void doSomething();
}

// Does not compile — must implement all abstract methods
public class Incomplete extends Base {
    // Missing @Override doSomething()
}

// Fix: implement all abstract methods or declare class abstract
public class Complete extends Base {
    @Override
    public void doSomething() {
        System.out.println("Done!");
    }
}

3. Calling Abstract Methods from Constructor

This failure scenario is subtler than a compile error — the code compiles fine, but it breaks at runtime in a way that’s hard to debug. When a subclass constructor runs, Java executes the parent constructor first. If that parent constructor calls a method that subclasses are expected to override, the override executes before the subclass’s own fields are initialized. The overridden method runs on a partially constructed object, reading fields that are still at their default values (0 for int, null for object references).

The result is a silent data corruption bug: fields you expected to be set have their zero/null values, and the object appears to behave incorrectly. The final field value in the example below is never set to 10 because initialize() runs before the assignment this.value = 10 executes. This is the infamous NPE and uninitialized field problem that catches most developers who aren’t aware of Java’s construction order.

public abstract class Base {
    public Base() {
        // Don't call overridable methods here
        initialize();  // Will call subclass override, but object not fully constructed
    }

    public abstract void initialize();
}

public class Derived extends Base {
    private final int value;

    public Derived() {
        this.value = 10;  // This happens AFTER initialize() called in Base constructor
    }

    @Override
    public void initialize() {
        // At this point, value is still default 0, not yet 10!
        System.out.println("Initializing with value: " + value);  // Prints 0!
    }
}

Trade-off Table

FeatureAbstract ClassInterface
ImplementationCan have fully implemented methodsDefault methods (Java 8+) but still limited
FieldsCan have any fieldsStatic constants only
Multiple inheritanceSingle class extends one abstractClass can implement many interfaces
ConstructorsCan have constructorsCannot have constructors
Access modifiersAll access levelsMethods are implicitly public

Code Snippets

Template Method Pattern

The Template Method pattern is the most compelling use case for abstract classes in practice. The idea is straightforward: define the skeleton of an algorithm in a final method on the abstract class, and let subclasses provide implementations for the steps that vary. The abstract class holds the high-level flow; subclasses fill in the details. This guarantees every subclass follows the same overall structure without any code duplication.

In the example below, process() is the template method. It defines the sequence — fetch, clean, analyze, save — but each step is either abstract (left to subclasses) or concrete (shared implementation). The clean() method is concrete: it does the same thing for every subclass. The fetchData(), analyze(), and save() methods are abstract: each subclass provides its own data source, analysis logic, and storage. When you call process() on any DataProcessor subclass, you get the full algorithm with the subclass’s specific implementations at each step.

public abstract class DataProcessor {
    // Template method — defines the algorithm skeleton
    public final void process(String dataSource) {
        String rawData = fetchData(dataSource);
        String cleanedData = clean(rawData);
        Object result = analyze(cleanedData);
        save(result);
    }

    // Steps to be implemented by subclasses
    protected abstract String fetchData(String source);
    protected abstract Object analyze(String data);
    protected abstract void save(Object result);

    // Shared implementation — can be used as-is or overridden
    protected String clean(String raw) {
        return raw.trim().toLowerCase();
    }
}

public class CloudDataProcessor extends DataProcessor {
    @Override
    protected String fetchData(String source) {
        return cloudClient.download(source);
    }

    @Override
    protected Object analyze(String data) {
        return new MLModel(data).predict();
    }

    @Override
    protected void save(Object result) {
        database.store(result);
    }
}

Abstract Class with Protected Helper

Another common abstract class pattern is the protected helper — the abstract class defines the public API as concrete methods, each one calling a protected abstract primitive that subclasses implement. The public methods handle the logic that applies across all implementations (null checks, bounds checks, state checks), while the protected methods contain only the subclass-specific storage logic. This keeps the public contract clean and consistent while delegating the actual data storage to subclasses.

The Queue<T> example below shows this clearly. The public enqueue(), dequeue(), isEmpty(), and size() methods all contain logic that every queue implementation needs — null guards, empty-state checks, size tracking. But the actual storage mechanism (array, linked list, database, file) is left to subclasses via the protected abstract methods. A LinkedListQueue and an ArrayQueue share the same public behavior but differ entirely in how they store elements internally. You could not achieve this cleanly with interfaces alone, because each implementation would have to duplicate the null-check and state-check logic.

public abstract class Queue<T> {
    // Abstract methods — subclasses implement storage
    protected abstract void enqueueInternal(T item);
    protected abstract T dequeueInternal();
    protected abstract boolean isEmptyInternal();
    protected abstract int sizeInternal();

    // Concrete methods using abstract primitives
    public void enqueue(T item) {
        if (item == null) {
            throw new IllegalArgumentException("Cannot enqueue null");
        }
        enqueueInternal(item);
    }

    public T dequeue() {
        if (isEmpty()) {
            throw new IllegalStateException("Queue is empty");
        }
        return dequeueInternal();
    }

    public boolean isEmpty() {
        return isEmptyInternal();
    }

    public int size() {
        return sizeInternal();
    }
}

Observability Checklist

  • At least one abstract method makes class worth being abstract
  • Abstract class has shared implementation (otherwise use interface)
  • All abstract methods implemented by concrete subclasses
  • Abstract methods are public (implicit in abstract class)
  • Constructors don’t call overridable methods

Security Notes

  • Sealed abstract classes (Java 17+) — restrict which classes can extend your abstract class
  • Don’t call abstract methods in constructors — subclass state not yet initialized
  • Final concrete methods — methods that shouldn’t be overridden should be final
  • Protected fields — if any, document their invariants clearly
// Sealed abstract class — control who can extend (Java 17+)
public abstract sealed class Payment permits CreditCardPayment, DebitPayment, WireTransferPayment {
    protected final double amount;

    protected Payment(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        this.amount = amount;
    }

    public abstract void execute();
}

public final class CreditCardPayment extends Payment { }
public final class DebitPayment extends Payment { }
public final class WireTransferPayment extends Payment { }

// No other class can extend Payment — compiler enforces this

Pitfalls

  1. Forgetting abstract keyword on methods — methods without implementation in abstract class must be abstract
  2. Calling abstract methods from constructor — subclass not yet constructed
  3. Too many abstract methods — if many, consider splitting into multiple abstract classes or interfaces
  4. Abstract class for only one concrete subclass — maybe just merge into the concrete class
  5. Deep hierarchies — abstract classes can create tight coupling; prefer composition
// Bad: abstract class with no real implementation — use interface
public abstract class Serializable {
    void serialize();  // Could just be an interface
}

// Good: abstract class with shared implementation
public abstract class LoggingBean {
    protected Logger logger = Logger.getLogger(getClass());

    protected void logInfo(String msg) { logger.info(msg); }
    protected void logError(String msg, Exception e) { logger.error(msg, e); }

    // Subclasses implement business logic
    public abstract void doSomething();
}

Quick Recap

  • abstract class = cannot be instantiated, serves as base for subclasses
  • abstract method = no implementation, must be overridden by concrete subclasses
  • Concrete subclass = implements all abstract methods, can be instantiated
  • Template method = final method in abstract class that calls abstract methods (subclass controls algorithm)
  • Cannot instantiate directly — only its concrete subclasses can be created

Interview Questions

1. What is an abstract class in Java?
An abstract class is a class marked with the `abstract` keyword that cannot be instantiated directly. It serves as a base type for subclasses and may contain abstract methods (methods without implementation) that subclasses must implement, concrete methods (with implementation) that are shared across subclasses, or both."

2. What is the difference between an abstract class and an interface?
An abstract class can have both abstract and concrete methods, any type of fields, and constructors. An interface (pre-Java 8) could only have abstract methods and static constants. Java 8+ allows default methods but still cannot have instance fields or constructors. A class can implement multiple interfaces but can only extend one abstract class."

3. Can an abstract class have a constructor?
Yes, abstract classes can have constructors. Even though you cannot instantiate an abstract class directly, subclasses must call `super()` in their constructor to ensure parent initialization runs. Constructors in abstract classes are typically used to initialize shared fields and establish invariants that all subclasses depend on."

4. Can an abstract class be sealed in Java?
Yes, Java 17+ allows `sealed` abstract classes. Combining `abstract sealed` with `permits` explicitly lists which classes may extend the abstract class. This provides controlled inheritance for security-sensitive hierarchies where you want to prevent unknown subclasses from being created."

5. What is the Template Method pattern and how does it relate to abstract classes?
The Template Method pattern uses a `final` method in an abstract class to define an algorithm's skeleton, calling abstract methods at key steps. Subclasses provide implementations of those abstract steps, controlling the algorithm's specific behaviors without changing its structure. This enforces consistency across all subclasses while allowing customization."

6. What happens if a subclass doesn't implement all abstract methods?
The code does not compile. Any subclass of an abstract class must implement all abstract methods from the parent, or the subclass itself must be declared abstract. This is enforced by the compiler — you cannot create a concrete class with unimplemented abstract methods."

7. Why should abstract methods not be called from constructors?
When a subclass constructor runs, the parent constructor executes first. If the parent constructor calls an abstract method, the subclass override executes before the subclass's own fields are initialized. This means the abstract method runs on a partially constructed object with uninitialized fields."

8. What is the difference between abstract class and concrete class?
A concrete class can be instantiated directly with `new`. An abstract class cannot be instantiated — it exists only to be extended. Concrete classes must implement all inherited abstract methods; abstract classes may leave methods unimplemented for subclasses to fill in."

9. Can an abstract class extend a concrete class?
Yes, an abstract class can extend a concrete class. The abstract class inherits all the concrete class's implementation and can add its own abstract methods on top. This is uncommon but valid — the abstract class is adding a layer of abstraction on top of existing functionality."

10. When should you prefer an abstract class over an interface?
Prefer an abstract class when you have genuine shared implementation that subclasses need to inherit — reusable code, not just a contract. When you need constructors or instance fields. When you want single inheritance with multiple interface implementations. When the relationship is genuinely an 'is-a' relationship with shared behavior."

11. Can an abstract class implement an interface?
Yes, an abstract class can implement an interface and need not implement all of its methods. The abstract class can leave some interface methods unimplemented, and concrete subclasses of the abstract class must implement them. This is a common pattern for providing partial implementation of a contract."

12. What is a concrete subclass?
A concrete subclass is a class that extends an abstract class and implements all of its abstract methods. Unlike an abstract class, a concrete subclass can be instantiated with `new`. All abstract methods from the parent hierarchy must be implemented for a class to be concrete."

13. Can abstract methods be private in Java?
No, abstract methods cannot be private. If an abstract method were private, subclasses could not override it, which defeats the purpose of being abstract. The abstract method must be accessible to any class that might extend the abstract class and provide an implementation."

14. What is the relationship between abstract classes and the fragile base class problem?
Abstract classes suffer from the same fragile base class problem as regular classes — changes to the abstract parent can break subclasses. However, because abstract classes typically have more undefined behavior (abstract methods), subclasses have more flexibility in how they implement behavior. Still, changing concrete methods in the abstract parent can unexpectedly affect subclasses."

15. Can you declare a class both abstract and final?
No, this is a compile error. A class declared `abstract` cannot be instantiated, by definition — something must extend it. A class declared `final` cannot be extended. These modifiers are mutually exclusive. The compiler will reject the combination."

16. How do abstract classes support polymorphism?
You can declare a variable of the abstract type and assign any concrete subclass instance to it. Method calls on the abstract reference will resolve to the concrete implementation at runtime. This is polymorphism — the calling code works with the abstract interface without knowing which concrete subclass is actually being used."

17. What is the difference between abstract class and interface for multiple inheritance?
A class can implement multiple interfaces but can only extend one abstract class. This is the key advantage of interfaces for achieving multiple inheritance-like behavior. If you need both shared implementation and multiple contracts, you can extend one abstract class and implement multiple interfaces."

18. Can abstract classes have static methods?
Yes, abstract classes can have static methods. Static methods belong to the class, not to instances, so they can be called without an instance. They are not inherited as abstract methods would be, but they are accessible to all subclasses and can be called normally."

19. What is the difference between the Template Method pattern and the Strategy pattern?
Template Method uses inheritance — the abstract class defines the algorithm skeleton and subclasses provide step implementations. Strategy uses composition — the context class holds an interface reference and clients inject different strategy implementations at runtime. Template Method locks in the algorithm structure; Strategy allows swapping the entire algorithm."

20. How do sealed abstract classes improve security?
Sealed abstract classes with `permits` explicitly list which classes may extend them. The compiler enforces that no other class can extend a sealed abstract class. This prevents malicious or unexpected subclasses from being introduced into the hierarchy, which is important for security-sensitive code that relies on specific subclasses behaving correctly."

Further Reading

Conclusion

Abstract classes occupy a middle ground between interfaces (pure contracts, no implementation) and concrete classes (fully implemented). They exist to provide shared implementation that subclasses inherit, while also defining abstract methods that each subclass must implement. This makes abstract classes ideal for situations where you have genuine code reuse across related classes, not just a shared contract.

The Template Method pattern is the canonical use case for abstract classes. A final method in the abstract class defines an algorithm’s skeleton, calling abstract methods at key points. Subclasses provide the specific implementations of those steps while being forced to follow the overall structure. This ensures consistent behavior across all subclasses without duplicating the algorithm itself.

Constructors in abstract classes work differently than in concrete classes — you cannot instantiate an abstract class directly, but subclasses must still call super() to ensure the parent initialization runs. The security checklist point about not calling overridable methods from constructors is critical: when a subclass constructor runs, the parent constructor runs first, and if that parent constructor calls an abstract method, the subclass override executes on a partially initialized object.

The sealed class feature (Java 17+) adds control over who can extend an abstract class. Using sealed with permits explicitly lists which classes may inherit, preventing unexpected subclasses and potential security issues. This is particularly useful for abstract classes that form a controlled hierarchy, like the payment types example in the security notes.

Abstract classes connect directly to polymorphism (covered in Polymorphism in Java) — you can store any concrete subclass in a variable of the abstract type, and the correct overridden methods will be called at runtime. They also relate to interfaces (detailed in Interfaces in Java), which a class can implement many of while extending only one abstract class, giving you both multiple contracts and shared implementation in the same type hierarchy.

Category

Related Posts

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals

ArrayList in Java

Learn ArrayList: dynamic resizing, internal array management, when to choose ArrayList over plain arrays, and performance trade-offs.

#java-arraylist #java #java-fundamentals