Contents
- What Is an Abstract Class
- Abstract Methods
- Constructors & Initialization
- Template Method Pattern
- Concrete Methods & Shared State
- Abstract Class vs Interface — Decision Guide
- Best Practices & Pitfalls
A class declared with the abstract keyword cannot be instantiated with new. It exists solely to be extended. An abstract class may contain abstract methods (declared without a body), concrete methods (with a full implementation), instance fields, static members, and constructors. None of this is available in a plain interface (which has no instance fields or constructors).
// Abstract class — cannot be instantiated
public abstract class Vehicle {
// Instance fields — real shared state
private final String registrationNumber;
private final String make;
protected int speedKph = 0;
// Constructor — called by subclasses via super(...)
protected Vehicle(String registrationNumber, String make) {
this.registrationNumber = registrationNumber;
this.make = make;
}
// Abstract method — every subclass MUST implement this
public abstract int maxSpeedKph();
// Abstract method — subclass defines the fuel type
public abstract String fuelType();
// Concrete method — shared by all vehicles
public void accelerate(int deltaKph) {
speedKph = Math.min(speedKph + deltaKph, maxSpeedKph());
System.out.printf("%s accelerates to %d kph%n", make, speedKph);
}
public String getRegistration() { return registrationNumber; }
public String getMake() { return make; }
}
// Concrete subclass — provides the two required abstract methods
public class ElectricCar extends Vehicle {
private final int batteryKwh;
public ElectricCar(String reg, String make, int batteryKwh) {
super(reg, make);
this.batteryKwh = batteryKwh;
}
@Override public int maxSpeedKph() { return 250; }
@Override public String fuelType() { return "Electric"; }
public int getBatteryKwh() { return batteryKwh; }
}
// Usage
ElectricCar tesla = new ElectricCar("EV-001", "Tesla Model 3", 75);
tesla.accelerate(80); // Tesla Model 3 accelerates to 80 kph
// new Vehicle(...) — COMPILE ERROR: Vehicle is abstract
A class that contains any abstract method must itself be declared abstract. If a subclass does not implement all inherited abstract methods, it must also be declared abstract. The chain continues until a fully concrete class provides implementations for every abstract method.
An abstract method is a method declaration with no body — just a signature, terminated with a semicolon. It defines what a subclass must do without specifying how. Abstract methods can be public or protected but never private (a private method can't be overridden) or static (static methods aren't polymorphic).
public abstract class DataExporter {
// Abstract — subclass defines the target format
protected abstract String serialize(java.util.List<?> records);
// Abstract — subclass defines the file extension
protected abstract String fileExtension();
// Abstract — optional header row for the format
protected abstract String header();
// Concrete — fixed export algorithm that uses the abstract pieces
public final void export(java.util.List<?> records, String basePath) {
String path = basePath + "." + fileExtension();
String content = header() + "\n" + serialize(records);
System.out.printf("Writing %d bytes to %s%n", content.length(), path);
// write content to path...
}
}
// CSV implementation
public class CsvExporter extends DataExporter {
@Override
protected String serialize(java.util.List<?> records) {
StringBuilder sb = new StringBuilder();
for (Object r : records) sb.append(r).append("\n");
return sb.toString();
}
@Override protected String fileExtension() { return "csv"; }
@Override protected String header() { return "id,name,value"; }
}
// JSON implementation
public class JsonExporter extends DataExporter {
@Override
protected String serialize(java.util.List<?> records) {
return records.toString(); // simplified
}
@Override protected String fileExtension() { return "json"; }
@Override protected String header() { return ""; }
}
Abstract classes can and should have constructors. They cannot be called with new directly, but they are called by subclass constructors via super(...). This is how an abstract class forces every subclass to provide required initialization — the compiler will reject a subclass that doesn't call a valid superclass constructor.
public abstract class BaseEntity {
private final long id;
private final java.time.Instant createdAt;
private java.time.Instant updatedAt;
// Protected constructor — only subclasses can call via super(...)
protected BaseEntity(long id) {
if (id <= 0) throw new IllegalArgumentException("id must be positive, got: " + id);
this.id = id;
this.createdAt = java.time.Instant.now();
this.updatedAt = this.createdAt;
}
// Overloaded constructor for reconstructing from storage
protected BaseEntity(long id, java.time.Instant createdAt, java.time.Instant updatedAt) {
this.id = id;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
protected void touch() { this.updatedAt = java.time.Instant.now(); }
public long getId() { return id; }
public java.time.Instant getCreatedAt() { return createdAt; }
public java.time.Instant getUpdatedAt() { return updatedAt; }
}
public class Product extends BaseEntity {
private String name;
private double price;
// Must call super — otherwise compile error
public Product(long id, String name, double price) {
super(id); // initializes id, createdAt, updatedAt
this.name = name;
this.price = price;
}
public void setPrice(double price) {
this.price = price;
touch(); // inherited protected method
}
public String getName() { return name; }
public double getPrice() { return price; }
}
Never call abstract (or overridable) methods from a superclass constructor. When the constructor runs, the subclass instance is not fully initialized yet — its fields still hold default values. The overridden method in the subclass may read those uninitialized fields and produce subtle bugs or NullPointerExceptions.
The Template Method pattern is the most important pattern built around abstract classes. The abstract class defines the skeleton of an algorithm — a final or concrete method that calls a series of steps — and declares certain steps as abstract, deferring their implementation to subclasses. The algorithm structure is fixed; only the variable parts change per subclass.
public abstract class ReportGenerator {
// Template method — the fixed algorithm. final prevents subclasses from
// accidentally reordering or skipping steps.
public final String generate(java.util.List<String> data) {
StringBuilder out = new StringBuilder();
out.append(renderHeader());
out.append("\n");
for (String item : data) {
out.append(renderRow(item));
out.append("\n");
}
out.append(renderFooter(data.size()));
postProcess(out.toString()); // optional hook — default is a no-op
return out.toString();
}
// Abstract steps — subclass must implement
protected abstract String renderHeader();
protected abstract String renderRow(String item);
protected abstract String renderFooter(int rowCount);
// Hook method — subclass may override, but doesn't have to
protected void postProcess(String output) {}
}
// Plain text report
public class TextReport extends ReportGenerator {
@Override protected String renderHeader() { return "=== REPORT ==="; }
@Override protected String renderRow(String item) { return " - " + item; }
@Override protected String renderFooter(int rowCount) { return "Total: " + rowCount + " items"; }
}
// Markdown report
public class MarkdownReport extends ReportGenerator {
@Override protected String renderHeader() { return "# Report\n| Item |"; }
@Override protected String renderRow(String item) { return "| " + item + " |"; }
@Override protected String renderFooter(int rowCount) { return "\n_" + rowCount + " records_"; }
// Override the hook to also print to console
@Override
protected void postProcess(String output) {
System.out.println("Markdown report generated (" + output.length() + " chars)");
}
}
// Usage — same call site, different output per subclass
java.util.List<String> items = java.util.List.of("Alice", "Bob", "Carol");
System.out.println(new TextReport().generate(items));
System.out.println(new MarkdownReport().generate(items));
Marking the template method final prevents subclasses from accidentally overriding the algorithm skeleton. Subclasses are intended to vary the steps, not the structure. The hook methods (like postProcess) have empty default implementations, making them optional extension points.
Unlike interfaces (which can only hold stateless default methods), abstract classes can hold mutable instance fields and concrete methods that operate on those fields. This is their primary advantage over interfaces when modelling a family of related types: the shared state and behaviour live in one place, and subclasses inherit both without duplication.
public abstract class ConnectionPool {
// Shared mutable state — not possible in an interface
private final java.util.Deque<Connection> available = new java.util.ArrayDeque<>();
private final java.util.Set<Connection> inUse = new java.util.HashSet<>();
private final int maxSize;
protected ConnectionPool(int maxSize) {
this.maxSize = maxSize;
}
// Concrete — shared pool management logic
public synchronized Connection borrow() {
if (!available.isEmpty()) {
Connection c = available.poll();
inUse.add(c);
return c;
}
if (inUse.size() < maxSize) {
Connection c = createConnection(); // abstract — subclass provides
inUse.add(c);
return c;
}
throw new IllegalStateException("Pool exhausted (max=" + maxSize + ")");
}
public synchronized void release(Connection c) {
if (inUse.remove(c)) {
if (isHealthy(c)) { // abstract — subclass validates
available.push(c);
} else {
closeConnection(c); // abstract — subclass closes
}
}
}
public int availableCount() { return available.size(); }
public int inUseCount() { return inUse.size(); }
// Abstract — subclass knows how to create, validate and close connections
protected abstract Connection createConnection();
protected abstract boolean isHealthy(Connection c);
protected abstract void closeConnection(Connection c);
}
With default methods in interfaces (Java 8+), the choice between abstract class and interface is less obvious than it once was. This table gives a practical decision guide:
| Question | If yes → prefer |
| Do you need mutable instance fields? | Abstract class |
| Do you need a constructor to enforce initialization? | Abstract class |
| Do you need protected helpers visible only to subclasses? | Abstract class |
| Will unrelated classes implement this type? | Interface |
| Should it be usable as a lambda target type? | Interface (functional) |
| Do you need multiple inheritance of type? | Interface |
| Is the relationship "is-a" with significant shared state? | Abstract class |
| Is the relationship a capability or role (can-do)? | Interface |
// Abstract class — right choice: shared state, constructor enforcement, protected helpers
public abstract class AbstractCache<K, V> {
private final int capacity;
protected final java.util.Map<K, V> store;
protected AbstractCache(int capacity) {
this.capacity = capacity;
this.store = new java.util.LinkedHashMap<>(capacity, 0.75f, true);
}
public V get(K key) { return store.get(key); }
public int size() { return store.size(); }
public void put(K key, V value) {
if (store.size() >= capacity) evict();
store.put(key, value);
}
protected abstract void evict(); // LRU, LFU, FIFO — subclass decides
}
// Interface — right choice: capability, multiple inheritance, lambda-friendly
public interface Serializable {
byte[] serialize();
}
public interface Cacheable {
String getCacheKey();
java.time.Duration ttl();
}
// A domain class can extend the abstract cache AND implement both interfaces
public class UserCache extends AbstractCache<Long, User>
implements Serializable, Cacheable {
public UserCache(int capacity) { super(capacity); }
@Override protected void evict() {
// remove the eldest entry
store.remove(store.keySet().iterator().next());
}
@Override public byte[] serialize() { return store.toString().getBytes(); }
@Override public String getCacheKey() { return "user-cache"; }
@Override public java.time.Duration ttl() { return java.time.Duration.ofMinutes(10); }
}
- Never call overridable methods from constructors. The subclass isn't fully initialized yet — its fields hold default values. Stick to private or final methods in constructors.
- Mark template methods final. If the algorithm structure must not change, make the template method final so subclasses can't accidentally replace it.
- Prefer abstract methods over hook methods. Abstract methods guarantee subclasses provide the step. Hook methods (concrete no-ops) are optional but can be silently ignored.
- Keep the abstract class focused. If you find yourself adding unrelated behaviour, split into multiple types or use composition instead of inheritance.
- Document the contract. Abstract methods are a promise. Javadoc should explain exactly what the subclass is expected to do — preconditions, postconditions, and expected behaviour.
- Consider using interfaces + composition first. Deep inheritance hierarchies are hard to understand and refactor. Abstract classes are justified when genuine, non-trivial state must be shared.
// Pitfall: calling overridable method from constructor
public abstract class Animal {
public Animal() {
// BAD: sound() is abstract — calls subclass method before subclass is init'd
System.out.println("Animal says: " + sound());
}
public abstract String sound();
}
public class Dog extends Animal {
private final String name = "Buddy"; // not yet set when super() runs!
@Override public String sound() { return name + " says Woof"; }
// Output: "Animal says: null says Woof" — name is null at super() call time
}
// Fix: don't call overridable methods in constructors
public abstract class AnimalFixed {
public AnimalFixed() { /* no overridable calls */ }
public abstract String sound();
public void introduce() { System.out.println("I say: " + sound()); }
}