Interfaces vs Abstract Classes

Contracts vs templates β€” and when each is the right tool

← Back to Index

What Are Interfaces and Abstract Classes?

Both interfaces and abstract classes are tools for abstraction β€” defining what something must do without specifying how it does it. They let you write code that works with any implementation of a contract, without knowing the concrete class behind it.

The problem they solve: without abstraction, your code is coupled to specific implementations. If OrderService directly creates a StripePaymentProcessor, switching to PayPal means rewriting OrderService. With an interface, OrderService depends only on PaymentProcessor β€” the concrete implementation can be swapped, tested, or extended without touching the caller.

// Without abstraction: tightly coupled to one implementation
            public class OrderService {
                private StripePaymentProcessor processor = new StripePaymentProcessor();
            
                public void checkout(Order order) {
                    processor.charge(order.total());  // locked to Stripe forever
                }
            }
            
            // With an interface: depends on the contract, not the implementation
            public interface PaymentProcessor {
                void charge(BigDecimal amount);
            }
            
            public class OrderService {
                private final PaymentProcessor processor;  // any implementation works
            
                public OrderService(PaymentProcessor processor) { this.processor = processor; }
            
                public void checkout(Order order) {
                    processor.charge(order.total());  // Stripe, PayPal, mock β€” doesn't matter
                }
            }
            
            // In production: inject StripePaymentProcessor
            // In tests: inject MockPaymentProcessor β€” zero infrastructure needed

Java provides two mechanisms for abstraction. An interface is a pure contract β€” it defines capabilities a class promises to provide (CAN-DO). An abstract class is a partial implementation β€” it defines a family of related types that share common code but leave specific behaviour to subclasses (IS-A). Knowing when to use each is one of the marks of an experienced Java developer.

Why Java has both

Interfaces enable multiple inheritance of type β€” a Duck can be both Flyable and Swimmable without any class hierarchy connecting those two capabilities. Abstract classes enable shared implementation β€” common fields, constructors, and concrete methods that every subclass inherits. Since Java 8 added default methods to interfaces, the line between the two has blurred β€” but constructors and instance state remain exclusive to abstract classes.

Key Differences at a Glance

Both prevent direct instantiation and both can declare abstract methods. The crucial differences are in state, inheritance, and intent:

Feature Interface Abstract Class
Intent Capability / contract (CAN-DO) Identity / template (IS-A)
Multiple inheritance βœ… A class can implement many ❌ A class can extend only one
Instance fields ❌ Only public static final βœ… Any type, any access modifier
Constructors ❌ Not allowed βœ… Called via super()
Method visibility Implicitly public (abstract/default) Any access modifier
Methods (Java 8+) abstract, default, static, private (9+) Any β€” abstract, concrete, final…
Keyword implements extends
Decision rule

Start with an interface. Switch to an abstract class only when you genuinely need shared state (instance fields) or a constructor. In modern Java (8+), interfaces can have default methods, so the gap has narrowed significantly β€” but constructors and instance fields remain exclusive to abstract classes.

Interfaces

An interface defines a contract: any class that implements it guarantees to provide those methods. Unrelated classes can share the same interface β€” a Duck and an Airplane can both be Flyable without any inheritance relationship between them.

Interface syntax: all features

public interface MessageSender {

    // Constant (implicitly public static final)
    int MAX_LENGTH = 1600;

    // Abstract method β€” every implementor MUST provide this
    void send(String recipient, String message);
    boolean isAvailable();

    // Default method (Java 8+) β€” optional override, has implementation
    default void sendIfAvailable(String recipient, String message) {
        if (isAvailable()) send(recipient, message);
        else System.out.println("Service unavailable");
    }

    // Static method (Java 8+) β€” utility, NOT inherited by implementors
    static boolean isValidRecipient(String r) {
        return r != null && !r.isBlank();
    }

    // Private method (Java 9+) β€” shared logic for default methods only
    private String truncate(String msg) {
        return msg.length() <= MAX_LENGTH ? msg : msg.substring(0, MAX_LENGTH);
    }
}

