☕ Java

Java Abstract Class & Concrete Class — Syntax, Rules, Examples & Best Practices

Everything you need to know about Java abstract and concrete classes — abstract methods, rules, constructors in abstract classes, abstract vs interface, abstract vs concrete, template method pattern, anti-patterns, and real-world production code examples.

📅

Last Updated

March 2026

⏱️

Read Time

22 min

🎯

Level

Beginner

🏷️

Chapter

22 of 35

What is Abstraction in Java?

Abstraction is one of the four pillars of Object-Oriented Programming. It is the principle of hiding implementation complexity and exposing only the essential interface to the outside world. Abstraction answers the question: "What does this thing do?" — while hiding "How does it do it?".

Real-world analogy: when you drive a car, you interact with the steering wheel, accelerator, and brakes — the abstract interface. You do not need to understand the internal combustion engine, gear transmission, or anti-lock braking system underneath. The car exposes WHAT you can do (steer, accelerate, brake), not HOW it works internally. Java implements abstraction through two mechanisms: abstract classes and interfaces.

What is an Abstract Class in Java?

An abstract class in Java is a class declared with the abstract keyword. It represents an incomplete design — it may define some behavior (concrete methods, fields, constructors) but leaves certain behaviors undefined (abstract methods) for subclasses to provide. Because it is incomplete, an abstract class cannot be instantiated directly. It exists solely to be extended.

Abstract classes are the ideal tool when you have a family of related classes that share common state and behavior, but differ in specific operations. For example: Shape is abstract — all shapes have a color and a position (shared state), and all shapes can be drawn (shared behavior), but the specific calculation of area differs per shape type. Circle, Rectangle, and Triangle are concrete subclasses that implement calculateArea() in their own way.

📌
Syntax

public abstract class ClassName { // Fields (instance variables) — allowed // Constructors — allowed // Concrete methods — allowed // Abstract methods — allowed (and the point!) public abstract returnType methodName(params); }

📋
What It Can Contain

1. Instance variables (fields) — shared state for all subclasses. 2. Constructors — initialized via super() from subclasses. 3. Concrete (non-abstract) methods — shared behavior. 4. Abstract methods — contract that subclasses MUST implement. 5. Static methods and static variables — fully supported. 6. final methods — shared behavior that CANNOT be overridden.

🔁
How It Works

Abstract class defined → Extended by concrete subclass → Subclass MUST implement all abstract methods → Concrete subclass can be instantiated → Objects accessed through abstract class reference for polymorphism.

☕ JavaAbstractClassBasics.java
// Abstract class — cannot be instantiated
public abstract class Shape {

    // ✅ Instance fields — shared state
    protected String color;
    protected String name;

    // ✅ Constructor — called via super() from subclasses
    public Shape(String name, String color) {
        this.name  = name;
        this.color = color;
    }

    // ✅ Abstract method — contract: every subclass MUST implement this
    public abstract double calculateArea();

    // ✅ Abstract method — another contract
    public abstract double calculatePerimeter();

    // ✅ Concrete method — shared behavior for ALL shapes
    public void display() {
        System.out.println(name + " [" + color + "]"
            + " | Area: " + String.format("%.2f", calculateArea())
            + " | Perimeter: " + String.format("%.2f", calculatePerimeter()));
    }

    // ✅ Concrete getter — shared utility
    public String getColor() { return color; }
    public String getName()  { return name;  }
}

// ❌ Cannot instantiate abstract class
// Shape s = new Shape("Shape", "Red"); // COMPILE ERROR

// ✅ Can use as a reference type (polymorphism)
// Shape s = new Circle(5.0, "Blue"); // OK — Circle is a concrete subclass

What is a Concrete Class in Java?

A concrete class is any class that provides complete implementations for all its methods — including all abstract methods inherited from any abstract parent class or interface — and can therefore be directly instantiated with new. Every ordinary Java class you have written so far is a concrete class. The word 'concrete' implies completeness: nothing is left undefined.

☕ JavaConcreteClasses.java
// ✅ Concrete class extending abstract Shape
public class Circle extends Shape {

    private double radius;

    public Circle(double radius, String color) {
        super("Circle", color); // Calls abstract class constructor
        this.radius = radius;
    }

    // ✅ MUST implement all abstract methods from Shape
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }

    public double getRadius() { return radius; }
}

// ✅ Another concrete class
public class Rectangle extends Shape {

    private double length;
    private double width;

    public Rectangle(double length, double width, String color) {
        super("Rectangle", color);
        this.length = length;
        this.width  = width;
    }

    @Override
    public double calculateArea() {
        return length * width;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * (length + width);
    }
}

class Demo {
    public static void main(String[] args) {

        // ✅ Concrete classes CAN be instantiated
        Circle    c = new Circle(7.0, "Red");
        Rectangle r = new Rectangle(5.0, 3.0, "Blue");

        c.display(); // Circle [Red] | Area: 153.94 | Perimeter: 43.98
        r.display(); // Rectangle [Blue] | Area: 15.00 | Perimeter: 16.00

        // ✅ Polymorphism — abstract class reference holds concrete objects
        Shape[] shapes = { new Circle(4.0, "Green"),
                           new Rectangle(6.0, 2.0, "Yellow") };
        for (Shape shape : shapes) {
            shape.display(); // Calls correct overridden version at runtime
        }
    }
}

Abstract Methods — Rules, Syntax & Purpose

An abstract method is a method declared with the abstract keyword that has no body — just a signature terminated by a semicolon. It defines a contract: every non-abstract subclass must provide its own implementation. Abstract methods are the mechanism through which abstract classes enforce a common API across all concrete subclasses.

Valid Abstract Method Modifiers

public abstract void method(); protected abstract void method(); (package-private) abstract void method(); Abstract methods CAN be: public, protected, or package-private (default). They can declare checked/unchecked exceptions in throws clause.

Invalid Abstract Method Modifiers

