☕ Java

Abstract Class

An abstract class is a class that cannot be instantiated — it exists only to be extended. It occupies the middle ground between a concrete class (fully implemented, instantiable) and an interface (no state, no implementation). An abstract class can declare abstract methods that subclasses must implement, and concrete methods that subclasses inherit. It can have fields, constructors, and any combination of abstract and concrete members. This combination makes it the ideal vehicle for the template method pattern, for sharing common state across a class hierarchy, and for defining a partial implementation that subclasses complete. This entry covers the abstract modifier in depth, abstract methods, the template method pattern, when to choose abstract class over interface, and the design rules that make abstract classes effective.

The abstract Modifier — Classes and Methods

The abstract modifier applied to a class prevents it from being instantiated directly. You cannot write new Animal() if Animal is abstract. This is a design statement: Animal is not a complete concept on its own — it is a framework for concrete animals (Dog, Cat, Bird) to build on. The abstract modifier enforces at compile time what the designer intended conceptually. The abstract modifier applied to a method declares a method with a signature but no body. The curly brace and the return statement are replaced by a semicolon. Abstract methods create an obligation — every concrete (non-abstract) subclass in the hierarchy must provide an implementation for every inherited abstract method, or it too must be declared abstract. The compiler enforces this: attempting to compile a non-abstract subclass with unimplemented abstract methods is a compile error. These two features work together to create a framework. The abstract class defines the structure and the shared behaviour. Abstract methods define the variation points — the parts of the structure that each concrete subclass fills in differently. The compiler guarantees that every concrete subclass has addressed every variation point. Nothing slips through. A class can be declared abstract even if it has no abstract methods — this prevents instantiation while keeping all methods concrete. This is useful for base classes designed only to be extended, where direct instantiation would be semantically wrong even though every method is implemented.
Java
// ── Abstract class — cannot be instantiated ──────────────────────────
public abstract class Animal {

    // ── Concrete fields — shared by all subclasses ────────────────────
    private final String name;
    protected     int    age;

    // ── Constructor — called by subclasses via super() ─────────────────
    protected Animal(String name) {
        Objects.requireNonNull(name, "name cannot be null");
        this.name = name;
        this.age  = 0;
    }

    // ── Abstract methods — subclass MUST implement these ──────────────
    public abstract String makeSound();
    public abstract String getType();
    public abstract double getDailyFeedGrams();

    // ── Concrete methods — shared implementation ──────────────────────
    public void eat() {
        System.out.printf("%s eats %.0fg of food.%n",
            name, getDailyFeedGrams());
    }

    public void sleep() {
        System.out.println(name + " sleeps.");
    }

    public String getName() { return name; }
    public int    getAge()  { return age;  }

    @Override
    public String toString() {
        return getType() + " named " + name;
    }
}

// ── Concrete subclass — MUST implement all abstract methods ───────────
public class Dog extends Animal {

    private final String breed;

    public Dog(String name, String breed) {
        super(name);
        this.breed = breed;
    }

    @Override public String makeSound()           { return "Woof!"; }
    @Override public String getType()             { return "Dog";   }
    @Override public double getDailyFeedGrams()   { return 300.0;   }
    public     String getBreed()                  { return breed;   }
}

// ── Abstract subclass — does NOT have to implement all abstract methods ─
public abstract class Feline extends Animal {

    protected boolean nocturnal;

    protected Feline(String name, boolean nocturnal) {
        super(name);
        this.nocturnal = nocturnal;
    }

    // Implements some abstract methods from Animal
    @Override public String getType() { return "Feline"; }

    // Adds a NEW abstract method for subclasses to implement
    public abstract boolean canBedomesticated();

    // Leaves makeSound() and getDailyFeedGrams() for concrete subclasses
}

public class Cat extends Feline {
    public Cat(String name) { super(name, true); }
    @Override public String makeSound()           { return "Meow!"; }
    @Override public double getDailyFeedGrams()   { return 60.0;    }
    @Override public boolean canBedomesticated()  { return true;    }
    @Override public String getType()             { return "Cat";   }
}