Multiple interface implementation

interface Flyable   { void fly();  default int maxAltitude() { return 10_000; } }
interface Swimmable { void swim(); }
interface Walkable  { void walk(); }

// Duck gains all three capabilities β€” no inheritance hierarchy required
public class Duck implements Flyable, Swimmable, Walkable {
    @Override public void fly()  { ... }
    @Override public void swim() { ... }
    @Override public void walk() { ... }
}

// Airplane shares Flyable with Duck β€” completely unrelated class
public class Airplane implements Flyable {
    @Override public void fly()         { ... }
    @Override public int  maxAltitude() { return 40_000; }
}

// This method accepts both β€” polymorphism across unrelated types
void launch(Flyable f) { f.fly(); }

Functional interfaces and lambdas

// Any interface with exactly ONE abstract method is a functional interface
@FunctionalInterface  // optional but recommended β€” compiler enforces SAM rule
public interface Validator<T> {
    boolean validate(T input);

    // default methods don't break the @FunctionalInterface rule
    default Validator<T> and(Validator<T> other) {
        return input -> this.validate(input) && other.validate(input);
    }
}

// Implementation via lambda β€” no anonymous class boilerplate
Validator<String> notEmpty  = s -> !s.isBlank();
Validator<String> notTooLong = s -> s.length() <= 50;
Validator<String> combined   = notEmpty.and(notTooLong);

// Built-in functional interfaces (java.util.function):
// Function<T,R>    β€” T β†’ R            (transform)
// Consumer<T>      β€” T β†’ void         (consume)
// Supplier<T>      β€” () β†’ T           (produce)
// Predicate<T>     β€” T β†’ boolean      (test)
// BiFunction<T,U,R>β€” (T,U) β†’ R       (two-arg transform)

Abstract Classes

An abstract class provides a partial implementation for a family of related classes. Unlike interfaces, it can have instance state and constructors β€” which means it can enforce invariants at creation time and share mutable data across the hierarchy.

Abstract class syntax: all features

public abstract class Animal {
    // Instance fields β€” each object has its own copy (interfaces can't do this)
    protected final String name;
    protected       int    age;
    private   final String id;      // private = only this class sees it

    // Constructor β€” enforces invariants on creation
    public Animal(String name, int age) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException();
        this.name = name;
        this.age  = age;
        this.id   = "ANI-" + System.currentTimeMillis();
    }

    // Abstract methods β€” subclasses MUST implement
    public abstract void makeSound();
    public abstract void move();

    // Concrete methods β€” shared across all subclasses
    public void eat()  { System.out.println(name + " eating"); }
    public void sleep() { System.out.println(name + " sleeping"); }

    // final method β€” subclasses cannot override (algorithm is fixed)
    public final String getId() { return id; }
}

// Concrete subclass β€” must implement ALL abstract methods
public class Dog extends Animal {
    private final String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);   // MUST be first line β€” parent enforces its invariants
        this.breed = breed;
    }

    @Override public void makeSound() { System.out.println(name + " barks"); }
    @Override public void move()      { System.out.println(name + " runs"); }
    public void fetch()                { System.out.println(name + " fetches!"); }
}

Template Method pattern β€” the primary use case

// Abstract class defines the ALGORITHM SKELETON
public abstract class DataProcessor {

    // final β€” the flow is fixed, subclasses cannot reorder steps
    public final void run() {
        connect();
        readData();
        processData();   // ← subclass fills this in
        writeResults();  // ← subclass fills this in
        disconnect();
    }

    private void    connect()    { System.out.println("Connecting..."); }
    private void    disconnect() { System.out.println("Disconnecting..."); }
    protected void readData()   { System.out.println("Reading..."); } // overrideable

    protected abstract void processData();
    protected abstract void writeResults();
}

public class CsvProcessor extends DataProcessor {
    @Override protected void processData()  { System.out.println("Parsing CSV..."); }
    @Override protected void writeResults() { System.out.println("Writing CSV..."); }
}

