RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
MediumJava#206 min readJul 11, 2026

Interface vs Abstract Class.

practice
oop
learning
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this question because it checks whether you know when to define a contract and when to share code.

Question: What is the difference between an interface and an abstract class in Java?

Answer: An interface is mainly a contract: it says what a class can do. An abstract class is a partial blueprint: it can define shared fields, constructors, and common code, while still leaving some methods unfinished. In Java, a class can implement many interfaces, but it can extend only one class, so interfaces give flexibility and abstract classes give reuse.

Interview-Ready Answer: I use an interface when I want to define a capability, like sendable or payable, and I use an abstract class when I want to share state and common behavior. The big practical difference is that a class can implement many interfaces but extend only one abstract class. Also, interfaces are mostly contracts, while abstract classes can hold instance fields, constructors, and reusable logic, so I choose based on whether I need flexibility or shared implementation.

🧠 Memory Map
Memory map — visual summary of this topic

Big idea

Detailed Explanation: Think of an interface as a promise and an abstract class as a starter kit. The promise says every implementer must provide certain behavior. The starter kit gives you shared code, shared data, and a base constructor so subclasses do not repeat the same work.

How it works under the hood

  1. When a class implements an interface, the compiler checks that it provides every required method, unless the class itself is abstract.
  2. When a class extends an abstract class, it inherits fields and concrete methods immediately, and it must implement only the remaining abstract methods.
  3. At runtime, Java uses polymorphism: the call goes to the actual object type, not the variable type. That means both interfaces and abstract classes still dispatch in effectively constant time, or O(1).
  4. The JVM and JIT compiler, which means Just-In-Time compiler, optimize these calls heavily, so the choice is almost never about speed. It is about design.
  5. Since Java 8, interfaces can have default and static methods; since Java 9, they can also have private helper methods. That makes interfaces more flexible than they used to be, but they still do not replace abstract classes when you need per-object state.

Interface vs abstract class

FeatureInterfaceAbstract class
PurposeCapabilityShared base
StateConstants onlyInstance fields
ConstructorsNoYes
InheritanceMany allowedOnly one class
MethodsAbstract, default, static, privateAbstract and concrete
Best forLoose couplingReuse and templates

When to use each

  • Use an interface when unrelated classes should share the same capability, such as Comparable, Runnable, or a payment gateway contract.
  • Use an abstract class when subclasses need the same fields, constructor setup, validation, or template flow.
  • Use an interface first if you are designing an API for flexibility. You can add default methods later to evolve it without breaking all implementations.
  • Use an abstract class when you want to control part of the algorithm, such as validation before sending, then let subclasses fill in the specific step.

Important edge cases

  • An interface cannot have per-object state. Its fields are implicitly public static final, which means constant values shared by all objects.
  • An abstract class can have final methods, protected helpers, and a constructor, even though you cannot instantiate it directly.
  • A class can extend one abstract class and implement many interfaces at the same time. That is why interfaces are often used for capabilities like logging, serialization, or sorting.
  • There is no real performance win in choosing one over the other. In production, network calls, database calls, and JSON parsing dominate the cost, not the method dispatch.

Memory note: If you remember only one rule, remember this: interface = what it can do; abstract class = what it is plus shared code.

Real-World Story: In a checkout service, a team built multiple payment processors for card, wallet, and bank transfer. They used an interface for the common contract, but the shared retry logic and request validation lived better in an abstract base class. One release, a developer copied validation into one processor and missed a null check, so only that payment path started returning 500 errors during peak traffic. Users saw failed checkouts, logs showed IllegalArgumentException and intermittent retries, and the support team got a spike in abandoned carts.

The fix was to move the shared validation and retry policy into an abstract class and keep the payment capability as an interface. That made the contract clear, reduced duplication, and made future processors safer to add.

Java
import java.util.Arrays;
import java.util.List;

interface Notifier {
    void send(String recipient, String message);
}

abstract class BaseNotifier implements Notifier {
    private final String serviceName;
    private final int maxRetries;

    protected BaseNotifier(String serviceName, int maxRetries) {
        if (serviceName == null || serviceName.isBlank()) {
            throw new IllegalArgumentException("serviceName must not be blank");
        }
        if (maxRetries < 1) {
            throw new IllegalArgumentException("maxRetries must be at least 1");
        }
        this.serviceName = serviceName;
        this.maxRetries = maxRetries;
    }

