Skip to content
Java abstraction 6 min read

Abstract Class vs Interface

Choosing between an abstract class and an interface is one of the most common design decisions in Java. Both define contracts that other classes must fulfill, but they serve different purposes and carry different rules — knowing which to reach for makes your design cleaner and more maintainable.

The Core Idea

  • An abstract class is a partial implementation — it can carry state, constructors, and fully coded methods alongside the abstract ones you leave for subclasses.
  • An interface is a pure contract (historically), declaring what a type can do without saying how. Since Java 8 it can also carry default and static method bodies, but it still cannot hold instance state.

Think of an abstract class as a half-built house (walls up, plumbing rough-in done) and an interface as a building code (rules every house on the block must follow, regardless of style).

Quick Comparison Table

FeatureAbstract ClassInterface
abstract / interface keywordabstract classinterface
InstantiationNoNo
Instance fieldsYesNo (only public static final constants)
ConstructorsYesNo
Concrete methodsYesYes (via default / static, Java 8+)
Abstract methodsYesYes (implicitly public abstract)
Access modifiers on methodsAnypublic only (implicitly)
Multiple inheritanceNo (single extends)Yes (a class can implement many)
extends / implementsA class extends one abstract classA class implements many interfaces
When to useShared state + partial behaviorCapability contract across unrelated types

Declaring and Using Each

Abstract Class Example

abstract class Animal {
    String name; // instance state — allowed

    Animal(String name) {
        this.name = name; // constructor — allowed
    }

    // Concrete method — shared by all subclasses
    void breathe() {
        System.out.println(name + " breathes.");
    }

    // Abstract method — each subclass decides
    abstract void makeSound();
}

class Dog extends Animal {
    Dog(String name) { super(name); }

    @Override
    void makeSound() {
        System.out.println(name + " says: Woof!");
    }
}
// new Dog("Rex").breathe(); new Dog("Rex").makeSound();
Rex breathes.
Rex says: Woof!

Interface Example

interface Flyable {
    // implicitly public static final
    int MAX_ALTITUDE = 10_000;

    // implicitly public abstract
    void fly();

    // default method (Java 8+)
    default void land() {
        System.out.println("Landing safely.");
    }
}

class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Bird flaps its wings and flies.");
    }
}
// new Bird().fly(); new Bird().land();
Bird flaps its wings and flies.
Landing safely.

Implementing Multiple Interfaces

A class can only extends one class (abstract or not), but it can implements as many interfaces as needed:

interface Swimmable {
    void swim();
}

interface Flyable {
    void fly();
}

class Duck implements Flyable, Swimmable {
    @Override
    public void fly() { System.out.println("Duck flies low."); }

    @Override
    public void swim() { System.out.println("Duck paddles smoothly."); }
}

This is how Java achieves multiple inheritance of type without the diamond-problem pitfalls of C++. See Types of Inheritance for the full picture.

Tip: If two interfaces you implement declare the same default method, the compiler forces you to override it in your class — ambiguity is resolved at compile time, not at runtime.

When to Use Which

Reach for an Abstract Class when…

  • You want to share state (instance fields) among related classes.
  • You need constructors to enforce initialization rules.
  • Subclasses are closely related (same family — e.g., Vehicle → Car, Vehicle → Truck).
  • You want to provide a template method — a concrete algorithm that calls abstract steps.
abstract class Report {
    // Template method — algorithm fixed, steps open
    final void generate() {
        fetchData();
        format();
        print();
    }

    abstract void fetchData();
    abstract void format();

    void print() {
        System.out.println("Sending to printer...");
    }
}

The final keyword on generate() locks the algorithm; subclasses only fill in the blanks.

Reach for an Interface when…

  • You want to define a capability that unrelated classes can share (Comparable, Runnable, Serializable).
  • You need multiple inheritance of behavior (a class implements several roles).
  • You are designing an API for others to implement — interfaces are more stable contracts.
  • You want to take advantage of Java 8+ default methods to evolve an API without breaking existing implementors.

Note: The standard library’s Runnable, Callable, Comparator, and Iterable are all interfaces — they describe what a type can do, not what kind of thing it is.

Java 8+ Default Methods: When the Line Blurs

Java 8 added default methods to interfaces, letting you ship method bodies without breaking existing implementations. This narrowed the gap between abstract classes and interfaces.

interface Logger {
    void log(String message);

    default void logError(String message) {
        log("[ERROR] " + message);
    }
}

Any class that already implemented Logger gets logError for free — no code changes needed.

Warning: Default methods are not a backdoor to add instance state. You still cannot have instance fields in an interface. If you need state, an abstract class is the right tool.

Under the Hood

How the JVM Handles Each

Abstract classes are compiled to regular .class files. The JVM’s invokespecial bytecode instruction is used for constructor calls, and invokevirtual handles virtual dispatch — exactly like any class. The abstract constraint is enforced purely at compile time; by runtime the concrete subclass is in play.

Interfaces are compiled to .class files too, but method calls on interface references use the invokeinterface bytecode instruction. Historically this was slightly slower than invokevirtual because the JVM had to search the interface method table (itable) rather than the virtual method table (vtable). Modern JITs inline and optimize both equally well in hot code. See vtable & Dynamic Dispatch for deeper detail.

Default methods in interfaces are stored in the interface’s own bytecode. When a class does not override a default method, the JVM resolves the call by walking up the interface hierarchy at link time — again, the JIT makes this effectively free in practice.

Memory Layout

Because interfaces cannot have instance fields, implementing an interface adds zero bytes to the object’s heap layout. Abstract class fields, on the other hand, are part of every subclass instance — just like a regular superclass. This matters when you’re creating millions of objects and want to minimize memory footprint.

Common Pitfalls

  • Over-abstracting early — don’t reach for an abstract class or interface before you have at least two concrete implementations. YAGNI applies.
  • Mixing state into interfaces — trying to store mutable shared state in an interface leads to public static final constants, which are shared by all — not what you usually want.
  • Forgetting @Override — always annotate overridden methods. If you mistype the method signature, the compiler catches it instead of silently creating a new, unrelated method.
  • Making abstract classes too fat — if a class has both abstract methods and lots of concrete state, consider splitting the contract (interface) from the shared behavior (a non-abstract helper class).

Tip: In modern Java design, the most common pattern is to define contracts with interfaces and share implementation with package-private abstract or concrete base classes. This is how AbstractList backs ArrayList and LinkedList in the Collections Framework.

  • Abstract Class — deep dive into abstract class syntax, rules, and patterns
  • Interfaces — full guide to interface syntax, default methods, and functional interfaces
  • Polymorphism — how abstract types enable runtime flexibility
  • Inheritance — the extends relationship and how abstract classes fit in
  • Types of Inheritance — why Java uses interfaces for multiple inheritance
  • Encapsulation — how abstract types pair with encapsulation to build clean APIs
Last updated June 13, 2026
Was this helpful?