private abstract void method(); // ❌ private can't be overridden static abstract void method(); // ❌ static doesn't participate in polymorphism final abstract void method(); // ❌ final prevents overriding — contradicts abstract native abstract void method(); // ❌ native implies a body exists synchronized abstract void m(); // ❌ synchronized implies a body

📋
Abstract Method Rules

1. Can ONLY exist in an abstract class or interface. 2. Has NO body — only signature + semicolon. 3. Every concrete (non-abstract) subclass MUST implement it. 4. If a subclass doesn't implement it, the subclass must be declared abstract. 5. The implementing method in subclass must use @Override. 6. An abstract class can have ZERO abstract methods (valid but unusual).

☕ JavaAbstractMethodRules.java
public abstract class Vehicle {

    protected String brand;
    protected int    year;

    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year  = year;
    }

    // ✅ Abstract methods — contract for all vehicles
    public abstract void startEngine();
    public abstract void stopEngine();
    public abstract double getFuelEfficiency(); // km per litre

    // ✅ Abstract method with throws clause — allowed
    public abstract void refuel(double litres) throws IllegalArgumentException;

    // ✅ Concrete method — shared behavior
    public void printInfo() {
        System.out.println(brand + " (" + year + ") — Efficiency: "
                           + getFuelEfficiency() + " km/L");
    }

    // ❌ These would cause compile errors:
    // private abstract void check();        // private abstract — ERROR
    // static abstract void test();          // static abstract — ERROR
    // final abstract void validate();       // final abstract — ERROR

    // ✅ Abstract class CAN have static methods (not abstract)
    public static String getCategory() {
        return "Vehicle";
    }

    // ✅ Abstract class CAN have final methods
    public final String getBrand() {
        return brand; // Cannot be overridden by any subclass
    }
}

Rules of Abstract Classes — The Complete Ruleset

The Java compiler enforces strict rules for abstract classes. Understanding every rule prevents unexpected compile errors and design mistakes.

1️⃣
Rule 1 — Cannot Be Instantiated

An abstract class CANNOT be instantiated with 'new'. new AbstractClass() is always a compile error. However, an abstract class CAN be used as a reference type for polymorphism: AbstractClass ref = new ConcreteSubclass().

2️⃣
Rule 2 — Subclass Must Implement All Abstract Methods

A concrete (non-abstract) subclass that extends an abstract class MUST provide implementations for ALL abstract methods inherited from the parent. Missing even one abstract method implementation → compile error unless the subclass is also declared abstract.

3️⃣
Rule 3 — Abstract Class CAN Have Zero Abstract Methods

An abstract class is not required to contain any abstract methods — it just needs the 'abstract' keyword on the class declaration. This is valid and useful when you want to prevent direct instantiation of a class while still providing full implementation. Example: a utility base class not intended for direct use.

4️⃣
Rule 4 — Abstract Class CAN Have Constructors

Abstract classes can define constructors. These are not called directly (you cannot new AbstractClass()), but they ARE invoked through super() calls from subclass constructors. They initialize the shared fields declared in the abstract class.

5️⃣
Rule 5 — Abstract Class CAN Have Concrete Methods

Abstract classes can freely mix abstract and concrete methods. Concrete methods provide shared implementation available to all subclasses. This is the key advantage over interfaces (pre-Java 8) — abstract classes can hold real, shared logic.

6️⃣
Rule 6 — Abstract Class CAN Have Static & Final Members

Static methods, static variables, and final methods are all allowed in abstract classes. final methods are inherited but CANNOT be overridden — useful for enforcing invariant behavior across the hierarchy. Static methods belong to the class itself and are not subject to overriding.

7️⃣
Rule 7 — Abstract Class Supports Single Inheritance Only

A class can extend only ONE abstract class (Java's single inheritance rule). To achieve multiple type inheritance, use interfaces. A class can extend one abstract class AND implement multiple interfaces simultaneously.

8️⃣
Rule 8 — Abstract Class Can Extend Another Abstract Class

An abstract class can extend another abstract class without implementing its abstract methods — it passes the obligation down to the first concrete subclass. This enables multi-level abstract hierarchies where each level adds new abstract contracts.

☕ JavaAbstractClassRules.java
// ✅ Abstract class with ZERO abstract methods — valid, prevents instantiation
abstract class BaseLogger {
    protected String prefix;
    public BaseLogger(String prefix) { this.prefix = prefix; }
    public void log(String msg) { System.out.println(prefix + ": " + msg); }
    // No abstract methods — just prevents direct instantiation
}

// ✅ Abstract class extending another abstract class
abstract class Animal {
    protected String name;
    public Animal(String name) { this.name = name; }
    public abstract void makeSound();
    public abstract String getType();
    // Concrete shared behavior
    public void breathe() { System.out.println(name + " is breathing"); }
}

// ✅ Abstract subclass — passes abstract obligation forward
abstract class Pet extends Animal {
    protected String ownerName;
    public Pet(String name, String ownerName) {
        super(name); // Calls Animal constructor
        this.ownerName = ownerName;
    }
    // makeSound() still abstract — not implemented here
    // getType() still abstract — not implemented here
    // Adds new abstract method for this level
    public abstract String getFavoriteFood();
    // Adds new concrete method
    public void showOwner() {
        System.out.println(name + " belongs to " + ownerName);
    }
}

// ✅ Concrete class — MUST implement ALL abstract methods
class Dog extends Pet {
    public Dog(String name, String owner) { super(name, owner); }

    @Override public void   makeSound()       { System.out.println(name + " says: Woof!"); }
    @Override public String getType()         { return "Mammal"; }
    @Override public String getFavoriteFood() { return "Bone"; }
}

class RulesDemo {
    public static void main(String[] args) {
        // ❌ new Animal("X");  — COMPILE ERROR
        // ❌ new Pet("X","Y"); — COMPILE ERROR
        Dog d = new Dog("Bruno", "Rahul");
        d.makeSound();     // Bruno says: Woof!
        d.breathe();       // Bruno is breathing
        d.showOwner();     // Bruno belongs to Rahul
        System.out.println(d.getFavoriteFood()); // Bone

        // ✅ Polymorphic reference
        Animal a = new Dog("Max", "Priya");
        a.makeSound();  // Max says: Woof!  (runtime dispatch)
    }
}

Constructors in Abstract Classes

Abstract classes can have constructors — and they frequently should. Even though an abstract class cannot be instantiated directly, its constructor is called every time a concrete subclass is instantiated, via the super() call in the subclass constructor. Abstract class constructors serve the essential role of initializing the shared fields defined in the abstract class, ensuring every subclass object starts with a valid base state.

☕ JavaAbstractClassConstructor.java
public abstract class BankAccount {

    // Shared fields — initialized by abstract class constructor
    protected String accountNumber;
    protected String holderName;
    protected double balance;

    // ✅ Abstract class constructor — called via super() from subclasses
    public BankAccount(String accountNumber, String holderName, double initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Initial balance cannot be negative");
        }
        this.accountNumber = accountNumber;
        this.holderName    = holderName;
        this.balance       = initialBalance;
        System.out.println("BankAccount constructor called for: " + holderName);
    }

    // ✅ Overloaded constructor in abstract class
    public BankAccount(String accountNumber, String holderName) {
        this(accountNumber, holderName, 0.0); // Delegates to full constructor
    }

    // Abstract methods — subclass-specific behavior
    public abstract void deposit(double amount);
    public abstract void withdraw(double amount);
    public abstract double getInterestRate();

    // Concrete shared method
    public void printStatement() {
        System.out.println("Account: " + accountNumber
            + " | Holder: " + holderName
            + " | Balance: ₹" + String.format("%.2f", balance)
            + " | Rate: " + getInterestRate() + "%");
    }
}