// new Animal("Rex");   // COMPILE ERROR — Animal is abstract
// new Feline("Leo", true);  // COMPILE ERROR — Feline is abstract
Dog d = new Dog("Rex", "Lab");   // OK — Dog is concrete

Constructors in Abstract Classes

Abstract classes can and should have constructors, even though no object is ever created directly from the abstract class. The constructors exist to be called by subclass constructors via super(). When a subclass is instantiated, the abstract class's constructor runs as part of the object initialisation chain — it initialises the shared fields that all subclasses have in common. This is one of the key advantages of abstract classes over interfaces: they can hold state and initialise it correctly. Every Dog, Cat, and Bird that extends Animal carries the name field initialised by Animal's constructor. The invariant checks in Animal's constructor — that name is not null, for example — apply to every animal regardless of species. The validation logic is written once and inherited everywhere. Constructors in abstract classes follow the same rules as regular constructors with one addition: they should be declared protected rather than public. A public constructor on an abstract class is misleading — it suggests the class can be instantiated directly, which it cannot. Protected communicates the correct intent: this constructor exists for subclasses to call, not for external callers to use directly.
Java
// ── Abstract class with validated constructor ────────────────────────
public abstract class DatabaseConnection {

    private final String   host;
    private final int      port;
    private final String   database;
    private       boolean  connected;
    private       int      queryCount;

    // ── protected — only subclasses call this ─────────────────────────
    protected DatabaseConnection(String host, int port, String database) {
        if (host == null || host.isBlank())
            throw new IllegalArgumentException("Host cannot be blank");
        if (port < 1 || port > 65535)
            throw new IllegalArgumentException("Port out of range: " + port);
        if (database == null || database.isBlank())
            throw new IllegalArgumentException("Database cannot be blank");

        this.host      = host;
        this.port      = port;
        this.database  = database;
        this.connected = false;
        this.queryCount = 0;
    }

    // ── Abstract — each DB type connects differently ──────────────────
    public abstract void connect() throws SQLException;
    public abstract void disconnect();
    public abstract ResultSet execute(String sql) throws SQLException;
    public abstract String getDriverName();

    // ── Concrete — shared across all DB implementations ───────────────
    public void executeWithLogging(String sql) throws SQLException {
        System.out.println("Executing on " + getDriverName() + ": " + sql);
        execute(sql);
        queryCount++;
    }

    public String getConnectionString() {
        return host + ":" + port + "/" + database;
    }

    public boolean isConnected()  { return connected;   }
    public int     getQueryCount(){ return queryCount;  }

    // Subclasses call this to update connected state
    protected void setConnected(boolean connected) {
        this.connected = connected;
    }

    protected String getHost()     { return host;     }
    protected int    getPort()     { return port;     }
    protected String getDatabase() { return database; }
}

// ── Each implementation provides driver-specific logic ────────────────
public class PostgresConnection extends DatabaseConnection {

    private Connection jdbcConnection;

    public PostgresConnection(String host, int port, String database) {
        super(host, port, database);   // validation runs for ALL types
    }

    @Override
    public String getDriverName() { return "PostgreSQL"; }

    @Override
    public void connect() throws SQLException {
        String url = "jdbc:postgresql://" + getConnectionString();
        jdbcConnection = DriverManager.getConnection(url);
        setConnected(true);
    }

    @Override
    public void disconnect() {
        try { if (jdbcConnection != null) jdbcConnection.close(); }
        catch (SQLException ignored) {}
        setConnected(false);
    }

    @Override
    public ResultSet execute(String sql) throws SQLException {
        return jdbcConnection.createStatement().executeQuery(sql);
    }
}

The Template Method Pattern

