Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
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
| Feature | Abstract Class | Interface |
|---|---|---|
| Implementation | Can have fully implemented methods | Default methods (Java 8+) but still limited |
| Fields | Can have any fields | Static constants only |
| Multiple inheritance | Single class extends one abstract | Class can implement many interfaces |
| Constructors | Can have constructors | Cannot have constructors |
| Access modifiers | All access levels | Methods 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
- Forgetting abstract keyword on methods — methods without implementation in abstract class must be abstract
- Calling abstract methods from constructor — subclass not yet constructed
- Too many abstract methods — if many, consider splitting into multiple abstract classes or interfaces
- Abstract class for only one concrete subclass — maybe just merge into the concrete class
- 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
abstractclass = cannot be instantiated, serves as base for subclassesabstractmethod = no implementation, must be overridden by concrete subclasses- Concrete subclass = implements all abstract methods, can be instantiated
- Template method =
finalmethod in abstract class that calls abstract methods (subclass controls algorithm) - Cannot instantiate directly — only its concrete subclasses can be created
Interview Questions
Further Reading
- Polymorphism in Java — polymorphic behavior through abstract types
- Interfaces in Java — pure contracts vs shared implementation
- Composition over Inheritance — when composition fits better
- Oracle: Abstract Methods and Classes — official documentation on abstract class usage
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.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.
ArrayList in Java
Learn ArrayList: dynamic resizing, internal array management, when to choose ArrayList over plain arrays, and performance trade-offs.