// Concrete subclass — Savings Account
class SavingsAccount extends BankAccount {

    private static final double INTEREST_RATE = 4.0;
    private static final double MIN_BALANCE   = 1000.0;

    public SavingsAccount(String accNo, String holder, double initialBalance) {
        super(accNo, holder, initialBalance); // Calls abstract class constructor
        System.out.println("SavingsAccount constructor called");
    }

    @Override
    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
        balance += amount;
        System.out.println("Deposited ₹" + amount + " | New balance: ₹" + balance);
    }

    @Override
    public void withdraw(double amount) {
        if (balance - amount < MIN_BALANCE) {
            System.out.println("Withdrawal failed — minimum balance ₹" + MIN_BALANCE + " required");
            return;
        }
        balance -= amount;
        System.out.println("Withdrawn ₹" + amount + " | New balance: ₹" + balance);
    }

    @Override
    public double getInterestRate() { return INTEREST_RATE; }
}

class ConstructorDemo {
    public static void main(String[] args) {
        // Constructor chain: SavingsAccount → BankAccount (via super)
        SavingsAccount acc = new SavingsAccount("SB-001", "Meera", 5000.0);
        // Output:
        // BankAccount constructor called for: Meera
        // SavingsAccount constructor called

        acc.deposit(2000.0);   // Deposited ₹2000.0 | New balance: ₹7000.0
        acc.withdraw(500.0);   // Withdrawn ₹500.0  | New balance: ₹6500.0
        acc.printStatement(); // Account: SB-001 | Holder: Meera | Balance: ₹6500.00 | Rate: 4.0%
    }
}

Extending an Abstract Class — The Full Pattern

Extending an abstract class follows the same syntax as extending any class — class ConcreteClass extends AbstractClass. The key obligation: the concrete subclass must implement every abstract method it inherits. Each implementation should use @Override for compiler validation. The subclass also inherits all concrete methods and fields from the abstract parent.

☕ JavaExtendingAbstractClass.java
// Abstract base class for all payment methods
public abstract class PaymentMethod {

    protected String paymentId;
    protected String currency;

    public PaymentMethod(String paymentId, String currency) {
        this.paymentId = paymentId;
        this.currency  = currency;
    }

    // Abstract contracts — each payment type handles these differently
    public abstract boolean processPayment(double amount);
    public abstract boolean refund(double amount, String reason);
    public abstract String  getPaymentType();
    public abstract boolean isAvailable();

    // Concrete shared behavior — same for all payment methods
    public void printReceipt(double amount, boolean success) {
        System.out.println("--- " + getPaymentType() + " Receipt ---");
        System.out.println("Payment ID : " + paymentId);
        System.out.println("Amount     : " + currency + " " + amount);
        System.out.println("Status     : " + (success ? "SUCCESS ✅" : "FAILED ❌"));
    }

    // final method — cannot be overridden
    public final String getCurrency() { return currency; }
}

// ✅ Concrete subclass 1 — UPI
class UPIPayment extends PaymentMethod {

    private String upiId;
    private String bankName;

    public UPIPayment(String paymentId, String upiId, String bankName) {
        super(paymentId, "INR");
        this.upiId    = upiId;
        this.bankName = bankName;
    }

    @Override
    public boolean processPayment(double amount) {
        System.out.println("Processing UPI payment of ₹" + amount
            + " via " + upiId + " (" + bankName + ")");
        printReceipt(amount, true);
        return true;
    }

    @Override
    public boolean refund(double amount, String reason) {
        System.out.println("UPI Refund of ₹" + amount + " | Reason: " + reason);
        return true;
    }

    @Override public String  getPaymentType() { return "UPI"; }
    @Override public boolean isAvailable()    { return true; }
}

// ✅ Concrete subclass 2 — Credit Card
class CreditCardPayment extends PaymentMethod {

    private String cardLastFour;
    private String cardNetwork;

    public CreditCardPayment(String paymentId, String cardLastFour, String cardNetwork) {
        super(paymentId, "INR");
        this.cardLastFour = cardLastFour;
        this.cardNetwork  = cardNetwork;
    }