    @Override
    public final void send(String recipient, String message) {
        validate(recipient, message);

        RuntimeException lastFailure = null;
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                doSend(recipient, message, attempt);
                System.out.println(serviceName + " delivered to " + recipient);
                return;
            } catch (RuntimeException ex) {
                lastFailure = ex;
                System.out.println(serviceName + " attempt " + attempt + " failed: " + ex.getMessage());
            }
        }

        throw new IllegalStateException(
                serviceName + " could not deliver after " + maxRetries + " attempts",
                lastFailure);
    }

    protected abstract void doSend(String recipient, String message, int attempt);

    protected void validate(String recipient, String message) {
        if (recipient == null || recipient.isBlank()) {
            throw new IllegalArgumentException("recipient must not be blank");
        }
        if (message == null || message.isBlank()) {
            throw new IllegalArgumentException("message must not be blank");
        }
    }

    protected String serviceName() {
        return serviceName;
    }
}

class EmailNotifier extends BaseNotifier {
    EmailNotifier() {
        super("Email", 2);
    }

    @Override
    protected void doSend(String recipient, String message, int attempt) {
        if (recipient.endsWith("@baddomain.com")) {
            throw new RuntimeException("SMTP rejected recipient");
        }
        System.out.println("Sending email via " + serviceName() + " on attempt " + attempt + ": " + message);
    }
}

class SmsNotifier extends BaseNotifier {
    SmsNotifier() {
        super("SMS", 3);
    }

