Java Tutorial
🔍

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 abstract keyword; 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

AspectRegular ClassAbstract ClassInterface
abstract keywordNoYesNot applicable
InstantiationYes — newNo — cannot instantiateNo — cannot instantiate
Abstract methodsNoYes — abstract keywordYes — by default
Concrete methodsYesYesdefault and static only (Java 8+)
Instance fieldsYesYesNo — only constants
ConstructorsYesYes — called via super()No
Multiple inheritanceNo — extends oneNo — extends oneYes — implements many
Access modifiersAnyAnypublic by default
When to useComplete implementationPartial implementation + shared logicPure contract + capability definition

Declaring an Abstract Class

Java
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}
Java
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}
Java
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}
Java
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.

Java
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}
Java
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}
Java
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}
Java
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.

Java
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}
Java
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}
Java
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

RuleDescription
abstract keywordRequired in the method declaration
No bodyCannot have {} — not even empty braces
Must be overriddenEvery concrete subclass must implement all abstract methods
Cannot be privatePrivate methods are not visible to subclasses — cannot be overridden
Cannot be staticStatic methods belong to the class — no override mechanism
Cannot be finalfinal prevents overriding — contradicts the purpose of abstract
Cannot be in regular classAbstract methods can only exist in abstract classes or interfaces
Subclass can be abstract tooA subclass that does not implement all abstract methods must itself be abstract

When Abstract Class — When Interface

ScenarioUse Abstract ClassUse Interface
Shared fields neededYes — instance fields are allowedNo — only constants
Shared constructor logicYesNo — no constructors
Shared concrete method logicYesOnly via default methods
Multiple inheritance neededNo — single extendsYes — multiple implements
Pure contract / capabilityNo — overhead of classYes — cleaner
IS-A type hierarchyYes — when hierarchy makes senseYes — when capability-based
State requiredYesNo
Evolving an existing APIYes — add methods freelyCarefully — breaks non-default implementations
Framework hooks (Template Method)Yes — natural fitAwkward
Lightweight role definitionNoYes

Abstract Class — What Can and Cannot Be Done

FeatureAllowedNotes
abstract methodsYesMust be implemented by concrete subclasses
Concrete methodsYesInherited by subclasses
Instance fieldsYesEncapsulated in the abstract class
Static fieldsYesShared class-level data
ConstructorsYesCalled via super() from subclass
static abstract methodNoContradictory — static has no dispatch
private abstract methodNoPrivate methods not visible to subclasses
final abstract methodNofinal prevents override; abstract requires it
Instantiation with newNoCompile-time error
Partial implementationYesCan implement some but not all abstract methods
Implement interfaceYesAbstract 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

Java
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}
Java
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}
Java
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}
Java
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}
Java
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}
Java
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

Java
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

Java
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

Java
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

Java
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

TopicLink
How interfaces define pure contracts that abstract classes can implementInterfaces →
How polymorphism uses abstract class references for runtime method dispatchPolymorphism →
How inheritance determines which abstract methods a subclass must implementInheritance →
How abstraction uses both abstract classes and interfaces to hide complexityAbstraction →
How encapsulation in abstract classes protects shared fields from subclassesEncapsulation →
Java Abstract Classes | DevStackFlow