Skip to content

Java · Object-Oriented Programming

Java Interfaces vs Abstract Classes — When to Use Which

10 min read Updated 2026-06-20 Share:

Practice Interfaces vs Abstract Classes interview questions

Interfaces vs abstract classes in Java

"Interface or abstract class?" is one of the most common Java design questions — in interviews and in real code. Both let you program to an abstraction instead of a concrete type, but they answer different needs. An abstract class is a partially built base for a family of related classes; an interface is a contract any class can sign, even unrelated ones. Since Java 8 the line has blurred (interfaces now carry behavior), so the real skill is knowing what each one can't do and choosing on that basis. This guide walks through both, the rules that govern conflicts between them, and the judgment that makes the choice obvious.

Abstraction: why both exist

Abstraction means exposing a simplified contract while hiding the implementation behind it. Callers depend on the abstract type, not the concrete class — so you can swap implementations freely. This is the heart of "program to an interface, not an implementation."

interface PaymentGateway { void charge(int cents); }

class StripeGateway implements PaymentGateway {
  public void charge(int cents) { /* HTTP calls, retries, auth all hidden here */ }
}

PaymentGateway gw = new StripeGateway(); // calling code knows only the contract
gw.charge(1999);                         // swap in PayPalGateway later, no caller changes

Both interfaces and abstract classes give you this decoupling. The difference is how much they let you share alongside the contract — and that is what drives the choice.

What an abstract class can do

An abstract class is declared abstract and cannot be instantiated with new — it exists to be extended. Its superpower is that it can hold real state (instance fields), define a constructor, and mix abstract methods (no body, subclasses must implement) with concrete methods (shared implementation). A class extends exactly one.

abstract class Shape {
  private final String name;          // instance STATE — interfaces can't do this
  Shape(String name) { this.name = name; } // constructor runs during subclass construction

  abstract double area();             // subclasses MUST implement
  String describe() {                 // shared concrete behavior, reuses area()
    return name + " has area " + area();
  }
}
class Circle extends Shape {
  private final double r;
  Circle(double r) { super("circle"); this.r = r; } // must chain to super(...)
  double area() { return Math.PI * r * r; }
}
// Shape s = new Shape("x"); // ERROR — abstract, cannot instantiate

Note that a class with any abstract method must itself be abstract — but an abstract class is allowed to have zero abstract methods (sometimes useful purely to forbid direct instantiation). Think of an abstract class as a half-finished base you're not meant to use directly.

Constructors and shared state

A frequent interview gotcha: can an abstract class have a constructor? Yes. You can't new it directly, but its constructor runs when a subclass is instantiated, via the implicit or explicit super(...) call. That's exactly how the abstract class initializes its slice of the object's state.

abstract class Account {
  protected final String owner;
  private double balance;
  Account(String owner) { this.owner = owner; } // initializes shared state

  void deposit(double amt) { balance += amt; }   // shared, operates on shared state
  abstract double interestRate();                 // each subtype defines its own
}
class Savings extends Account {
  Savings(String owner) { super(owner); }         // constructor chaining is mandatory
  double interestRate() { return 0.04; }
}

Interfaces, by contrast, cannot have constructors — they have no instance state to initialize. The moment your abstraction needs to set up shared mutable fields at construction time, you need an abstract class, not an interface.

What an interface can do

An interface is a contract. Historically it held only abstract methods, but modern Java gives it default, static, and private methods too. The hard limits: no instance state (only public static final constants), no constructors, and a class can implement many interfaces. Every member is implicitly public.

interface Validator {
  boolean valid(String s);                         // abstract — the contract

  static Validator notEmpty() {                    // STATIC — factory tied to the type
    return s -> s != null && !s.isEmpty();
  }
  default boolean validAll(List<String> xs) {      // DEFAULT — optional shared behavior
    return xs.stream().allMatch(this::isOk);       // delegates to a private helper
  }
  private boolean isOk(String s) { return valid(s); } // PRIVATE — DRY for default methods
}

The three method flavors each earn their keep: default lets interfaces evolve without breaking implementers (this is how forEach was added to Iterable in Java 8); static holds factories and utilities tied to the type (like Comparator.comparing); private lets default/static methods share code without exposing it. Interfaces are now mini-toolkits, but still pure of mutable state.

Constants, not fields

Interfaces can hold data — but only constants. Every field is implicitly public static final, so it must be initialized at declaration and can never change. There is no per-instance storage.

interface Physics {
  double SPEED_OF_LIGHT = 299_792_458; // implicitly public static final
}
double c = Physics.SPEED_OF_LIGHT;     // access through the interface name