    @Override
    public boolean processPayment(double amount) {
        System.out.println("Charging ₹" + amount + " to " + cardNetwork
            + " card ending " + cardLastFour);
        printReceipt(amount, true);
        return true;
    }

    @Override
    public boolean refund(double amount, String reason) {
        System.out.println("Credit card refund of ₹" + amount + " | Reason: " + reason);
        return true;
    }

    @Override public String  getPaymentType() { return "Credit Card"; }
    @Override public boolean isAvailable()    { return true; }
}

class PaymentDemo {
    public static void main(String[] args) {

        // Polymorphism — abstract reference, concrete objects
        PaymentMethod[] methods = {
            new UPIPayment("PAY-001", "meera@okaxis", "Axis Bank"),
            new CreditCardPayment("PAY-002", "4567", "VISA")
        };

        for (PaymentMethod method : methods) {
            if (method.isAvailable()) {
                method.processPayment(1500.0);
            }
            System.out.println();
        }
    }
}

Partial Implementation — Abstract Subclass

A subclass that extends an abstract class does not have to implement all abstract methods — provided it declares itself as abstract too. This creates an abstract subclass that provides some implementations while leaving others for further subclasses to complete. This enables multi-level abstraction hierarchies where each layer adds progressively more specificity.

☕ JavaPartialImplementation.java
// Level 1: Fully abstract — defines the full contract
abstract class Employee {
    protected String name;
    protected String employeeId;
    protected double baseSalary;

    public Employee(String name, String id, double baseSalary) {
        this.name       = name;
        this.employeeId = id;
        this.baseSalary = baseSalary;
    }

    public abstract double calculateSalary();    // How salary is computed
    public abstract String getDesignation();     // Job title
    public abstract double calculateBonus();     // Bonus calculation

    public void printPayslip() {
        System.out.println("=== Payslip ===");
        System.out.println("Name        : " + name);
        System.out.println("ID          : " + employeeId);
        System.out.println("Designation : " + getDesignation());
        System.out.println("Net Salary  : ₹" + String.format("%.2f", calculateSalary()));
        System.out.println("Bonus       : ₹" + String.format("%.2f", calculateBonus()));
    }
}

// Level 2: Abstract subclass — partially implements Employee
// Implements getDesignation and calculateBonus, but leaves calculateSalary abstract
abstract class PermanentEmployee extends Employee {

    protected int yearsOfService;

    public PermanentEmployee(String name, String id, double base, int years) {
        super(name, id, base);
        this.yearsOfService = years;
    }

    // ✅ Implements bonus — standard rule for all permanent employees
    @Override
    public double calculateBonus() {
        return baseSalary * 0.10 * (yearsOfService > 5 ? 1.5 : 1.0);
    }

    // ❌ calculateSalary still abstract — deferred to concrete subclasses
    // ❌ getDesignation still abstract — each role has its own title
}

// Level 3: Concrete class — implements ALL remaining abstract methods
class SoftwareEngineer extends PermanentEmployee {

    private int     performanceRating; // 1–5
    private boolean isTeamLead;

    public SoftwareEngineer(String name, String id, double base,
                             int years, int rating, boolean isLead) {
        super(name, id, base, years);
        this.performanceRating = rating;
        this.isTeamLead        = isLead;
    }

    @Override
    public double calculateSalary() {
        double performanceMultiplier = 1.0 + (performanceRating - 3) * 0.05;
        double leadAllowance         = isTeamLead ? 5000.0 : 0.0;
        return baseSalary * performanceMultiplier + leadAllowance;
    }

    @Override
    public String getDesignation() {
        return isTeamLead ? "Senior Software Engineer (Lead)" : "Software Engineer";
    }
}

class PartialImplDemo {
    public static void main(String[] args) {
        SoftwareEngineer se = new SoftwareEngineer("Aryan", "EMP-042",
                                                    60000, 7, 4, true);
        se.printPayslip();
        // === Payslip ===
        // Name        : Aryan
        // ID          : EMP-042
        // Designation : Senior Software Engineer (Lead)
        // Net Salary  : ₹71000.00
        // Bonus       : ₹9000.00
    }
}

Abstract Class vs Interface — When to Use Which

Choosing between an abstract class and an interface is one of the most important OOP design decisions in Java. Both provide abstraction, but they serve different purposes and impose different constraints. With Java 8+ default methods, the boundary has narrowed, but the fundamental distinction remains: abstract class = IS-A relationship with shared state; interface = CAN-DO capability.

CriteriaAbstract ClassInterface
Keywordabstract classinterface
Instantiation❌ Cannot be instantiated❌ Cannot be instantiated
Instance variables✅ Yes — can have any fields❌ No — only public static final constants
Constructors✅ Yes❌ No constructors
Concrete methods✅ Yes — freely mix concrete and abstract⚠️ Only via default/static methods (Java 8+)
Abstract methods✅ Yes (optional)✅ Yes (all non-default methods are implicitly abstract)
Inheritance typeSingle — class extends one abstract class onlyMultiple — class implements many interfaces
Access modifiersAny (public, protected, private)All methods implicitly public
State (fields)✅ Shared mutable state possible❌ No shared mutable state
Use caseIS-A: related classes sharing state & behaviorCAN-DO: unrelated classes sharing capability
ExampleVehicle → Car, Truck, BikeSerializable, Comparable, Runnable
Java versionSince Java 1.0Since Java 1.0; default methods from Java 8
☕ JavaAbstractVsInterface.java
// ✅ Use ABSTRACT CLASS for IS-A + shared state
// All animals share name, age, breathe() — use abstract class
abstract class Animal {
    protected String name;
    protected int age;
    public Animal(String name, int age) {
        this.name = name; this.age = age;
    }
    public void breathe() { System.out.println(name + " breathes"); }
    public abstract void makeSound(); // Each animal sounds different
}

// ✅ Use INTERFACE for CAN-DO capabilities — unrelated classes
// A Dog can swim, a Duck can swim, a Ship can swim — no IS-A relationship
interface Swimmable {
    void swim();
    default void floatOnWater() {
        System.out.println("Floating on water...");
    }
}

