Interfaces & Abstract Classes

Interface vs Abstract Class

15 min Lesson 3 of 14

Interface vs Abstract Class

Now that you know what both tools look like, the practical question is: which one do I reach for? The answer comes down to two different relationships: what a type IS versus what a type CAN DO.

The Core Distinction

An abstract class models an is-a relationship. It represents a genuine parent type that shares state and behaviour with its children. An interface models a can-do (or behaves-like) relationship. It declares a capability that any unrelated class can choose to adopt.

  • A Dog is an Animal — use an abstract class.
  • A Dog can be Trainable — use an interface.
  • A Robot can also be Trainable, even though a Robot is not an Animal.
One-line rule: if you find yourself saying "an X is a kind of Y", reach for an abstract class. If you say "X is capable of doing Y", reach for an interface.

Key Technical Differences

Beyond the conceptual model, there are hard rules that constrain your choice:

  • Multiple inheritance: Java allows a class to implement many interfaces but extend only one abstract class. This single limitation is the most common reason to prefer interfaces.
  • Instance state: Abstract classes can have instance fields (stored state). Interfaces can only have public static final constants — they carry no per-object data.
  • Constructors: Abstract classes can define constructors to initialise shared fields. Interfaces have no constructors.
  • Access modifiers: Abstract class members can be private, protected, or public. Interface members are public by default (though private helper methods were added in Java 9).

Side-by-Side Example

Consider a drawing application. A Shape is a genuine parent — it has a colour field and a shared describe() method every shape inherits. Resizable and Exportable are capabilities that only some shapes need.

// Abstract class: shared state + partial implementation abstract class Shape { protected String color; Shape(String color) { this.color = color; } // shared concrete behaviour public String describe() { return color + " " + getClass().getSimpleName(); } // subclasses must supply this public abstract double area(); } // Capability interfaces interface Resizable { void resize(double factor); } interface Exportable { String toSvg(); } // Circle IS-A Shape, CAN-DO Resizable and Exportable class Circle extends Shape implements Resizable, Exportable { private double radius; Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } @Override public void resize(double f) { radius *= f; } @Override public String toSvg() { return "<circle r=\"" + radius + "\" fill=\"" + color + "\"/>"; } } // Rectangle IS-A Shape but is NOT Exportable class Rectangle extends Shape implements Resizable { private double w, h; Rectangle(String color, double w, double h) { super(color); this.w = w; this.h = h; } @Override public double area() { return w * h; } @Override public void resize(double f) { w *= f; h *= f; } }

Notice that Circle and Rectangle share the color field and describe() method through the abstract class — that state lives in one place. The capabilities (Resizable, Exportable) are mixed in independently, so you can add or remove them per class without touching the hierarchy.

When Shared State Forces Your Hand

If your design genuinely needs a shared mutable field, you must use an abstract class. Interfaces cannot hold it. Trying to simulate shared state through static interface constants is a code smell — constants are not the same as instance data.

// WRONG: trying to put shared mutable state in an interface interface BadIdea { // this is public static final — every implementor shares ONE value. // It cannot be per-object state. int count = 0; } // RIGHT: shared per-object state belongs in the abstract class abstract class Counter { protected int count = 0; public void increment() { count++; } public int getCount() { return count; } }

The "Evolving API" Factor

If you own a library and need to add a method later, the impact differs:

  • Abstract class: add a concrete method — existing subclasses inherit it for free, no breakage.
  • Interface (before Java 8): adding any method would break every implementor. Java 8 introduced default methods precisely to solve this — existing implementors inherit the default automatically.
Java 8+ best practice: prefer interfaces with default methods for public APIs. It keeps the design flexible (multiple implementations, multiple inheritance) and you can still evolve the interface without forcing immediate updates on every implementor.

Quick Decision Guide

  • Need to share instance fields across subclasses? → Abstract class.
  • Need a constructor to enforce initialisation? → Abstract class.
  • Want a class to inherit from multiple sources? → Interfaces (you can implement many).
  • Modelling a capability that unrelated types share? → Interface.
  • Publishing a library API that must stay backward-compatible? → Interface with default methods.
Common mistake: creating an abstract class just to share a utility method, when there is no real is-a relationship. Prefer a plain utility class with static methods, or a small interface with a default method, so you do not lock callers into a single inheritance chain unnecessarily.

Summary

Abstract classes and interfaces are complementary, not competing. Use an abstract class when you need to share state or enforce a strict type hierarchy. Use an interface when you are defining a capability that diverse, unrelated classes can adopt — and remember that a single class can implement many interfaces. In practice, modern Java code leans heavily on interfaces and reserves abstract classes for cases where shared mutable state or constructor logic genuinely belongs in the parent.