Java Abstract Classes
Java Abstract Classes
An abstract class sits in the middle of the abstraction spectrum — more abstract than a regular class, less abstract than an interface. It cannot be instantiated directly, but it can contain both abstract methods (which subclasses must implement) and concrete methods (which subclasses inherit and can optionally override). It can hold fields, constructors, and any level of access modifier.
The abstract class's value is precisely this middle ground. When multiple classes share common logic — a fixed algorithm, shared fields, shared validation — but differ in specific steps, the abstract class holds the shared parts and declares the variable parts as abstract. Subclasses fill in the gaps.
What Is an Abstract Class in Java?
An abstract class is a class declared with the abstract keyword that cannot be instantiated using new. It serves as a base class that other classes extend. It can contain:
- ›Abstract methods — declared without a body using the
abstractkeyword; every concrete subclass must implement them - ›Concrete methods — fully implemented; subclasses inherit and may override them
- ›Instance fields — regular fields; subclasses inherit them and access them via getters or through
super - ›Constructors — cannot be called directly, but called through
super()in subclass constructors - ›Static methods and fields — accessible normally through the class name
Abstract Class vs Regular Class vs Interface
| Aspect | Regular Class | Abstract Class | Interface |
|---|---|---|---|
abstract keyword | No | Yes | Not applicable |
| Instantiation | Yes — new | No — cannot instantiate | No — cannot instantiate |
| Abstract methods | No | Yes — abstract keyword | Yes — by default |
| Concrete methods | Yes | Yes | default and static only (Java 8+) |
| Instance fields | Yes | Yes | No — only constants |
| Constructors | Yes | Yes — called via super() | No |
| Multiple inheritance | No — extends one | No — extends one | Yes — implements many |
| Access modifiers | Any | Any | public by default |
| When to use | Complete implementation | Partial implementation + shared logic | Pure contract + capability definition |
Declaring an Abstract Class
// File: Shape.java
public abstract class Shape {
// Instance field — shared by all shapes
private final String colour;
// Constructor — called by subclass constructors via super()
public Shape(String colour) {
if (colour == null || colour.isBlank()) {
throw new IllegalArgumentException("Colour is required.");
}
this.colour = colour;
}
public String getColour() { return colour; }
// Abstract methods — every subclass MUST implement these
public abstract double area();
public abstract double perimeter();
// Concrete method — shared across all shapes, no override needed
public String describe() {
return getClass().getSimpleName()
+ " | Colour: " + colour
+ " | Area: " + String.format("%.2f", area())
+ " | Perimeter: " + String.format("%.2f", perimeter());
}
}// File: Circle.java
public class Circle extends Shape {
private final double radius;
public Circle(double radius, String colour) {
super(colour); // calls Shape constructor
if (radius <= 0) throw new IllegalArgumentException("Radius must be positive.");
this.radius = radius;
}
public double getRadius() { return radius; }
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}// File: Rectangle.java
public class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height, String colour) {
super(colour);
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Width and height must be positive.");
}
this.width = width;
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
}// File: ShapeDemo.java
import java.util.List;
public class ShapeDemo {
public static void main(String[] args) {
// Cannot do: new Shape("Red") — compile error
List<Shape> shapes = List.of(
new Circle(7.0, "Blue"),
new Rectangle(5.0, 3.0, "Red"),
new Circle(4.5, "Green")
);
for (Shape shape : shapes) {
System.out.println(shape.describe()); // concrete method — runtime dispatch
}
System.out.println("\nTotal area: "
+ String.format("%.2f", shapes.stream()
.mapToDouble(Shape::area)
.sum()));
}
}Output:
Circle | Colour: Blue | Area: 153.94 | Perimeter: 43.98
Rectangle | Colour: Red | Area: 15.00 | Perimeter: 16.00
Circle | Colour: Green | Area: 63.62 | Perimeter: 28.27
Total area: 232.56
Shape cannot be instantiated — it is a concept, not a specific thing. Circle and Rectangle are concrete shapes. The abstract class holds what all shapes share — a colour field, a constructor that validates it, and a describe() method that formats any shape's output. The variable parts — how to calculate area and perimeter — are abstract. Each subclass fills them in.
The Template Method Pattern
The Template Method Pattern is the most important design pattern built on abstract classes. It defines the skeleton of an algorithm in a concrete method of the abstract class, deferring certain steps to abstract methods that subclasses implement. The flow is fixed; the steps are variable.
// File: DataProcessor.java
public abstract class DataProcessor {
// Template method — final so subclasses cannot change the flow
public final ProcessingResult process(String rawData) {
System.out.println("[" + getProcessorName() + "] Starting processing...");
// Step 1 — validate input (subclass decides validation rules)
if (!validate(rawData)) {
return new ProcessingResult(false, "Validation failed.", null);
}
// Step 2 — transform data (subclass decides how)
String transformed = transform(rawData);
// Step 3 — post-process (optional — default is identity)
String finalData = postProcess(transformed);
System.out.println("[" + getProcessorName() + "] Done. Output length: "
+ finalData.length() + " chars.");
return new ProcessingResult(true, "Success", finalData);
}
// Abstract steps — subclasses define these
protected abstract String getProcessorName();
protected abstract boolean validate(String data);
protected abstract String transform(String data);
// Hook method — default behaviour provided, subclass may override
protected String postProcess(String data) {
return data; // default: no post-processing
}
// Result holder
public record ProcessingResult(boolean success, String message, String output) {}
}// File: JsonSanitiser.java
public class JsonSanitiser extends DataProcessor {
@Override
protected String getProcessorName() {
return "JSON-SANITISER";
}
@Override
protected boolean validate(String data) {
if (data == null || data.isBlank()) {
System.out.println(" Validation: Empty data rejected.");
return false;
}
if (!data.trim().startsWith("{") && !data.trim().startsWith("[")) {
System.out.println(" Validation: Not valid JSON structure.");
return false;
}
System.out.println(" Validation: OK");
return true;
}
@Override
protected String transform(String data) {
// Strips newlines and extra spaces for compact JSON
String compact = data.replaceAll("\\s+", " ").trim();
System.out.println(" Transform: Compacted JSON.");
return compact;
}
@Override
protected String postProcess(String data) {
// Wraps in a standard envelope
return "{\"data\":" + data + ",\"sanitised\":true}";
}
}// File: CsvNormaliser.java
public class CsvNormaliser extends DataProcessor {
private final char delimiter;
public CsvNormaliser(char delimiter) {
this.delimiter = delimiter;
}
@Override
protected String getProcessorName() {
return "CSV-NORMALISER";
}
@Override
protected boolean validate(String data) {
if (data == null || data.isBlank()) {
System.out.println(" Validation: Empty CSV rejected.");
return false;
}
long lineCount = data.lines().count();
System.out.println(" Validation: " + lineCount + " lines found. OK");
return true;
}
@Override
protected String transform(String data) {
// Converts all lines to uppercase and trims each field
String normalised = data.lines()
.map(line -> java.util.Arrays.stream(line.split(String.valueOf(delimiter)))
.map(String::trim)
.map(String::toUpperCase)
.reduce((a, b) -> a + delimiter + b)
.orElse(""))
.reduce((a, b) -> a + "\n" + b)
.orElse("");
System.out.println(" Transform: Normalised to uppercase.");
return normalised;
}
// postProcess not overridden — uses default (identity)
}// File: TemplateMethodDemo.java
public class TemplateMethodDemo {
public static void main(String[] args) {
DataProcessor jsonProcessor = new JsonSanitiser();
DataProcessor csvProcessor = new CsvNormaliser(',');
System.out.println("=== JSON Sanitiser ===");
DataProcessor.ProcessingResult jsonResult = jsonProcessor.process(
"{ \"orderId\" : \"ORD-001\" , \"amount\" : 1499 }"
);
System.out.println("Result: " + jsonResult.message());
System.out.println("Output: " + jsonResult.output());
System.out.println("\n=== CSV Normaliser ===");
DataProcessor.ProcessingResult csvResult = csvProcessor.process(
" sku-001 , laptop , 85000\n sku-002 , mouse , 1299 "
);
System.out.println("Result: " + csvResult.message());
System.out.println("Output:\n" + csvResult.output());
System.out.println("\n=== Invalid Input ===");
DataProcessor.ProcessingResult invalid = jsonProcessor.process("");
System.out.println("Result: " + invalid.message());
}
}Output:
=== JSON Sanitiser ===
[JSON-SANITISER] Starting processing...
Validation: OK
Transform: Compacted JSON.
Result: Success
Output: {"data":{ "orderId" : "ORD-001" , "amount" : 1499 },"sanitised":true}
=== CSV Normaliser ===
[CSV-NORMALISER] Starting processing...
Validation: 2 lines found. OK
Transform: Normalised to uppercase.
Result: Success
Output:
SKU-001,LAPTOP,85000
SKU-002,MOUSE,1299
=== Invalid Input ===
[JSON-SANITISER] Starting processing...
Validation: Empty data rejected.
Result: Validation failed.
process() is final — it controls the algorithm flow and cannot be overridden. validate(), transform(), and postProcess() are abstract or hook methods — each subclass fills them in. The caller uses DataProcessor references without knowing whether it is working with JsonSanitiser or CsvNormaliser. This is the Template Method Pattern in full production form.
Abstract Classes and Constructors
Abstract classes can have constructors — and they must, when they hold fields that subclasses need to initialise.
// File: BaseReport.java
public abstract class BaseReport {
private final String reportId;
private final String preparedBy;
private final java.time.LocalDate reportDate;
// Protected constructor — accessible to subclasses via super()
protected BaseReport(String reportId, String preparedBy) {
if (reportId == null || reportId.isBlank()) {
throw new IllegalArgumentException("Report ID required.");
}
this.reportId = reportId;
this.preparedBy = preparedBy;
this.reportDate = java.time.LocalDate.now();
}
public String getReportId() { return reportId; }
public String getPreparedBy() { return preparedBy; }
protected String getReportHeader() {
return "Report ID : " + reportId
+ "\nPrepared by: " + preparedBy
+ "\nDate : " + reportDate;
}
// Abstract — each report type provides its own body
public abstract String generateBody();
// Concrete template — assembles header + body
public final String generate() {
return "=".repeat(40) + "\n"
+ getReportHeader() + "\n"
+ "=".repeat(40) + "\n"
+ generateBody();
}
}// File: SalesReport.java
public class SalesReport extends BaseReport {
private final double totalRevenue;
private final int totalOrders;
private final String region;
public SalesReport(String reportId, String preparedBy,
double totalRevenue, int totalOrders, String region) {
super(reportId, preparedBy); // initialises BaseReport fields
this.totalRevenue = totalRevenue;
this.totalOrders = totalOrders;
this.region = region;
}
@Override
public String generateBody() {
double avgOrder = totalOrders > 0 ? totalRevenue / totalOrders : 0;
return "Region : " + region
+ "\nTotal orders : " + totalOrders
+ "\nTotal revenue: Rs." + String.format("%.2f", totalRevenue)
+ "\nAvg order : Rs." + String.format("%.2f", avgOrder);
}
}// File: ConstructorDemo.java
public class ConstructorDemo {
public static void main(String[] args) {
SalesReport report = new SalesReport(
"RPT-2024-001", "Ananya Krishnan",
485000.0, 320, "South India"
);
System.out.println(report.generate());
}
}Output:
========================================
Report ID : RPT-2024-001
Prepared by: Ananya Krishnan
Date : 2024-01-15
========================================
Region : South India
Total orders : 320
Total revenue: Rs.485000.00
Avg order : Rs.1515.63
The abstract class BaseReport holds the reportId, preparedBy, and reportDate fields — these are common to every report. Its constructor validates and initialises them. SalesReport calls super(reportId, preparedBy) to delegate this initialisation. The abstract class constructor cannot be called directly — it is always via super() from a subclass.
Abstract Method Rules
| Rule | Description |
|---|---|
abstract keyword | Required in the method declaration |
| No body | Cannot have {} — not even empty braces |
| Must be overridden | Every concrete subclass must implement all abstract methods |
Cannot be private | Private methods are not visible to subclasses — cannot be overridden |
Cannot be static | Static methods belong to the class — no override mechanism |
Cannot be final | final prevents overriding — contradicts the purpose of abstract |
| Cannot be in regular class | Abstract methods can only exist in abstract classes or interfaces |
| Subclass can be abstract too | A subclass that does not implement all abstract methods must itself be abstract |
When Abstract Class — When Interface
| Scenario | Use Abstract Class | Use Interface |
|---|---|---|
| Shared fields needed | Yes — instance fields are allowed | No — only constants |
| Shared constructor logic | Yes | No — no constructors |
| Shared concrete method logic | Yes | Only via default methods |
| Multiple inheritance needed | No — single extends | Yes — multiple implements |
| Pure contract / capability | No — overhead of class | Yes — cleaner |
| IS-A type hierarchy | Yes — when hierarchy makes sense | Yes — when capability-based |
| State required | Yes | No |
| Evolving an existing API | Yes — add methods freely | Carefully — breaks non-default implementations |
| Framework hooks (Template Method) | Yes — natural fit | Awkward |
| Lightweight role definition | No | Yes |
Abstract Class — What Can and Cannot Be Done
| Feature | Allowed | Notes |
|---|---|---|
abstract methods | Yes | Must be implemented by concrete subclasses |
| Concrete methods | Yes | Inherited by subclasses |
| Instance fields | Yes | Encapsulated in the abstract class |
| Static fields | Yes | Shared class-level data |
| Constructors | Yes | Called via super() from subclass |
static abstract method | No | Contradictory — static has no dispatch |
private abstract method | No | Private methods not visible to subclasses |
final abstract method | No | final prevents override; abstract requires it |
Instantiation with new | No | Compile-time error |
| Partial implementation | Yes | Can implement some but not all abstract methods |
| Implement interface | Yes | Abstract class can implement interface, leave methods abstract |
Basic Example
A simple Animal abstract class holds a shared name field and a concrete describe() method, while leaving the actual sound each animal makes as an abstract method for subclasses to fill in.
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("Rex");
Animal cat = new Cat("Whiskers");
dog.describe();
cat.describe();
}
}
abstract class Animal {
private final String name;
Animal(String name) {
this.name = name;
}
abstract String makeSound();
void describe() {
System.out.println(name + " says " + makeSound());
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
String makeSound() {
return "Woof";
}
}
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
String makeSound() {
return "Meow";
}
}Output
Rex says Woof Whiskers says Meow
How it works:
- ›
Animalcannot be instantiated directly —new Animal("X")would not compile — but it still holds the sharednamefield and constructor logic every subclass needs. - ›
makeSound()has no body inAnimal; it is a promise that every concrete subclass must keep. - ›
DogandCateach provide their ownmakeSound(), and the shareddescribe()method calls it polymorphically —describe()never needs to know which subclass it is running on.
Best Practices
Mark the template method as final
The template method defines the algorithm flow — it should not be overridable by subclasses. Making it final enforces the invariant that the sequence of steps never changes, only the steps themselves. A subclass that changes the flow breaks every caller that depends on the guaranteed sequence.
Minimise the number of abstract methods
Every abstract method is a burden on every concrete subclass — including test doubles. Keep the abstract contract as small as possible. If a step has a reasonable default, make it a hook method with a default implementation rather than abstract. Subclasses override only when they need different behaviour.
Use protected for abstract and hook methods — not public
Abstract and hook methods are implementation details of the algorithm. They should be callable from within the abstract class and overridable by subclasses — but not directly invocable by external callers. Declare them protected. External callers should only use the public template method.
Declare the abstract class with a protected constructor when possible
An abstract class with a public constructor signals that external callers can call it — but they cannot (abstract classes cannot be instantiated). Using protected is more accurate: only subclass constructors call it through super(). This prevents the misleading implication that direct instantiation is possible.
Common Mistakes
Mistake 1 — Trying to Instantiate an Abstract Class
public abstract class Vehicle {
public abstract String getType();
}
public class Main {
public static void main(String[] args) {
Vehicle v = new Vehicle(); // compile error
}
}Compile error: Vehicle is abstract; cannot be instantiated
Abstract classes exist to be extended, not instantiated. To get an object, create a concrete subclass. To declare a variable, use the abstract type — Vehicle v = new Car(...) is valid; new Vehicle() is not.
Mistake 2 — Making Abstract Methods private or static
public abstract class Processor {
// Compile error — abstract and private are contradictory
private abstract void process(); // private means not visible to subclass
// Compile error — abstract and static are contradictory
public static abstract void reset(); // static methods cannot be overridden
}Compile error: illegal combination of modifiers: abstract and private Compile error: illegal combination of modifiers: abstract and static
Abstract methods require subclasses to override them — which requires the methods to be visible (private prevents this) and to participate in dynamic dispatch (static prevents this). Neither combination makes logical sense.
Mistake 3 — Not Implementing All Abstract Methods in a Concrete Subclass
public abstract class Animal {
public abstract String sound();
public abstract String habitat();
}
// Missing habitat() — compile error unless class is also abstract
public class Dog extends Animal {
@Override
public String sound() { return "Woof"; }
// habitat() not implemented — compiler error
}Compile error: Dog is not abstract and does not override abstract method habitat() in Animal
Every concrete subclass must implement every abstract method from the entire parent chain. If a subclass cannot implement all of them, declare it abstract too — a concrete grandchild will eventually complete the chain.
Mistake 4 — Choosing Abstract Class When Interface Is Correct
// Wrong — no shared state, no shared implementation
// Should be an interface
public abstract class Serialisable {
public abstract byte[] serialise();
public abstract void deserialise(byte[] data);
// No fields, no concrete methods — an interface is cleaner and more flexible
}
// Correct — interface allows multiple implementation and imposes no class hierarchy
public interface Serialisable {
byte[] serialise();
void deserialise(byte[] data);
}When an abstract class has only abstract methods and no fields or concrete logic, it is functionally identical to an interface — but more restrictive, because implementing classes lose their single extends slot. Choose an interface unless you genuinely need shared fields or concrete method implementations.
Interview Questions
Q1. What is an abstract class in Java and when should you use it?
An abstract class is a class declared with abstract that cannot be instantiated. It can contain abstract methods — which subclasses must implement — and concrete methods that subclasses inherit. Use an abstract class when multiple classes share common fields, common constructor logic, or shared concrete method implementations, but differ in certain specific behaviours. If no shared implementation or state is needed, an interface is the cleaner choice.
Q2. What is the difference between an abstract class and an interface in Java?
An abstract class can have instance fields, constructors, and both abstract and concrete methods — but a class can extend only one. An interface defines abstract method signatures, can have default and static methods since Java 8, has no instance fields or constructors, and a class can implement multiple interfaces simultaneously. Use an abstract class for partial shared implementation in a type hierarchy. Use an interface for pure contracts, capability definitions, or when multiple type inheritance is needed.
Q3. Can an abstract class have a constructor in Java?
Yes. Abstract classes can have constructors — and they are necessary when the abstract class holds fields that subclasses need to initialise. The constructor is never called directly (since the class cannot be instantiated) but is always called through super() in a subclass constructor. Making the constructor protected is a common practice to signal that it is meant only for subclass use.
Q4. What is the Template Method Pattern and how does it use abstract classes?
The Template Method Pattern defines the skeleton of an algorithm in a final concrete method of an abstract class, deferring variable steps to abstract methods that subclasses implement. The abstract class controls the fixed algorithm sequence; the subclasses fill in the specific behaviours. The template method is final so no subclass can change the flow. This pattern is the canonical use case for abstract classes — fixing the what while leaving the how open for specialisation.
Q5. What is the difference between an abstract method and a hook method?
An abstract method has no body and forces every concrete subclass to provide an implementation — if a subclass does not, it must itself be declared abstract. A hook method is a concrete method in the abstract class with a default implementation that subclasses may optionally override. Hook methods allow customisation without obligation — subclasses that need different behaviour override; others inherit the default. Hook methods reduce the burden on subclasses compared to abstract methods.
Q6. Can an abstract class implement an interface in Java?
Yes. An abstract class can implement an interface without implementing all of the interface's abstract methods — the unimplemented methods become abstract methods of the abstract class, to be fulfilled by its concrete subclasses. This is a common layered design: an interface defines the contract, an abstract class provides shared implementation for some methods, and concrete classes complete the remaining abstract ones.
FAQs
What is an abstract class in Java?
An abstract class is a class declared with the abstract keyword that cannot be instantiated with new. It can contain abstract methods that subclasses must implement, concrete methods that subclasses inherit, fields, and constructors. It is used as a base class when multiple subclasses share some common implementation but differ in specific behaviours.
What is the difference between abstract class and interface?
An abstract class can have instance fields, constructors, and both abstract and concrete methods — a class can extend only one. An interface has no instance fields or constructors, all methods are abstract by default (with default/static methods available since Java 8) — a class can implement many. Use abstract class for shared implementation; use interface for pure contracts or multiple type inheritance.
Can abstract class have concrete methods in Java?
Yes. Abstract classes can have fully implemented concrete methods. These are inherited by all subclasses and represent shared behaviour that does not need to vary. A common pattern is to declare the algorithm template as a concrete final method and the variable steps as abstract.
What happens if a subclass does not implement all abstract methods?
The subclass must itself be declared abstract. It can then be extended by another class that implements the remaining abstract methods. A class cannot be concrete (instantiable) unless all abstract methods in its inheritance chain are implemented.
Can an abstract class have a static method?
Yes. Static methods in an abstract class work exactly like static methods in any class — they belong to the class, not to objects, and are called on the class name. Static methods cannot be abstract because abstraction requires overriding, and static methods are not polymorphic.
What is the difference between abstract method and abstract class?
An abstract method is a method declared with abstract that has no body — subclasses must implement it. An abstract class is a class declared with abstract that cannot be instantiated — it can contain abstract methods. An abstract method can only exist inside an abstract class or interface. An abstract class can exist with zero abstract methods (though this is uncommon) — it would still prevent instantiation but all subclass obligation would come from the concrete methods.
Summary
Abstract classes occupy the middle ground between concrete classes and interfaces — they provide structure through fields and constructors, share implementation through concrete methods, and impose obligations through abstract methods. The Template Method Pattern is their most powerful application: a fixed algorithm in a final concrete method, with variable steps delegated to abstract methods that each subclass fills in independently.
The decision between abstract class and interface comes down to what subclasses need to share. Shared state or shared constructor logic point to an abstract class. A pure contract with no shared implementation points to an interface. When both are needed — shared logic and multiple type flexibility — an abstract class can implement interfaces, leaving abstract methods for concrete subclasses to complete.
For interviews, be ready to explain the difference between abstract class and interface with concrete trade-offs, demonstrate the Template Method Pattern with real code, explain what abstract methods and hook methods are and when to use each, and describe why abstract classes can have constructors. These points appear in both service-based recall and product-based design discussions.
What to Read Next
Learn how to define a contract that a class must follow.
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
Defines a class that mixes an unimplemented method with a ready-to-use one.
Declares a method with no body that every concrete subclass is forced to implement.