    @Override
    protected void doSend(String recipient, String message, int attempt) {
        if (!recipient.matches("[0-9]{10}")) {
            throw new RuntimeException("invalid phone number format");
        }
        if (attempt < 2 && message.length() > 20) {
            throw new RuntimeException("gateway timeout");
        }
        System.out.println("Sending SMS via " + serviceName() + " on attempt " + attempt + ": " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        List<Notifier> notifiers = Arrays.asList(new EmailNotifier(), new SmsNotifier());

        // Polymorphism: the caller knows only the interface, not the concrete class.
        notifiers.get(0).send("alice@example.com", "Hello from the notification system");
        notifiers.get(1).send("5551234567", "Hello from the notification system");

        // Edge case: shared validation in the abstract class blocks bad input early.
        try {
            new EmailNotifier().send("  ", "This should fail");
        } catch (Exception e) {
            System.out.println("Edge case caught: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }

        // Failure path: a bad domain causes the base retry loop to report the problem clearly.
        try {
            new EmailNotifier().send("bob@baddomain.com", "This one will fail after retries");
        } catch (Exception e) {
            System.out.println("Failure caught: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
    }
}

Follow-up & Tricky Questions:

  • Can an interface have methods with a body? Yes. Since Java 8, interfaces can have default and static methods, and since Java 9 they can also have private helper methods. They still are not a replacement for class state.
  • Can an abstract class have a constructor? Yes. You cannot instantiate it directly, but its constructor runs when a subclass is created, which is how shared fields get initialized safely.
  • Can a class extend an abstract class and implement an interface? Yes, and that is very common. A class gets one parent class for shared code and many interfaces for extra capabilities.
  • When should I prefer an interface over an abstract class? Prefer an interface when you want loose coupling and multiple implementations that may not share the same parent. That is usually the better default for public APIs.
  • Can an interface have fields? Yes, but only constants. They are implicitly public static final, so they are shared values, not per-object variables.
  • Does choosing an interface make code faster? No meaningful difference in real applications. The JVM optimizes both well, and the real cost usually comes from I/O, not from the method call itself.
  • Tricky: If every method is abstract, is an abstract class basically the same as an interface? Not quite. An abstract class still supports constructors, instance fields, and single inheritance, which change how you design and use it.
  • Tricky: Can I create an instance of an abstract class? Not directly. You can only instantiate a concrete subclass, or an anonymous subclass that implements all abstract methods.

Common Mistakes:

  • Mistake: Saying interfaces cannot have any methods with bodies. Correction: Modern Java interfaces can have default, static, and private methods.
  • Mistake: Putting per-object fields in an interface. Correction: Interface fields are constants only; use an abstract class if you need state.
  • Mistake: Choosing an abstract class just because it feels more powerful. Correction: Start with an interface unless you truly need shared code or state.
  • Mistake: Believing one is faster than the other. Correction: The difference is negligible; design quality matters far more.

Memory Hook: Interface = job description. Abstract class = starter kit. A job description tells you what the role must do; a starter kit gives you the tools and the half-built workspace.

Cheat Sheet:

  • Interface = contract or capability.
  • Abstract class = shared base plus partial implementation.
  • Many interfaces, one class parent.
  • Interfaces: no instance state, no constructors.
  • Abstract classes: fields, constructors, concrete methods, abstract methods.
  • Performance choice is negligible; design choice matters.

Practice Tasks:

  • Create an interface Shape with area() and implement it in Circle and Rectangle.
  • Turn a shared helper into an abstract class with a constructor and one concrete validation method.
  • Refactor a class hierarchy so the common behavior moves to an abstract class and the capability contract stays in an interface.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

import java.util.Arrays; import java.util.List; interface Notifier { void send(String recipient, String message); } abstract class BaseNotifier implements Notifier { private final String serviceName; private final int maxRetries; protected BaseNotifier(String serviceName, int maxRetries) { if (serviceName == null || serviceName.isBlank()) { throw new IllegalArgumentException("serviceName must not be blank"); } if (maxRetries < 1) { throw new IllegalArgumentException("maxRetries must be at least 1"); } this.serviceName = serviceName; this.maxRetries = maxRetries; } @Override public final void send(String recipient, String message) { validate(recipient, message); RuntimeException lastFailure = null; for (int attempt = 1; attempt <= maxRetries; attempt++) { try { doSend(recipient, message, attempt); System.out.println(serviceName + " delivered to " + recipient); return; } catch (RuntimeException ex) { lastFailure = ex; System.out.println(serviceName + " attempt " + attempt + " failed: " + ex.getMessage()); } } throw new IllegalStateException( serviceName + " could not deliver after " + maxRetries + " attempts", lastFailure); } protected abstract void doSend(String recipient, String message, int attempt); protected void validate(String recipient, String message) { if (recipient == null || recipient.isBlank()) { throw new IllegalArgumentException("recipient must not be blank"); } if (message == null || message.isBlank()) { throw new IllegalArgumentException("message must not be blank"); } } protected String serviceName() { return serviceName; } } class EmailNotifier extends BaseNotifier { EmailNotifier() { super("Email", 2); } @Override protected void doSend(String recipient, String message, int attempt) { if (recipient.endsWith("@baddomain.com")) { throw new RuntimeException("SMTP rejected recipient"); } System.out.println("Sending email via " + serviceName() + " on attempt " + attempt + ": " + message); } } class SmsNotifier extends BaseNotifier { SmsNotifier() { super("SMS", 3); } @Override protected void doSend(String recipient, String message, int attempt) { if (!recipient.matches("[0-9]{10}")) { throw new RuntimeException("invalid phone number format"); } if (attempt < 2 && message.length() > 20) { throw new RuntimeException("gateway timeout"); } System.out.println("Sending SMS via " + serviceName() + " on attempt " + attempt + ": " + message); } } public class Main { public static void main(String[] args) { List<Notifier> notifiers = Arrays.asList(new EmailNotifier(), new SmsNotifier()); // Polymorphism: the caller knows only the interface, not the concrete class. notifiers.get(0).send("alice@example.com", "Hello from the notification system"); notifiers.get(1).send("5551234567", "Hello from the notification system"); // Edge case: shared validation in the abstract class blocks bad input early. try { new EmailNotifier().send(" ", "This should fail"); } catch (Exception e) { System.out.println("Edge case caught: " + e.getClass().getSimpleName() + " - " + e.getMessage()); } // Failure path: a bad domain causes the base retry loop to report the problem clearly. try { new EmailNotifier().send("bob@baddomain.com", "This one will fail after retries"); } catch (Exception e) { System.out.println("Failure caught: " + e.getClass().getSimpleName() + " - " + e.getMessage()); } } }