23. Interface vs Abstract Class Decision Matrix
Choose an interface for a capability/contract that unrelated classes can share; choose an abstract class for a shared skeleton (state + partial implementation) within a single inheritance hierarchy.
Java 21 (core language)Explanation
The decision comes down to two questions: Does the implementor need shared state? and Is the "is-a" relationship meaningful?
Use an interface when: you're defining a capability (Comparable, Runnable, Serializable) that any class can adopt; you want multiple inheritance of type; you're designing a public API and want maximum flexibility for implementors.
Use an abstract class when: subclasses share significant common code or state; you want to enforce a constructor contract; you need protected abstract hook methods (Template Method pattern); the class is part of a closed, well-defined hierarchy.
Since Java 8 default methods blurred the line, but abstract classes retain exclusive advantages: instance fields, constructors with arguments, non-public abstract methods, and the ability to hold mutable state. A rule of thumb from Effective Java (Item 20) is to prefer interfaces over abstract classes where either would work.
Example
import java.util.List;
public class InterfaceVsAbstract {
// INTERFACE: capability โ any class can be "Persistable"
interface Persistable {
void save();
default void saveAll(List<? extends Persistable> items) {
items.forEach(Persistable::save);
}
static Persistable noOp() { return () -> {}; }
}
// ABSTRACT CLASS: shared skeleton with state
abstract static class BaseEntity implements Persistable {
private final long id; // shared state
private boolean dirty = false; // mutable shared state
BaseEntity(long id) { this.id = id; }
public long getId() { return id; }
protected void markDirty() { dirty = true; }
protected boolean isDirty() { return dirty; }
@Override
public void save() {
if (dirty) {
persist(); // abstract hook
dirty = false;
}
}
protected abstract void persist(); // subclass fills in
}
static class User extends BaseEntity {
private String name;
User(long id, String name) { super(id); this.name = name; }
public void rename(String n) { this.name = n; markDirty(); }
@Override protected void persist() {
System.out.println("Saving User #" + getId() + " name=" + name);
}
}
public static void main(String[] args) {
User u = new User(1L, "Alice");
u.save(); // not dirty โ no output
u.rename("Alicia");
u.save(); // dirty โ "Saving User #1 name=Alicia"
// Unrelated class implements Persistable without extending BaseEntity
Persistable log = () -> System.out.println("Flushing log");
log.save();
}
}Sources
- https://docs.oracle.com/javase/tutorial/java/IandI/interfaceVsAbstractClass.html โ Interface vs abstract class
- https://www.pearson.com/en-us/subject-catalog/p/effective-java/P200000000138 โ Effective Java, Item 20