public class JsonProcessor extends DataProcessor {
    @Override protected void processData()  { System.out.println("Parsing JSON..."); }
    @Override protected void writeResults() { System.out.println("Writing JSON..."); }
}

Combining Interface + Abstract Class

The most powerful designs use both together β€” exactly what the Java Collections Framework does. The interface is the public contract; the abstract class provides shared implementation so concrete classes only need to fill in the variable parts.

/*
 *  Collections Framework pattern:
 *
 *  List<E> (interface)        β€” defines the contract: add, get, remove…
 *     └── AbstractList<E>     β€” provides iterator(), indexOf(), equals()…
 *           β”œβ”€β”€ ArrayList<E>  β€” array-backed, O(1) random access
 *           └── LinkedList<E> β€” node-backed, O(1) head/tail ops
 */

// Replicate the pattern for a notification system

// 1. Interface = public API contract
public interface Notifier {
    void send(String recipient, String message);
    boolean isAvailable();

    default void sendIfAvailable(String r, String msg) {
        if (isAvailable()) send(r, msg);
    }
}

// 2. Abstract class = shared implementation (state + template)
public abstract class BaseNotifier implements Notifier {
    protected final String       senderName;
    private final   List<String> log = new ArrayList<>();

    protected BaseNotifier(String senderName) { this.senderName = senderName; }

    // Template method: validates + delegates + logs
    @Override
    public final void send(String recipient, String message) {
        if (recipient == null || recipient.isBlank())
            throw new IllegalArgumentException("Invalid recipient");
        doSend(recipient, "[" + senderName + "] " + message);
        log.add(recipient + ": " + message);
    }

    public List<String> getLog() { return List.copyOf(log); }

    // Only the delivery mechanism varies between subclasses
    protected abstract void doSend(String recipient, String msg);
}

// 3. Concrete classes = only the variable part
public class EmailNotifier extends BaseNotifier {
    private final String smtpServer;
    public EmailNotifier(String name, String smtp) { super(name); this.smtpServer = smtp; }
    @Override protected void doSend(String r, String m) { /* SMTP logic */ }
    @Override public boolean isAvailable() { return true; }
}

public class SmsNotifier extends BaseNotifier {
    private final String apiKey;
    public SmsNotifier(String name, String key) { super(name); this.apiKey = key; }
    @Override protected void doSend(String r, String m) { /* SMS API logic */ }
    @Override public boolean isAvailable() { return apiKey != null; }
}

// Caller depends only on the interface β€” concrete type is irrelevant
Notifier email = new EmailNotifier("System", "smtp.example.com");
Notifier sms   = new SmsNotifier("System", "api-key-123");

List.of(email, sms).forEach(n -> n.sendIfAvailable("user@example.com", "Hello!"));

The Diamond Problem with Default Methods

Java 8 default methods brought limited multiple inheritance of implementation. When two interfaces provide the same default method, the implementing class must resolve the ambiguity explicitly.

interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }

// ❌ Compile error: C inherits unrelated defaults for hello()
class C implements A, B { }

// βœ… Must override and explicitly choose
class C implements A, B {
    @Override
    public void hello() {
        A.super.hello();  // explicitly pick A's version
        // or: B.super.hello(); or your own implementation
    }
}

/*
 *  Resolution priority (highest to lowest):
 *
 *  1. Class method     β€” always beats interface defaults
 *  2. Sub-interface    β€” more specific interface wins over parent interface
 *  3. No winner        β€” compiler error, class must override
 */

// Rule 1 in action: class wins over interface
class Parent { public void hello() { System.out.println("Parent"); } }

class Child extends Parent implements A { }  // no override needed
new Child().hello();  // "Parent" β€” class wins

Common Pitfalls

Interface used as a constant holder β€” anti-pattern
// ❌ BAD: Constants interface β€” classes "implement" it to get the constants,
// polluting their public API with unrelated symbols forever
public interface Constants {
    int MAX_SIZE = 100;
    String PREFIX = "APP_";
}

