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.
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 |
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
// β 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 }
// β 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
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
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.
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.