interface Trainable {
    void learn(String command);
    boolean canPerform(String command);
}

// Dog IS-A Animal AND CAN swim AND CAN be trained
// One abstract class + multiple interfaces
class Dog extends Animal implements Swimmable, Trainable {

    public Dog(String name, int age) { super(name, age); }

    @Override public void makeSound() { System.out.println(name + ": Woof!"); }
    @Override public void swim()       { System.out.println(name + " is swimming"); }

    @Override
    public void learn(String command) {
        System.out.println(name + " learned: " + command);
    }
    @Override
    public boolean canPerform(String command) { return true; }
}

class AbstractVsInterfaceDemo {
    public static void main(String[] args) {
        Dog dog = new Dog("Rocky", 3);
        dog.breathe();          // From abstract class Animal
        dog.makeSound();        // Rocky: Woof!
        dog.swim();             // Rocky is swimming
        dog.floatOnWater();     // Floating on water... (interface default)
        dog.learn("Sit");       // Rocky learned: Sit

        // Polymorphism with each type
        Animal   a = dog; a.makeSound();  // Animal reference
        Swimmable s = dog; s.swim();       // Swimmable reference
        Trainable t = dog; t.learn("Roll"); // Trainable reference
    }
}

Abstract Class vs Concrete Class — Side by Side

Understanding the precise differences between abstract and concrete classes helps you decide which to use when designing a class hierarchy.

CriteriaAbstract ClassConcrete Class
Declarationabstract keyword requiredNo special keyword — default is concrete
Instantiation❌ Cannot use new directly✅ Can use new directly
Abstract methods✅ Can declare abstract methods❌ Cannot declare abstract methods
Concrete methods✅ Can have concrete methods✅ All methods are concrete
Constructors✅ Yes — called via super() chain✅ Yes — called directly with new
PurposeBlueprint — defines what subclasses must doImplementation — provides complete, working behavior
Can be extended?✅ Yes — intended to be extended✅ Yes — but may not be designed for it
Use in polymorphism✅ Excellent reference type for polymorphism✅ Can also be used as reference type
CompletenessIntentionally incomplete — abstract methods missingComplete — all methods have bodies
Exampleabstract class Shape, abstract class Vehicleclass Circle, class Car, class ArrayList

Template Method Pattern — The Classic Abstract Class Design Pattern

The Template Method Pattern is the most natural design pattern for abstract classes. It defines the skeleton of an algorithm in a concrete method of the abstract class, deferring specific steps to abstract methods that subclasses implement. The abstract class controls the sequence of steps; the subclasses provide the implementation of individual steps. The template method itself is often declared final to prevent subclasses from altering the algorithm structure.

☕ JavaTemplateMethodPattern.java
/**
 * Template Method Pattern — Data Import Pipeline
 * Abstract class defines the fixed sequence (validate → parse → transform → save).
 * Subclasses implement the type-specific steps.
 */
public abstract class DataImporter {

    // ✅ TEMPLATE METHOD — final so subclasses cannot change the algorithm flow
    public final void importData(String source) {
        System.out.println("\n--- Starting import from: " + source + " ---");

        // Step 1: Validate — subclass-specific
        if (!validate(source)) {
            System.out.println("Validation failed. Import aborted.");
            return;
        }
        System.out.println("✅ Validation passed");

        // Step 2: Read raw data — subclass-specific
        String rawData = readData(source);
        System.out.println("✅ Data read: " + rawData.length() + " characters");

        // Step 3: Parse — subclass-specific
        Object[] records = parseData(rawData);
        System.out.println("✅ Parsed: " + records.length + " records");

        // Step 4: Transform — optional hook (has default no-op implementation)
        records = transform(records);

        // Step 5: Save — shared concrete behavior
        saveToDatabase(records);

        System.out.println("--- Import complete ---\n");
    }

    // Abstract steps — subclasses MUST implement
    protected abstract boolean  validate(String source);
    protected abstract String   readData(String source);
    protected abstract Object[] parseData(String rawData);

    // Hook method — optional override; default is a no-op
    protected Object[] transform(Object[] records) {
        return records; // Default: no transformation
    }

    // Concrete step — shared by all importers
    private void saveToDatabase(Object[] records) {
        System.out.println("✅ Saved " + records.length + " records to database");
    }
}

// Concrete subclass — CSV Importer
class CSVImporter extends DataImporter {

    @Override
    protected boolean validate(String source) {
        return source != null && source.endsWith(".csv");
    }

    @Override
    protected String readData(String source) {
        // Simulated CSV read
        return "id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com";
    }

    @Override
    protected Object[] parseData(String rawData) {
        return rawData.split("\n"); // Simple line split for demo
    }
}

// Concrete subclass — JSON Importer (overrides hook method too)
class JSONImporter extends DataImporter {

    @Override
    protected boolean validate(String source) {
        return source != null && source.endsWith(".json");
    }

    @Override
    protected String readData(String source) {
        return "[{\"id\":1,\"name\":\"Priya\"},{\"id\":2,\"name\":\"Raj\"}]";
    }

    @Override
    protected Object[] parseData(String rawData) {
        return rawData.replace("[","").replace("]","").split("\},\{");
    }

    // ✅ Overrides hook method — adds transformation step
    @Override
    protected Object[] transform(Object[] records) {
        System.out.println("✅ Transforming: normalizing JSON fields");
        return records;
    }
}

class TemplateDemo {
    public static void main(String[] args) {
        DataImporter csv  = new CSVImporter();
        DataImporter json = new JSONImporter();

        csv.importData("users.csv");
        json.importData("products.json");
        csv.importData("data.txt"); // Validation fails — not a .csv
    }
}

Common Mistakes & Pitfalls — Bugs That Fool Everyone

These mistakes appear consistently in beginner and intermediate Java code involving abstract classes. Most compile without obvious errors but lead to incorrect design or subtle runtime behavior.