The template method pattern is the most important design pattern that abstract classes enable. A template method is a concrete method in the abstract class that defines the skeleton of an algorithm — the sequence of steps — and calls abstract methods (called hook methods) for the steps that vary between subclasses. The invariant structure lives in the template; the variable steps live in the subclass implementations of the hooks. This pattern inverts the conventional flow of calling. In ordinary code, the caller calls the callee. In the template method pattern, the abstract class (the framework) calls the subclass (the application code). This inversion — called the Hollywood Principle: "don't call us, we'll call you" — is what gives frameworks their power. Spring's JdbcTemplate uses this pattern: it handles connection acquisition, exception translation, and resource cleanup (the invariant template), while the developer provides the SQL and row mapping (the variable hooks). The template method pattern eliminates duplication across subclasses. If five report types all follow the sequence: validate parameters → fetch data → format output → send result, writing this sequence in five separate classes duplicates both the structure and any logic in the invariant steps. The abstract class writes the sequence once; each subclass provides only its specific data-fetching and formatting logic.
Java
// ── Template method pattern ───────────────────────────────────────────
public abstract class ReportGenerator {

    // ── TEMPLATE METHOD — the algorithm skeleton ──────────────────────
    // final prevents subclasses from changing the algorithm structure
    public final Report generate(ReportRequest request) {
        validateRequest(request);               // Step 1: shared concrete
        ReportData data  = fetchData(request);  // Step 2: abstract hook
        ReportData clean = cleanData(data);     // Step 3: concrete with default
        String     formatted = format(clean);   // Step 4: abstract hook
        Report     report    = buildReport(request, formatted); // Step 5: shared
        deliver(report, request.getRecipients()); // Step 6: abstract hook
        auditGeneration(report);                  // Step 7: shared concrete
        return report;
    }

    // ── Abstract hooks — subclasses MUST implement these ─────────────
    protected abstract ReportData fetchData(ReportRequest request);
    protected abstract String format(ReportData data);
    protected abstract void deliver(Report report, List<String> recipients);

    // ── Concrete steps — shared across all report types ───────────────
    private void validateRequest(ReportRequest request) {
        Objects.requireNonNull(request, "request cannot be null");
        if (request.getRecipients().isEmpty())
            throw new IllegalArgumentException(
                "Report must have at least one recipient");
        if (request.getFrom().isAfter(request.getTo()))
            throw new IllegalArgumentException(
                "From date must not be after to date");
    }

    private Report buildReport(ReportRequest request,
                                String formattedContent) {
        return new Report(
            UUID.randomUUID().toString(),
            getReportType(),
            formattedContent,
            Instant.now());
    }

    private void auditGeneration(Report report) {
        System.out.println("Generated " + getReportType() +
            " report " + report.getId() +
            " at " + report.getGeneratedAt());
    }

    // ── Hook with default — subclass may override ─────────────────────
    protected ReportData cleanData(ReportData raw) {
        return raw;   // default: no cleaning needed
        // Subclass can override if it needs to filter or transform
    }

    public abstract String getReportType();
}

// ── Subclasses fill in only the variable steps ────────────────────────
public class SalesReport extends ReportGenerator {

    private final SalesRepository salesRepo;
    private final EmailService    emailService;

    public SalesReport(SalesRepository sales, EmailService email) {
        this.salesRepo    = sales;
        this.emailService = email;
    }

    @Override
    public String getReportType() { return "SALES"; }

    @Override
    protected ReportData fetchData(ReportRequest request) {
        return salesRepo.aggregate(request.getFrom(), request.getTo());
    }

    @Override
    protected String format(ReportData data) {
        return CsvFormatter.format(data);   // Sales uses CSV
    }

    @Override
    protected void deliver(Report report, List<String> recipients) {
        emailService.sendReport(report, recipients);
    }
}

public class InventoryReport extends ReportGenerator {

    @Override public String getReportType() { return "INVENTORY"; }

    @Override
    protected ReportData fetchData(ReportRequest request) {
        return inventoryRepo.snapshot(request.getTo());
    }

    @Override
    protected ReportData cleanData(ReportData raw) {
        return raw.filterZeroQuantity();   // OVERRIDE the hook
    }

