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
1// File: Shape.java
2
3public abstract class Shape {
4
5 // Instance field — shared by all shapes
6 private final String colour;
7
8 // Constructor — called by subclass constructors via super()
9 public Shape(String colour) {
10 if (colour == null || colour.isBlank()) {
11 throw new IllegalArgumentException("Colour is required.");
12 }
13 this.colour = colour;
14 }
15
16 public String getColour() { return colour; }
17
18 // Abstract methods — every subclass MUST implement these
19 public abstract double area();
20 public abstract double perimeter();
21
22 // Concrete method — shared across all shapes, no override needed
23 public String describe() {
24 return getClass().getSimpleName()
25 + " | Colour: " + colour
26 + " | Area: " + String.format("%.2f", area())
27 + " | Perimeter: " + String.format("%.2f", perimeter());
28 }
29}1// File: Circle.java
2
3public class Circle extends Shape {
4
5 private final double radius;
6
7 public Circle(double radius, String colour) {
8 super(colour); // calls Shape constructor
9 if (radius <= 0) throw new IllegalArgumentException("Radius must be positive.");
10 this.radius = radius;
11 }
12
13 public double getRadius() { return radius; }
14
15 @Override
16 public double area() {
17 return Math.PI * radius * radius;
18 }
19
20 @Override
21 public double perimeter() {
22 return 2 * Math.PI * radius;
23 }
24}1// File: Rectangle.java
2
3public class Rectangle extends Shape {
4
5 private final double width;
6 private final double height;
7
8 public Rectangle(double width, double height, String colour) {
9 super(colour);
10 if (width <= 0 || height <= 0) {
11 throw new IllegalArgumentException("Width and height must be positive.");
12 }
13 this.width = width;
14 this.height = height;
15 }
16
17 public double getWidth() { return width; }
18 public double getHeight() { return height; }
19
20 @Override
21 public double area() {
22 return width * height;
23 }
24
25 @Override
26 public double perimeter() {
27 return 2 * (width + height);
28 }
29}1// File: ShapeDemo.java
2
3import java.util.List;
4
5public class ShapeDemo {
6
7 public static void main(String[] args) {
8
9 // Cannot do: new Shape("Red") — compile error
10 List<Shape> shapes = List.of(
11 new Circle(7.0, "Blue"),
12 new Rectangle(5.0, 3.0, "Red"),
13 new Circle(4.5, "Green")
14 );
15
16 for (Shape shape : shapes) {
17 System.out.println(shape.describe()); // concrete method — runtime dispatch
18 }
19
20 System.out.println("\nTotal area: "
21 + String.format("%.2f", shapes.stream()
22 .mapToDouble(Shape::area)
23 .sum()));
24 }
25}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.
1// File: DataProcessor.java
2
3public abstract class DataProcessor {
4
5 // Template method — final so subclasses cannot change the flow
6 public final ProcessingResult process(String rawData) {
7 System.out.println("[" + getProcessorName() + "] Starting processing...");
8
9 // Step 1 — validate input (subclass decides validation rules)
10 if (!validate(rawData)) {
11 return new ProcessingResult(false, "Validation failed.", null);
12 }
13
14 // Step 2 — transform data (subclass decides how)
15 String transformed = transform(rawData);
16
17 // Step 3 — post-process (optional — default is identity)
18 String finalData = postProcess(transformed);
19
20 System.out.println("[" + getProcessorName() + "] Done. Output length: "
21 + finalData.length() + " chars.");
22
23 return new ProcessingResult(true, "Success", finalData);
24 }
25
26 // Abstract steps — subclasses define these
27 protected abstract String getProcessorName();
28 protected abstract boolean validate(String data);
29 protected abstract String transform(String data);
30
31 // Hook method — default behaviour provided, subclass may override
32 protected String postProcess(String data) {
33 return data; // default: no post-processing
34 }
35
36 // Result holder
37 public record ProcessingResult(boolean success, String message, String output) {}
38}1// File: JsonSanitiser.java
2
3public class JsonSanitiser extends DataProcessor {
4
5 @Override
6 protected String getProcessorName() {
7 return "JSON-SANITISER";
8 }
9
10 @Override
11 protected boolean validate(String data) {
12 if (data == null || data.isBlank()) {
13 System.out.println(" Validation: Empty data rejected.");
14 return false;
15 }
16 if (!data.trim().startsWith("{") && !data.trim().startsWith("[")) {
17 System.out.println(" Validation: Not valid JSON structure.");
18 return false;
19 }
20 System.out.println(" Validation: OK");
21 return true;
22 }
23
24 @Override
25 protected String transform(String data) {
26 // Strips newlines and extra spaces for compact JSON
27 String compact = data.replaceAll("\\s+", " ").trim();
28 System.out.println(" Transform: Compacted JSON.");
29 return compact;
30 }
31
32 @Override
33 protected String postProcess(String data) {
34 // Wraps in a standard envelope
35 return "{\"data\":" + data + ",\"sanitised\":true}";
36 }
37}1// File: CsvNormaliser.java
2
3public class CsvNormaliser extends DataProcessor {
4
5 private final char delimiter;
6
7 public CsvNormaliser(char delimiter) {
8 this.delimiter = delimiter;
9 }
10
11 @Override
12 protected String getProcessorName() {
13 return "CSV-NORMALISER";
14 }
15
16 @Override
17 protected boolean validate(String data) {
18 if (data == null || data.isBlank()) {
19 System.out.println(" Validation: Empty CSV rejected.");
20 return false;
21 }
22 long lineCount = data.lines().count();
23 System.out.println(" Validation: " + lineCount + " lines found. OK");
24 return true;
25 }
26
27 @Override
28 protected String transform(String data) {
29 // Converts all lines to uppercase and trims each field
30 String normalised = data.lines()
31 .map(line -> java.util.Arrays.stream(line.split(String.valueOf(delimiter)))
32 .map(String::trim)
33 .map(String::toUpperCase)
34 .reduce((a, b) -> a + delimiter + b)
35 .orElse(""))
36 .reduce((a, b) -> a + "\n" + b)
37 .orElse("");
38 System.out.println(" Transform: Normalised to uppercase.");
39 return normalised;
40 }
41 // postProcess not overridden — uses default (identity)
42}1// File: TemplateMethodDemo.java
2
3public class TemplateMethodDemo {
4
5 public static void main(String[] args) {
6
7 DataProcessor jsonProcessor = new JsonSanitiser();
8 DataProcessor csvProcessor = new CsvNormaliser(',');
9
10 System.out.println("=== JSON Sanitiser ===");
11 DataProcessor.ProcessingResult jsonResult = jsonProcessor.process(
12 "{ \"orderId\" : \"ORD-001\" , \"amount\" : 1499 }"
13 );
14 System.out.println("Result: " + jsonResult.message());
15 System.out.println("Output: " + jsonResult.output());
16
17 System.out.println("\n=== CSV Normaliser ===");
18 DataProcessor.ProcessingResult csvResult = csvProcessor.process(
19 " sku-001 , laptop , 85000\n sku-002 , mouse , 1299 "
20 );
21 System.out.println("Result: " + csvResult.message());
22 System.out.println("Output:\n" + csvResult.output());
23
24 System.out.println("\n=== Invalid Input ===");
25 DataProcessor.ProcessingResult invalid = jsonProcessor.process("");
26 System.out.println("Result: " + invalid.message());
27 }
28}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.
1// File: BaseReport.java
2
3public abstract class BaseReport {
4
5 private final String reportId;
6 private final String preparedBy;
7 private final java.time.LocalDate reportDate;
8
9 // Protected constructor — accessible to subclasses via super()
10 protected BaseReport(String reportId, String preparedBy) {
11 if (reportId == null || reportId.isBlank()) {
12 throw new IllegalArgumentException("Report ID required.");
13 }
14 this.reportId = reportId;
15 this.preparedBy = preparedBy;
16 this.reportDate = java.time.LocalDate.now();
17 }
18
19 public String getReportId() { return reportId; }
20 public String getPreparedBy() { return preparedBy; }
21
22 protected String getReportHeader() {
23 return "Report ID : " + reportId
24 + "\nPrepared by: " + preparedBy
25 + "\nDate : " + reportDate;
26 }
27
28 // Abstract — each report type provides its own body
29 public abstract String generateBody();
30
31 // Concrete template — assembles header + body
32 public final String generate() {
33 return "=".repeat(40) + "\n"
34 + getReportHeader() + "\n"
35 + "=".repeat(40) + "\n"
36 + generateBody();
37 }
38}1// File: SalesReport.java
2
3public class SalesReport extends BaseReport {
4
5 private final double totalRevenue;
6 private final int totalOrders;
7 private final String region;
8
9 public SalesReport(String reportId, String preparedBy,
10 double totalRevenue, int totalOrders, String region) {
11 super(reportId, preparedBy); // initialises BaseReport fields
12 this.totalRevenue = totalRevenue;
13 this.totalOrders = totalOrders;
14 this.region = region;
15 }
16
17 @Override
18 public String generateBody() {
19 double avgOrder = totalOrders > 0 ? totalRevenue / totalOrders : 0;
20 return "Region : " + region
21 + "\nTotal orders : " + totalOrders
22 + "\nTotal revenue: Rs." + String.format("%.2f", totalRevenue)
23 + "\nAvg order : Rs." + String.format("%.2f", avgOrder);
24 }
25}1// File: ConstructorDemo.java
2
3public class ConstructorDemo {
4
5 public static void main(String[] args) {
6
7 SalesReport report = new SalesReport(
8 "RPT-2024-001", "Ananya Krishnan",
9 485000.0, 320, "South India"
10 );
11
12 System.out.println(report.generate());
13 }
14}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 |
Real-World Example — Payment Processing Pipeline
The Business Problem
You are building the payment processing framework for a fintech platform — similar to what Razorpay or PayU uses internally for its multi-gateway architecture. Every payment — UPI, card, wallet — goes through the same pipeline: validate the request, authorise with the gateway, record the transaction, and notify the customer. The steps are the same; the implementation of each step differs by payment type. The abstract class holds the fixed pipeline; each subclass provides the variable step implementations.
Implementation
1// File: PaymentConfig.java
2
3public final class PaymentConfig {
4
5 public static final double MAX_SINGLE_TRANSACTION = 200000.0;
6 public static final String SUCCESS_STATUS = "SUCCESS";
7 public static final String FAILED_STATUS = "FAILED";
8
9 private PaymentConfig() {}
10}1// File: PaymentResult.java
2
3public class PaymentResult {
4
5 private final String transactionId;
6 private final String status;
7 private final String message;
8 private final double amount;
9
10 public PaymentResult(String transactionId, String status,
11 String message, double amount) {
12 this.transactionId = transactionId;
13 this.status = status;
14 this.message = message;
15 this.amount = amount;
16 }
17
18 public String getTransactionId() { return transactionId; }
19 public String getStatus() { return status; }
20 public String getMessage() { return message; }
21 public double getAmount() { return amount; }
22
23 @Override
24 public String toString() {
25 return transactionId + " | " + status + " | Rs." + amount
26 + " | " + message;
27 }
28}1// File: AbstractPaymentProcessor.java
2
3public abstract class AbstractPaymentProcessor {
4
5 private final String processorName;
6 private int transactionCount;
7
8 protected AbstractPaymentProcessor(String processorName) {
9 this.processorName = processorName;
10 this.transactionCount = 0;
11 }
12
13 public String getProcessorName() { return processorName; }
14 public int getTransactionCount(){ return transactionCount; }
15
16 // TEMPLATE METHOD — the fixed pipeline, cannot be overridden
17 public final PaymentResult execute(String orderId, double amount) {
18
19 String txnId = processorName + "-TXN-" + (++transactionCount);
20 System.out.println("\n[" + processorName + "] Processing " + txnId);
21
22 // Step 1 — common validation (abstract: subclass defines rules)
23 if (!validateRequest(orderId, amount)) {
24 return new PaymentResult(txnId, PaymentConfig.FAILED_STATUS,
25 "Validation failed.", amount);
26 }
27
28 // Step 2 — gateway authorization (abstract: different per gateway)
29 boolean authorised = authorise(txnId, amount);
30 if (!authorised) {
31 return new PaymentResult(txnId, PaymentConfig.FAILED_STATUS,
32 "Authorisation failed.", amount);
33 }
34
35 // Step 3 — record transaction (concrete: same for all)
36 recordTransaction(txnId, orderId, amount);
37
38 // Step 4 — notify customer (hook: optional, subclass may override)
39 notifyCustomer(orderId, txnId, amount);
40
41 return new PaymentResult(txnId, PaymentConfig.SUCCESS_STATUS,
42 "Payment completed.", amount);
43 }
44
45 // Abstract steps — each subclass provides its own implementation
46 protected abstract boolean validateRequest(String orderId, double amount);
47 protected abstract boolean authorise(String txnId, double amount);
48
49 // Concrete step — shared across all payment types
50 private void recordTransaction(String txnId, String orderId, double amount) {
51 System.out.println(" [DB] Recorded: " + txnId
52 + " | Order: " + orderId
53 + " | Rs." + amount);
54 }
55
56 // Hook method — default notification; subclass may override
57 protected void notifyCustomer(String orderId, String txnId, double amount) {
58 System.out.println(" [NOTIFY] Default notification sent for " + orderId);
59 }
60}1// File: UpiPaymentProcessor.java
2
3public class UpiPaymentProcessor extends AbstractPaymentProcessor {
4
5 private final String upiHandle;
6
7 public UpiPaymentProcessor(String upiHandle) {
8 super("UPI");
9 this.upiHandle = upiHandle;
10 }
11
12 @Override
13 protected boolean validateRequest(String orderId, double amount) {
14 if (!upiHandle.contains("@")) {
15 System.out.println(" [VALIDATE] Invalid UPI handle: " + upiHandle);
16 return false;
17 }
18 if (amount <= 0 || amount > PaymentConfig.MAX_SINGLE_TRANSACTION) {
19 System.out.println(" [VALIDATE] Amount out of range: Rs." + amount);
20 return false;
21 }
22 System.out.println(" [VALIDATE] UPI handle " + upiHandle + " OK");
23 return true;
24 }
25
26 @Override
27 protected boolean authorise(String txnId, double amount) {
28 // Simulates UPI gateway call
29 System.out.println(" [AUTH] UPI gateway call for Rs." + amount
30 + " from " + upiHandle);
31 return true; // simulated success
32 }
33
34 @Override
35 protected void notifyCustomer(String orderId, String txnId, double amount) {
36 System.out.println(" [NOTIFY] UPI SMS: 'Rs." + amount
37 + " debited from " + upiHandle
38 + ". Ref: " + txnId + "'");
39 }
40}1// File: CardPaymentProcessor.java
2
3public class CardPaymentProcessor extends AbstractPaymentProcessor {
4
5 private final String maskedCard;
6 private final String cardNetwork;
7 private final double creditLimit;
8
9 public CardPaymentProcessor(String maskedCard,
10 String cardNetwork, double creditLimit) {
11 super("CARD");
12 this.maskedCard = maskedCard;
13 this.cardNetwork = cardNetwork;
14 this.creditLimit = creditLimit;
15 }
16
17 @Override
18 protected boolean validateRequest(String orderId, double amount) {
19 if (amount > creditLimit) {
20 System.out.println(" [VALIDATE] Amount Rs." + amount
21 + " exceeds credit limit Rs." + creditLimit);
22 return false;
23 }
24 if (amount <= 0) {
25 System.out.println(" [VALIDATE] Invalid amount.");
26 return false;
27 }
28 System.out.println(" [VALIDATE] " + cardNetwork + " card "
29 + maskedCard + " OK");
30 return true;
31 }
32
33 @Override
34 protected boolean authorise(String txnId, double amount) {
35 System.out.println(" [AUTH] " + cardNetwork
36 + " network authorisation for Rs." + amount);
37 return amount <= creditLimit; // fails if over limit
38 }
39 // Uses default notifyCustomer() — no override
40}1// File: PaymentPipelineDemo.java
2
3import java.util.List;
4
5public class PaymentPipelineDemo {
6
7 public static void main(String[] args) {
8
9 AbstractPaymentProcessor upiProcessor = new UpiPaymentProcessor("rahul@upi");
10 AbstractPaymentProcessor cardProcessor = new CardPaymentProcessor(
11 "**** **** **** 4521", "VISA", 50000.0
12 );
13
14 List<AbstractPaymentProcessor> processors = List.of(upiProcessor, cardProcessor);
15
16 System.out.println("=== Payment Pipeline Demo ===");
17
18 PaymentResult r1 = upiProcessor.execute("ORD-101", 1499.0);
19 System.out.println(" Result: " + r1);
20
21 PaymentResult r2 = cardProcessor.execute("ORD-102", 8999.0);
22 System.out.println(" Result: " + r2);
23
24 // UPI amount exceeds limit
25 PaymentResult r3 = upiProcessor.execute("ORD-103", 250000.0);
26 System.out.println(" Result: " + r3);
27
28 System.out.println("\nTransaction counts:");
29 for (AbstractPaymentProcessor p : processors) {
30 System.out.println(" " + p.getProcessorName()
31 + ": " + p.getTransactionCount() + " transaction(s)");
32 }
33 }
34}Output:
=== Payment Pipeline Demo ===
[UPI] Processing UPI-TXN-1
[VALIDATE] UPI handle rahul@upi OK
[AUTH] UPI gateway call for Rs.1499.0 from rahul@upi
[DB] Recorded: UPI-TXN-1 | Order: ORD-101 | Rs.1499.0
[NOTIFY] UPI SMS: 'Rs.1499.0 debited from rahul@upi. Ref: UPI-TXN-1'
Result: UPI-TXN-1 | SUCCESS | Rs.1499.0 | Payment completed.
[CARD] Processing CARD-TXN-1
[VALIDATE] VISA card **** **** **** 4521 OK
[AUTH] VISA network authorisation for Rs.8999.0
[DB] Recorded: CARD-TXN-1 | Order: ORD-102 | Rs.8999.0
[NOTIFY] Default notification sent for ORD-102
Result: CARD-TXN-1 | SUCCESS | Rs.8999.0 | Payment completed.
[UPI] Processing UPI-TXN-2
[VALIDATE] Amount out of range: Rs.250000.0
Result: UPI-TXN-2 | FAILED | Rs.250000.0 | Validation failed.
Transaction counts:
UPI: 2 transaction(s)
CARD: 1 transaction(s)
The pipeline steps — validate, authorise, record, notify — are always the same. The abstract class controls the sequence through a final template method. Each subclass provides its gateway-specific implementation. The recordTransaction() method is private — it never changes and no subclass should touch it. The notifyCustomer() hook has a default — CardPaymentProcessor skips overriding it and gets the default notification.
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
1public abstract class Vehicle {
2 public abstract String getType();
3}
4
5public class Main {
6 public static void main(String[] args) {
7 Vehicle v = new Vehicle(); // compile error
8 }
9}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
1public abstract class Processor {
2
3 // Compile error — abstract and private are contradictory
4 private abstract void process(); // private means not visible to subclass
5
6 // Compile error — abstract and static are contradictory
7 public static abstract void reset(); // static methods cannot be overridden
8}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
1public abstract class Animal {
2 public abstract String sound();
3 public abstract String habitat();
4}
5
6// Missing habitat() — compile error unless class is also abstract
7public class Dog extends Animal {
8 @Override
9 public String sound() { return "Woof"; }
10 // habitat() not implemented — compiler error
11}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
1// Wrong — no shared state, no shared implementation
2// Should be an interface
3public abstract class Serialisable {
4 public abstract byte[] serialise();
5 public abstract void deserialise(byte[] data);
6 // No fields, no concrete methods — an interface is cleaner and more flexible
7}
8
9// Correct — interface allows multiple implementation and imposes no class hierarchy
10public interface Serialisable {
11 byte[] serialise();
12 void deserialise(byte[] data);
13}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
| Topic | Link |
|---|---|
| How interfaces define pure contracts that abstract classes can implement | Interfaces → |
| How polymorphism uses abstract class references for runtime method dispatch | Polymorphism → |
| How inheritance determines which abstract methods a subclass must implement | Inheritance → |
| How abstraction uses both abstract classes and interfaces to hide complexity | Abstraction → |
| How encapsulation in abstract classes protects shared fields from subclasses | Encapsulation → |