OOP

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