// βœ… GOOD: final class with private constructor β€” utility class pattern
public final class AppConstants {
    private AppConstants() {}
    public static final int    MAX_SIZE = 100;
    public static final String PREFIX   = "APP_";
}

// βœ… BETTER for related sets of values: enum
public enum Status { PENDING, ACTIVE, CANCELLED }
Calling overridable methods from abstract class constructor
// ❌ DANGEROUS: abstract class constructor calls abstract/overridable method
public abstract class Base {
    public Base() {
        init();  // called before subclass constructor runs!
    }
    protected abstract void init();
}

public class Child extends Base {
    private final String value = "hello";
    @Override
    protected void init() {
        System.out.println(value);  // prints null! value not yet initialised
    }
}

// βœ… FIX: use a factory method or lazy initialisation instead
Implementing interface methods with wrong visibility
interface Service { void execute(); }  // implicitly public

// ❌ ERROR: package-private is MORE restrictive than public
class MyService implements Service {
    void execute() { ... }  // missing public keyword
}

// βœ… CORRECT: must be at least as visible as the interface declares
class MyService implements Service {
    @Override
    public void execute() { ... }
}

Interview Questions

πŸŽ“ Junior level

Q: What is the main difference between an interface and an abstract class?
A class can implement many interfaces but extend only one abstract class. Interfaces cannot have instance state or constructors; abstract classes can. Interfaces define capabilities (CAN-DO); abstract classes define identity (IS-A). Since Java 8, interfaces can have default and static methods, narrowing the gap β€” but state and constructors remain exclusive to abstract classes.

Q: Can an interface extend another interface?
Yes, and it can extend multiple interfaces. An interface uses extends (not implements) to inherit from other interfaces. Example: interface C extends A, B. The implementing class then must provide all abstract methods from the entire chain.

Q: What is a functional interface?
An interface with exactly one abstract method (SAM β€” Single Abstract Method). It can have any number of default and static methods; those don't count. @FunctionalInterface makes the compiler enforce the SAM rule. Functional interfaces are the target type for lambda expressions and method references. Examples: Runnable, Comparator, Function<T,R>.

Q: Can an abstract class have a constructor?
Yes. Even though you can't instantiate it directly, its constructor runs when a concrete subclass is instantiated via super(). This is how abstract classes enforce shared invariants β€” validating and setting fields that every subclass will rely on.

πŸ”₯ Senior level

Q: Why were default methods added to Java 8, and what problem do they create?
They were added to enable API evolution without breaking existing implementations. Adding forEach() to Iterable or stream() to Collection would have broken every class implementing those interfaces. Default methods let the JDK add new methods while keeping backward compatibility. The problem they create: if two interfaces define the same default method, implementing classes must override to resolve ambiguity β€” the compiler rejects the ambiguity silently. This is Java's pragmatic answer to the diamond problem, not a clean one.

Q: When would you choose an abstract class over an interface in 2026?
Practically: when you need (a) instance state shared across the hierarchy, (b) a constructor that enforces invariants, or (c) non-public methods (protected hooks for subclasses). With Java 8+ default methods, interfaces can now share behaviour β€” so the old reason "I need a shared method implementation" no longer applies. In Spring, the shift is clearly toward interfaces for everything and composition for sharing code.

Q: What is the Interface Segregation Principle and how does it apply here?
ISP (the I in SOLID): clients should not be forced to depend on methods they don't use. Applied to interface design: keep interfaces small and focused. A Printable interface and a Serializable interface are better than a DocumentOperations interface that forces every implementor to provide both. In practice: if you find yourself writing throw new UnsupportedOperationException() in an interface implementation, you're violating ISP β€” split the interface.

Q: Why is calling an overridable method from an abstract class constructor dangerous?
When new Child() is called, the JVM first runs Base() (the abstract class constructor). If that constructor calls init() which is overridden in Child, the Child.init() executes before Child's own constructor body runs. That means any final fields declared in Child are still at their default values (null, 0). This is a well-known source of NullPointerExceptions that are extremely hard to debug. Rule: never call overridable methods from constructors.