☕ JavaAbstractClassMistakes.java
// ❌ MISTAKE 1: Trying to instantiate an abstract class
abstract class Fruit {
    public abstract String getTaste();
}
// Fruit f = new Fruit(); // COMPILE ERROR: Fruit is abstract; cannot be instantiated
// ✅ Fix: instantiate a concrete subclass
// Fruit f = new Mango();  // Mango extends Fruit and implements getTaste()

// ❌ MISTAKE 2: Forgetting to implement an abstract method in concrete subclass
abstract class Base {
    public abstract void methodA();
    public abstract void methodB();
}
// class Concrete extends Base {
//     @Override public void methodA() { ... }
//     // methodB() not implemented!
// }
// COMPILE ERROR: Concrete is not abstract and does not override abstract method methodB()
// ✅ Fix: implement ALL abstract methods, or declare Concrete as abstract

// ❌ MISTAKE 3: Declaring abstract method as private
abstract class Example {
    // private abstract void secret(); // COMPILE ERROR
    // private methods cannot be overridden — abstract has no meaning
    // ✅ Use protected or public for abstract methods
    protected abstract void process();
}

// ❌ MISTAKE 4: Declaring abstract method as final
abstract class Contract {
    // public final abstract void doWork(); // COMPILE ERROR
    // final = cannot override; abstract = MUST override — direct contradiction
    // ✅ Remove either final or abstract
    public abstract void doWork();
}

// ❌ MISTAKE 5: Calling super() without matching abstract class constructor
abstract class Config {
    protected String env;
    public Config(String env) { this.env = env; } // Only parameterized constructor
}
// class DevConfig extends Config {
//     public DevConfig() { } // COMPILE ERROR — implicit super() has no match
// }
// ✅ Fix: call the parameterized super() explicitly
class DevConfig extends Config {
    public DevConfig() { super("development"); } // Explicit call
}

// ❌ MISTAKE 6: Using abstract class when interface is more appropriate
// If there are NO shared fields and NO shared concrete methods,
// prefer an interface — it allows multiple inheritance and is more flexible.
abstract class Printable {
    public abstract void print(); // No fields, no concrete methods
}
// ✅ Better as an interface:
interface PrintableI {
    void print();
}

Bad Practices & Anti-Patterns — What Senior Developers Reject

These anti-patterns involving abstract classes are common causes of failed code reviews and brittle class hierarchies in professional Java teams.

🚫
Abstract Class with No Abstract Methods

Declaring a class abstract just to prevent instantiation, when no abstract methods exist, is a code smell. If the reason is 'this class should not be used directly', consider making the constructor package-private or protected instead. An abstract class with zero abstract methods provides no polymorphic contract — it is an abuse of the mechanism. The only valid exception: deliberate use of the Template Method pattern where hook methods are all concrete.

🚫
Deep Abstract Hierarchy (Inheritance Hell)

Chains of abstract classes 5–6 levels deep become impossible to trace and maintain. Each new level adds cognitive overhead for every developer who reads the code. Prefer COMPOSITION over INHERITANCE for code reuse beyond 2–3 levels. Deep hierarchies violate the principle that subclasses should be understandable without reading all their ancestors.

🚫
Fat Abstract Class (Doing Too Much)

An abstract class that mixes unrelated responsibilities — logging, validation, persistence, and business logic all in one — becomes a god class. Every subclass inherits all of this bloat, even if it only needs one piece. Prefer small, focused abstract classes. Move unrelated concerns to interfaces or injected collaborators (composition).

🚫
Overriding Concrete Methods Without Good Reason

Concrete methods in an abstract class represent guaranteed shared behavior. If subclasses routinely override them with completely different logic, the shared method serves no real purpose. This is a sign the hierarchy is wrong — the shared behavior isn't actually shared. Reconsider whether these classes truly belong in the same hierarchy.

🚫
Exposing Abstract Class Constructor as Public

The constructor of an abstract class can never be called directly (abstract classes cannot be instantiated). Making it public misleads readers into thinking it can be. Abstract class constructors should be protected — only subclass constructors in the same or different packages need to call super(). A public constructor on an abstract class is never needed.

🚫
Using Abstract Class Where Interface Suffices

If your abstract class has no instance fields and no concrete methods (pure abstract methods only), replace it with an interface. Interfaces support multiple inheritance, are more flexible, and impose less coupling. This is the most common misuse — using abstract class as a 'poor man's interface' when a real interface would be better.

☕ JavaAbstractAntiPatterns.java
// ❌ ANTI-PATTERN 1: Abstract class used as pure interface — no fields, no concrete methods
abstract class Drawable {
    public abstract void draw();
    public abstract void resize(double factor);
    public abstract void setColor(String color);
}
// ✅ BETTER: Use an interface
interface DrawableI {
    void draw();
    void resize(double factor);
    void setColor(String color);
}

// ❌ ANTI-PATTERN 2: Public constructor on abstract class
abstract class AbstractProcessor {
    public String config;
    public AbstractProcessor(String config) { // ❌ public constructor
        this.config = config;
    }
    public abstract void process();
}
// ✅ BETTER: protected constructor
abstract class ProcessorFixed {
    protected String config;
    protected ProcessorFixed(String config) { // ✅ protected
        this.config = config;
    }
    public abstract void process();
}

// ❌ ANTI-PATTERN 3: Deep unnecessary hierarchy
abstract class A { abstract void doA(); }
abstract class B extends A { abstract void doB(); }
abstract class C extends B { abstract void doC(); }
abstract class D extends C { abstract void doD(); }
class E extends D {
    // Must implement doA, doB, doC, doD — impossible to read without tracing 4 levels
    void doA() {} void doB() {} void doC() {} void doD() {}
}
// ✅ BETTER: Flatten with interfaces + one abstract class + composition

Real-World Production Code Examples — Abstract Classes in Context

The following examples model idiomatic Java abstract class usage in real enterprise Spring Boot-style codebases — illustrating clean abstraction at each application layer.

☕ JavaReportGenerator.java — Abstract Class in Reporting Layer
package com.techsustainify.report;

import java.time.LocalDate;
import java.util.List;

/**
 * Abstract base for all report generators.
 * Template Method Pattern: generateReport() defines the fixed pipeline.
 * Subclasses implement format-specific logic.
 */
public abstract class ReportGenerator {

    // Shared state — every report has these
    protected String     reportTitle;
    protected LocalDate  reportDate;
    protected String     generatedBy;

    protected ReportGenerator(String title, String generatedBy) {
        this.reportTitle = title;
        this.reportDate  = LocalDate.now();
        this.generatedBy = generatedBy;
    }

    // ✅ Template method — fixed pipeline, cannot be altered by subclasses
    public final byte[] generateReport(List<?> data) {
        validateData(data);
        String header  = buildHeader();
        String body    = buildBody(data);
        String footer  = buildFooter();
        String content = header + "\n" + body + "\n" + footer;
        return exportToFormat(content);
    }

    // Abstract steps — format-specific implementation
    protected abstract String buildHeader();
    protected abstract String buildBody(List<?> data);
    protected abstract String buildFooter();
    protected abstract byte[] exportToFormat(String content);
    public    abstract String getFormatName();

    // Hook — subclasses may add custom validation
    protected void validateData(List<?> data) {
        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("Report data cannot be empty");
        }
    }

    // Concrete shared utility
    protected String getReportMeta() {
        return reportTitle + " | Date: " + reportDate + " | By: " + generatedBy;
    }
}

// Concrete subclass — PDF Report
class PDFReportGenerator extends ReportGenerator {

    private boolean includeWatermark;

    public PDFReportGenerator(String title, String generatedBy, boolean watermark) {
        super(title, generatedBy);
        this.includeWatermark = watermark;
    }

    @Override
    protected String buildHeader() {
        return "[PDF HEADER] " + getReportMeta();
    }

    @Override
    protected String buildBody(List<?> data) {
        StringBuilder sb = new StringBuilder("[PDF BODY]\n");
        data.forEach(item -> sb.append("  • ").append(item).append("\n"));
        return sb.toString();
    }

    @Override
    protected String buildFooter() {
        return includeWatermark
               ? "[PDF FOOTER] CONFIDENTIAL — " + reportTitle
               : "[PDF FOOTER] " + reportTitle;
    }

    @Override
    protected byte[] exportToFormat(String content) {
        System.out.println("Exporting as PDF:\n" + content);
        return content.getBytes();
    }

    @Override public String getFormatName() { return "PDF"; }
}

// Concrete subclass — Excel Report
class ExcelReportGenerator extends ReportGenerator {

    private String sheetName;

    public ExcelReportGenerator(String title, String generatedBy, String sheet) {
        super(title, generatedBy);
        this.sheetName = sheet;
    }

    @Override
    protected String buildHeader() {
        return "Sheet: " + sheetName + " | " + getReportMeta();
    }

    @Override
    protected String buildBody(List<?> data) {
        StringBuilder sb = new StringBuilder("ROW_NO,VALUE\n");
        int i = 1;
        for (Object item : data) {
            sb.append(i++).append(",").append(item).append("\n");
        }
        return sb.toString();
    }

    @Override
    protected String buildFooter() {
        return "Total rows: " + sheetName;
    }

    @Override
    protected byte[] exportToFormat(String content) {
        System.out.println("Exporting as Excel:\n" + content);
        return content.getBytes();
    }

    @Override public String getFormatName() { return "Excel"; }
}

class ReportDemo {
    public static void main(String[] args) {
        List<String> salesData = List.of("Laptop ₹75000", "Phone ₹25000", "Tablet ₹35000");

        ReportGenerator pdf   = new PDFReportGenerator("Q1 Sales", "Admin", true);
        ReportGenerator excel = new ExcelReportGenerator("Q1 Sales", "Admin", "SalesSheet");

        pdf.generateReport(salesData);
        System.out.println("---");
        excel.generateReport(salesData);
    }
}
☕ JavaNotificationSender.java — Abstract Class for Communication Layer
package com.techsustainify.notification;

import java.time.LocalDateTime;

public abstract class NotificationSender {

    protected String senderName;
    protected boolean logEnabled;

    protected NotificationSender(String senderName, boolean logEnabled) {
        this.senderName  = senderName;
        this.logEnabled  = logEnabled;
    }

    // Template method
    public final boolean send(String recipient, String message) {
        if (!validateRecipient(recipient)) {
            log("INVALID RECIPIENT: " + recipient);
            return false;
        }
        String formatted = formatMessage(message);
        boolean result   = deliver(recipient, formatted);
        if (logEnabled) log((result ? "SENT" : "FAILED") + " to " + recipient);
        return result;
    }

    protected abstract boolean validateRecipient(String recipient);
    protected abstract String  formatMessage(String rawMessage);
    protected abstract boolean deliver(String recipient, String message);
    public    abstract String  getChannelName();

    private void log(String entry) {
        System.out.println("[" + LocalDateTime.now().toLocalTime()
                           + "] [" + getChannelName() + "] " + entry);
    }
}

class EmailSender extends NotificationSender {
    public EmailSender() { super("EmailService", true); }

    @Override
    protected boolean validateRecipient(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }
    @Override
    protected String formatMessage(String msg) {
        return "Subject: Notification\n\n" + msg + "\n\n-- " + senderName;
    }
    @Override
    protected boolean deliver(String recipient, String message) {
        System.out.println("Sending email to " + recipient + ":\n" + message);
        return true;
    }
    @Override public String getChannelName() { return "EMAIL"; }
}

class SMSSender extends NotificationSender {
    public SMSSender() { super("SMSGateway", true); }

    @Override
    protected boolean validateRecipient(String phone) {
        return phone != null && phone.matches("\\+?[0-9]{10,13}");
    }
    @Override
    protected String formatMessage(String msg) {
        return msg.length() > 160 ? msg.substring(0, 157) + "..." : msg;
    }
    @Override
    protected boolean deliver(String phone, String message) {
        System.out.println("SMS to " + phone + ": " + message);
        return true;
    }
    @Override public String getChannelName() { return "SMS"; }
}

class NotificationDemo {
    public static void main(String[] args) {
        NotificationSender[] senders = {
            new EmailSender(),
            new SMSSender()
        };
        for (NotificationSender sender : senders) {
            sender.send("user@example.com", "Your order has been shipped!");
            sender.send("+919876543210",    "Your OTP is 482910");
        }
    }
}

Abstract Class Hierarchy Flowchart — Visual Inheritance Flow

This flowchart illustrates how abstract classes, abstract subclasses, and concrete subclasses relate in a Java hierarchy, and how instantiation flows through the constructor chain.

📐 Abstract ClassCannot instantiate. Has abstract + concrete methods.
Subclass extends
🔍 Does subclass implement ALL abstract methods?
NO — stays abstract
📐 Abstract SubclassStill cannot instantiate. Defers some methods.
Further subclass
🟩 Concrete SubclassAll methods implemented. Can be instantiated.
new ConcreteClass()
▶ new ConcreteClass(args)Instantiation begins
JVM invokes constructor
⚙️ Concrete constructor runsCalls super(args) explicitly or implicitly
super() chain
⚙️ Abstract class constructorInitializes shared fields (name, id, etc.)
Fields initialized
✅ Object readyFully initialized concrete object
Use via ref
🔄 Polymorphic usageAbstractClass ref = new ConcreteClass(); ref.method();

Code Execution Flow — from source to output

Java Abstract Class Interview Questions — Beginner to Advanced

These questions are consistently asked in Java fresher and experienced developer interviews, OCPJP certification exams, and campus placement tests.

Practice Questions — Test Your Abstract Class Knowledge

Challenge yourself with these practice questions. Attempt each independently before reading the answer — active recall is proven to be 2–3x more effective than passive reading.

1. Will this compile? If not, why? abstract class A { abstract void doWork(); } class B extends A { // doWork() not implemented }

Easy

2. What is the output? abstract class Base { Base() { System.out.println("Base constructor"); } abstract void show(); } class Child extends Base { Child() { System.out.println("Child constructor"); } @Override void show() { System.out.println("Child show"); } } public class Test { public static void main(String[] args) { Base b = new Child(); b.show(); } }

Easy

3. Design an abstract class 'Discount' with: a shared field 'discountCode', a constructor, an abstract method 'calculateDiscount(double price)', and a concrete method 'applyDiscount(double price)' that prints the final price after calling calculateDiscount(). Then write two concrete subclasses: 'FlatDiscount' (fixed amount off) and 'PercentageDiscount' (percent off).

Medium

4. What is wrong with the following code? abstract class Validator { private abstract boolean validate(String input); }

Easy

5. Implement the Template Method Pattern for a 'DatabaseMigration' abstract class with the template method 'migrate()' that: (1) takes a backup, (2) runs migration scripts, (3) validates the result, (4) sends a report. Steps 2 and 3 are abstract; steps 1 and 4 are shared concrete methods.

Medium

6. What is the output? Explain why. abstract class Printer { public Printer() { print(); } public abstract void print(); } class ColorPrinter extends Printer { private String color = "Blue"; public ColorPrinter() { super(); } @Override public void print() { System.out.println("Color: " + color); } } public class Test { public static void main(String[] args) { new ColorPrinter(); } }

Hard

7. Choose between abstract class and interface for each scenario: (a) Shape hierarchy with Circle, Rectangle — all shapes have a color field and a shared draw() method. (b) Sortable behavior for Student, Product, Employee. (c) Document processors for PDF, Word, Excel — all share an author field and a validate() step.

Medium

8. A class extends an abstract class but only implements some of the abstract methods. What must be true about that class? Can it be used to create objects?

Medium

Conclusion — Abstract Classes: The Blueprint Architecture of Java OOP

Abstract classes are the architectural backbone of Java class hierarchies. They define what a family of related classes has in common — shared state, shared behavior, and a common contract — while leaving the specifics to concrete subclasses. They are the mechanism through which Java enforces the Open/Closed Principle: open for extension (add new subclasses), closed for modification (the abstract class's structure remains stable).

The difference between beginner and senior Java code is visible in how they use abstract classes. Beginners either avoid them entirely (duplicating logic across classes) or overuse them (creating deep, rigid hierarchies). Senior developers use abstract classes precisely: as partial implementations with a clear contract, leveraging the Template Method Pattern to define stable algorithms with swappable steps, and preferring interfaces when there is no shared state to justify an abstract class.

ConceptKey RuleExample
Abstract classCannot instantiate. Has abstract + concrete members.abstract class Shape { abstract double area(); }
Concrete classImplements all abstract methods. Can be instantiated.class Circle extends Shape { double area() {...} }
Abstract methodNo body. Subclass MUST implement. Not private/static/final.public abstract void process();
Constructor in abstract classValid. Called via super() from subclass constructors.protected Shape(String color) { this.color=color; }
Partial implementationAbstract subclass defers some methods to next level.abstract class PermanentEmployee extends Employee
Abstract class vs interfaceUse abstract class for IS-A + shared state. Interface for CAN-DO.Vehicle → Car (abstract); Serializable (interface)
Template Method PatternAbstract class defines algorithm; subclasses implement steps.final importData() calls abstract parse(), validate()
Hook methodConcrete method with default no-op; optional override.protected Object[] transform(r) { return r; }
Constructor visibilityAbstract class constructors should be protected.protected BankAccount(String id, double bal)

Your next step: Java Interfaces — where you'll explore pure contract-based abstraction, default and static interface methods (Java 8+), sealed interfaces (Java 17+), and how interfaces complement abstract classes to form the complete Java abstraction toolkit. ☕

Frequently Asked Questions — Java Abstract Class & Concrete Class