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
// ── 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 concreteConstructors in Abstract Classes
// ── 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
// ── 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 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