Free lesson · Java Basic To Advance
Introduction to Abstract Classes
The Partial Implementation
Module 9 covered interfaces — pure contracts that define what methods must exist but provide no implementation (with the modern exception of default methods). This Module covers abstract classes — classes that combine implementation with required customization. They're the middle ground between fully concrete classes and pure interfaces.
An abstract class typically provides:
- Some complete methods (the "shared" behavior)
- Some abstract methods (the "must be customized" behavior)
- Instance state shared across subclasses
- Constructors for that shared state
Subclasses inherit the complete methods, implement the abstract ones, and get the shared state for free. This is the partial implementation pattern — half a class, with subclasses completing it.
Brandon Carter continuing through Module 10. Abstract classes are one of the most-debated OOP topics. Some teams use them heavily, others almost never. Knowing when each approach fits is senior-level Java design.
What an Abstract Class Is
An abstract class is declared with the abstract keyword:
public abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public abstract void speak();
}
This class:
- Has a constructor (so subclasses can call it)
- Has a private field (instance state)
- Has a complete method (
getName()) - Has an abstract method (
speak()— no body)
The key restriction: you cannot instantiate an abstract class directly:
Animal a = new Animal("Rex"); // compile error — Animal is abstract
You can only instantiate concrete subclasses:
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(getName() + " says Woof!");
}
}
Dog rex = new Dog("Rex");
rex.speak(); // "Rex says Woof!"
Dog inherits getName() and the name field, must provide speak(). The combination produces a complete, concrete class.
Why Abstract Classes Exist
Three primary reasons:
1. Share implementation across related classes.
If multiple classes have common state and behavior, an abstract parent factors that out:
public abstract class HttpHandler {
private final String basePath;
private final Logger logger;
private long requestCount = 0;
protected HttpHandler(String basePath, Logger logger) {
this.basePath = basePath;
this.logger = logger;
}
public final HttpResponse handle(HttpRequest request) {
requestCount++;
logger.info("Handling " + basePath + " request");
try {
return doHandle(request);
} catch (Exception e) {
logger.error("Handler failed", e);
return HttpResponse.serverError();
}
}
protected abstract HttpResponse doHandle(HttpRequest request);
public long getRequestCount() {
return requestCount;
}
}
Every HttpHandler subclass gets:
- The basePath/logger fields managed for it
- The orchestration (counting, logging, error handling)
- A consistent error-handling pattern
- The getRequestCount() method
Subclasses only have to provide doHandle() — the actual handler logic. The framework provides everything else. This factor of "common scaffolding + customized core" is the abstract class's main use case.
2. Force subclasses to implement specific methods.
Abstract methods are the mechanism for "you MUST provide this." A regular concrete class can be extended without overriding anything. An abstract class with abstract methods forces subclasses to implement them.
This makes the contract explicit and compiler-enforced. You can't extend an abstract class and "forget" to implement a required method — the compiler catches it.
3. Provide a partially-completed type that callers can use.
Callers can work with the abstract class type even if they have a concrete subclass:
public void process(Animal animal) {
System.out.println("Processing " + animal.getName());
animal.speak();
}
process(new Dog("Rex")); // works through polymorphism
process(new Cat("Whiskers"));
The process method accepts any Animal subclass. It knows getName() exists (concrete in Animal) and that speak() will work (abstract, but every concrete subclass must implement it).
What Distinguishes Abstract from Concrete Classes
The differences are mechanical:
| Property | Concrete Class | Abstract Class |
|---|---|---|
abstract keyword on class |
No | Yes |
| Can be instantiated | Yes | No |
| Can have abstract methods | No | Yes |
| Can have concrete methods | Yes | Yes |
| Can have constructors | Yes | Yes (called by subclasses) |
| Can have instance fields | Yes | Yes |
| Can be extended | Yes (unless final) | Yes |
The only "different" features of abstract classes: they can't be instantiated directly, and they can have abstract methods. Otherwise they behave like regular classes — they have state, constructors, methods, and can be extended.
A Real Production Pattern
At AWS S3, our operation framework has an abstract S3Operation base class:
public abstract class S3Operation<Req, Resp> {
protected final String region;
protected final S3Client client;
private final MetricsCollector metrics;
private final RetryStrategy retryStrategy;
protected S3Operation(String region, S3Client client,
MetricsCollector metrics, RetryStrategy retry) {
this.region = region;
this.client = client;
this.metrics = metrics;
this.retryStrategy = retry;
}
public final Resp execute(Req request) {
long start = System.nanoTime();
try {
validateRequest(request);
Resp response = executeInternal(request);
metrics.recordSuccess(getOperationName(), System.nanoTime() - start);
return response;
} catch (Exception e) {
metrics.recordFailure(getOperationName(), e);
throw e;
}
}
protected abstract String getOperationName();
protected abstract void validateRequest(Req request);
protected abstract Resp executeInternal(Req request);
}
public class GetObjectOperation extends S3Operation<GetRequest, GetResponse> {
public GetObjectOperation(/* ... */) { super(/* ... */); }
@Override protected String getOperationName() { return "GetObject"; }
@Override protected void validateRequest(GetRequest r) {
if (r.getBucket() == null) throw new IllegalArgumentException();
}
@Override protected GetResponse executeInternal(GetRequest r) {
return client.getObject(r);
}
}
Every S3 operation subclass gets retry, metrics, validation orchestration, and error handling for free. Subclasses focus on their specific logic. There are over 50 operation types in our codebase — without the abstract base, every one would need to duplicate the scaffolding.
This is the abstract class pattern at scale: significant shared implementation + abstract hooks for customization.
When NOT to Use Abstract Classes
Abstract classes have costs:
Single-inheritance constraint. A class extending your abstract class can't extend anything else. If subclasses might need to participate in other hierarchies, abstract classes lock them in.
Inflexibility. Adding abstract methods later breaks all subclasses. Even adding concrete methods might collide with subclass methods.
Implementation coupling. Subclasses inherit your implementation. Changes to the parent ripple through. This is the "fragile base class" problem from Topic 8.12.
Hard to test in isolation. Abstract classes can't be instantiated for testing. You have to test through concrete subclasses, often creating test-only subclasses just for testing.
If these costs outweigh the benefits, prefer interfaces with default methods, or use composition.
When Abstract Classes Fit Best
Abstract classes are the right choice when:
-
Significant shared state. Multiple subclasses need the same instance fields and the same constructor logic to set them up.
-
Substantial shared implementation. The shared methods are too much to express as default methods on an interface.
-
Genuine is-a hierarchy. All subclasses are clearly subtypes of the abstract concept (Animal → Dog/Cat, not Functionality → Service/Helper).
-
Template method pattern fits. The parent orchestrates with
finalmethods, subclasses customize via abstract methods. -
Framework or library design. Where you provide a base class for users to extend, with controlled extension points.
graph TD
A["Need an abstraction?"] --> B{"Significant<br/>shared state?"}
B -->|Yes| C["Abstract class"]
B -->|No| D{"Multiple inheritance<br/>needed?"}
D -->|Yes| E["Interface"]
D -->|No| F{"Significant<br/>shared implementation?"}
F -->|Yes - too much for defaults| C
F -->|No - simple contract| E
style C fill:#3b82f6,color:#fff
style E fill:#10b981,color:#fff
This diagram shows the high-level decision between abstract classes and interfaces. Start with whether you need shared instance state — fields that multiple subclasses need to inherit. If yes, abstract class is the right choice; interfaces can't have instance state. If you don't need shared state but you do need a class to participate in multiple type relationships, use interfaces — a class can implement many interfaces but extend only one abstract class. If neither shared state nor multiple inheritance matters, the decision comes down to how much shared implementation you have. Lots of shared logic that's awkward as default methods favors abstract class. Simple contracts favor interfaces. Module 9 Topic 12 covered this comparison in depth.
What This Module Covers
Module 10 covers abstract classes in depth:
- Topic 10.2 — The
abstractkeyword (syntax mechanics) - Topic 10.3 — Abstract methods (declarations and rules)
- Topic 10.4 — Concrete methods in abstract classes (shared implementation)
- Topic 10.5 — Constructors in abstract classes (chain mechanics)
- Topic 10.6 — Fields and state (instance-level data)
- Topic 10.7 — Extending abstract classes (rules for subclasses)
- Topic 10.8 — Abstract classes and inheritance (multi-level hierarchies)
- Topic 10.9 — Template method pattern (the key design pattern)
- Topic 10.10 — Abstract classes vs interfaces (the design decision, revisited)
- Topic 10.11 — Skeletal implementation pattern (interface + abstract class combined)
- Topic 10.12 — Signature Interview Lens
After this module, you'll be able to design abstract class hierarchies, choose between abstract classes and interfaces, recognize the template method pattern, and combine both abstraction mechanisms via skeletal implementations.
Common Pitfalls
Warning: Trying to instantiate an abstract class. Compile error. You can only
newconcrete subclasses.Forgetting that abstract classes can have constructors. They can — the constructor runs as part of the subclass's construction chain.
Adding too many abstract methods. Each one is a burden on every subclass. Keep abstract methods focused.
Choosing abstract class when interface would do. Single-inheritance limits subclasses. Prefer interfaces when in doubt (Effective Java Item 20).
Premature abstraction. Don't extract an abstract class until you have multiple concrete classes sharing actual structure. YAGNI applies.
Why This Matters in Interviews
Interview: Abstract class questions appear in nearly every Java interview. "What's an abstract class?" "When would you use one?" "What's the difference between abstract class and interface?" (revisiting Topic 9.12).
Topic 10.12 covers the full Interview Lens.
Try It
Exercise: Write each:
- An abstract class
Shapewith concretegetName()and abstractarea(). Two concrete subclasses (Circle, Square).- Try to instantiate Shape directly — observe the compile error.
- Instantiate the subclasses, treat them as Shape references, call methods.
- Add a constructor to Shape. Verify subclasses must call super().
- Try writing the same abstraction as an interface with a default method for getName(). Compare the two approaches.
Each exercise builds basic abstract class intuition.