    @Override
    protected String format(ReportData data) {
        return HtmlFormatter.format(data);   // Inventory uses HTML
    }

    @Override
    protected void deliver(Report r, List<String> recipients) {
        portalService.publish(r, recipients);
    }
}

Abstract Class vs Interface — When to Choose Each

Abstract classes and interfaces both provide abstraction but serve different design purposes. The choice between them depends on what relationship is being modelled and what needs to be shared. Use an abstract class when subclasses share common state (fields) that all of them carry; when subclasses share default behaviour that most of them inherit without overriding; when you want to define a constructor that validates shared invariants; when you want to use protected access to share implementation details with subclasses without exposing them publicly; or when the type relationship is genuinely is-a (a Dog truly is an Animal, not just something that behaves like one). Use an interface when you are defining a capability or role that classes can adopt regardless of where they sit in the class hierarchy; when multiple unrelated classes need to share a contract (a File, a DatabaseConnection, and a NetworkSocket can all be Closeable); when you want to support multiple type adoption (a class can implement many interfaces but extend only one abstract class); or when the type relationship is can-do rather than is-a (a Dog can-do Trainable, can-do Comparable — these are capabilities, not essence). Java 8 added default methods to interfaces, which blur the line somewhat by allowing interfaces to provide behaviour. But the key distinguishing factor remains state: interfaces cannot have instance fields. If the shared abstraction requires shared state that each object carries individually, an abstract class is the right tool.
Java
// ── Abstract class: shared state + shared behaviour + is-a ───────────
public abstract class Employee {

    private final String   employeeId;      // SHARED STATE — all employees have this
    private final String   name;
    private       double   salary;
    private final LocalDate hireDate;

    protected Employee(String id, String name, double salary) {
        this.employeeId = id;
        this.name       = name;
        this.salary     = salary;
        this.hireDate   = LocalDate.now();
    }

    // ABSTRACT — each type calculates differently
    public abstract double calculateBonus();
    public abstract String getJobTitle();

    // CONCRETE SHARED — all employees get these
    public void raiseSalary(double pct) {
        if (pct < 0) throw new IllegalArgumentException();
        salary *= (1 + pct / 100);
    }

    public int getYearsOfService() {
        return Period.between(hireDate, LocalDate.now()).getYears();
    }

    public double getSalary()     { return salary;      }
    public String getEmployeeId() { return employeeId;  }
    public String getName()       { return name;        }
}

// ── Interface: capability / role, no shared state ────────────────────
public interface Auditable {
    // No instance fields — just the contract
    String getAuditDescription();
    Instant getLastModified();
    String  getModifiedBy();
}

public interface Exportable {
    byte[] exportToPdf();
    String exportToCsv();
}

// ── Classes can implement multiple interfaces regardless of hierarchy ──
// Manager IS-A Employee (abstract class — one only)
// Manager CAN Auditable AND Exportable (interfaces — as many as needed)
public class Manager extends Employee
        implements Auditable, Exportable {

    private final List<Employee> team;

    public Manager(String id, String name, double salary) {
        super(id, name, salary);
        this.team = new ArrayList<>();
    }

    @Override public double calculateBonus() { return getSalary() * 0.20; }
    @Override public String getJobTitle()    { return "Manager";          }

    @Override public String  getAuditDescription() { return "Manager " + getName(); }
    @Override public Instant getLastModified()      { return Instant.now();         }
    @Override public String  getModifiedBy()        { return "system";              }

    @Override public byte[] exportToPdf() { return new byte[0]; }
    @Override public String exportToCsv() { return "manager,..."; }
}

// ── Decision summary ──────────────────────────────────────────────────
//
// Use ABSTRACT CLASS when:          Use INTERFACE when:
// • Shared instance fields          • No shared state needed
// • Shared constructor logic        • Multiple type adoption needed
// • is-a relationship               • can-do / role relationship
// • Protected shared behaviour      • Unrelated classes share a contract
// • Template method pattern         • Pure API definition

Related Topics in Object-Oriented Programming

OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.