Beware the old "constant interface" anti-pattern — implementing an interface just to inherit its constants pollutes your type's public API with an implementation detail. Prefer an enum or a final class with a private constructor for grouped constants. An interface holds constants and behavior; never mutable state.

Multiple inheritance of type

The single biggest practical reason to reach for an interface: a class can implementsmany interfaces, but extends only one class. Interfaces give Java multiple inheritance of type (and, since Java 8, of behavior via defaults) without multiple inheritance of state — which is what causes the classic diamond problem.

interface Swimmer { default void move()  { System.out.println("swim");  } }
interface Flyer   { default void glide() { System.out.println("glide"); } }

class Duck implements Swimmer, Flyer { } // inherits BOTH capabilities, no conflict
new Duck().move();
new Duck().glide();

Because interfaces hold no instance fields, there is no ambiguous state to merge — that's precisely why Java permits multiple interfaces but only single class inheritance. Interfaces are safe multiple inheritance of capability; classes are single inheritance of implementation.

Resolving default-method conflicts

Multiple inheritance of behavior does introduce one ambiguity: what if two interfaces supply conflicting default methods? The compiler refuses to guess — the class must override to break the tie, and can delegate to a specific parent with the special Interface.super.method() syntax.

interface A { default String hi() { return "A"; } }
interface B { default String hi() { return "B"; } }

class C implements A, B {
  @Override public String hi() {
    return A.super.hi();   // explicitly pick A's default (or write your own)
  }
}

There's also a precedence rule when a class and an interface collide: class always beats interface. A concrete method inherited from a superclass wins over an interface default, even if the interface seems "more specific." Among interfaces alone, a more-specific subinterface's default beats a less-specific one; genuine ties you resolve by hand. The ordering to memorize: superclass method > interface default > you must override.

Functional interfaces

A functional interface has exactly one abstract method (SAM — Single Abstract Method), which makes it the target type of a lambda or method reference. default and static methods don't count against the limit. The @FunctionalInterface annotation asks the compiler to enforce the one-abstract-method rule, so an accidental second abstract method becomes a compile error.

@FunctionalInterface
interface Transformer {
  String apply(String s);                 // the single abstract method (SAM)
  default Transformer andTrim() {         // defaults don't break the SAM rule
    return s -> apply(s).trim();
  }
}

Transformer upper = s -> s.toUpperCase(); // lambda implements the SAM
upper.apply("  hi  ");                     // "  HI  "

The JDK is full of these: Runnable, Comparator, Function, Predicate, Supplier. This is one capability abstract classes simply can't offer — you cannot write a lambda for an abstract class. If you want your abstraction to be lambda-friendly, it has to be a single-method interface.

Marker interfaces

A marker interface has no methods at all — it exists purely to tag a class so code or the JVM can detect a capability at runtime via instanceof. The classic examples are Serializable, Cloneable, and RandomAccess.

class Config implements Serializable { } // "instances of this class may be serialized"

Object o = new Config();
if (o instanceof Serializable) { /* the JVM checks exactly this before serializing */ }

Modern Java often prefers annotations for metadata, but marker interfaces still give you two things annotations don't: a real type you can use in method signatures and a compile-time check via instanceof. They're a niche but legitimate use of interfaces with no abstract-class equivalent.

When to choose which

Decide by what you need to share:

  • Reach for an interface when you're defining a capability that unrelated classes can have, when you need multiple inheritance of type, or when you want a lambda target. No shared state.
  • Reach for an abstract class when you have a family of related classes that share fields, a constructor, or substantial implementation.
interface Comparable<T> { int compareTo(T o); } // any class can opt into being comparable
abstract class HttpServlet {                     // a family sharing request/response plumbing
  protected void service() { /* shared template logic, calls abstract hooks */ }
}

In practice they combine beautifully: declare the public API as an interface, then offer an optional AbstractXxx skeletal implementation for implementers who want the shared plumbing — exactly the List / AbstractList pattern in the JDK. The default heuristic: start with an interface, and only promote to (or add) an abstract class when you must share state or a real chunk of implementation.

Recap

An abstract class is a half-built base for a tight family of related types — it can hold state, a constructor, and a mix of concrete and abstract methods, but a class extends only one. An interface is a contract any class can sign: it allows default/static/private methods and public static final constants, but no instance state and no constructors, and a class implements many — giving Java safe multiple inheritance of type and behavior. Conflicting defaults force an override (Interface.super.method()), and class beats interface when they collide. Remember the specialized interface roles — functional interfaces for lambdas (SAM + @FunctionalInterface) and marker interfaces for instanceof tagging. Default to an interface; reach for an abstract class only when shared state or implementation demands it, and combine them via skeletal AbstractXxx classes when you want both.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel