Design flexible contracts with interfaces and share logic with abstract classes.
Published September 21, 2026
Both interfaces and abstract classes let you write code against a general type while the specific behaviour comes from implementations you plug in later. They solve different problems, though:
Comparable, Runnable, PaymentGateway. It says nothing about how.Choosing well matters for how easily a codebase can grow, and it's a staple interview question.
public interface PaymentGateway {
PaymentResult charge(Money amount, String customerId); // implicitly public abstract
default boolean supportsRefunds() { // default method (Java 8+): optional to override
return false;
}
static PaymentGateway noop() { // static helper (Java 8+)
return (amount, customer) -> PaymentResult.approved("noop");
}
}
public class StripeGateway implements PaymentGateway {
@Override
public PaymentResult charge(Money amount, String customerId) { /* call Stripe */ }
@Override
public boolean supportsRefunds() { return true; }
}
What an interface can contain:
public.List.sort) without breaking every existing implementation.public static final.What it can't have: instance fields (per-object state) or constructors.
A class can implement many interfaces (class Task implements Runnable, Comparable<Task>, Serializable). This is how Java provides multiple inheritance of type without multiple inheritance of state.
public abstract class ReportGenerator {
private final Clock clock; // shared state
protected ReportGenerator(Clock clock) { this.clock = clock; }
// The fixed algorithm โ final so subclasses can't break the sequence
public final Report generate(ReportRequest request) {
List<Row> rows = fetchData(request); // step supplied by subclasses
List<Row> filtered = applyFilters(rows, request);
return new Report(render(filtered), clock.instant());
}
protected abstract List<Row> fetchData(ReportRequest request); // must implement
protected abstract String render(List<Row> rows); // must implement
protected List<Row> applyFilters(List<Row> rows, ReportRequest r) { // optional hook with a default
return rows;
}
}
public class SalesCsvReport extends ReportGenerator {
public SalesCsvReport(Clock clock) { super(clock); }
@Override protected List<Row> fetchData(ReportRequest r) { /* query sales */ }
@Override protected String render(List<Row> rows) { /* build CSV */ }
}
What an abstract class offers that an interface doesn't:
protected hooks, and private helpers used by subclasses indirectly.final methods that fix an algorithm's structure while delegating specific steps. That's the Template Method pattern shown above.It can't be instantiated, and a class can extend only one abstract class.
| Interface | Abstract class | |
|---|---|---|
| Purpose | A capability / contract | Partial implementation for related classes |
| Instance state | โ (constants only) | โ |
| Constructors | โ | โ |
| Method bodies | default, static, private | Any |
| Access modifiers of members | public (private helpers since Java 9) | Any |
| A class can have | Many | One |
| Relationship expressed | "can do" (Comparable) | "is a kind of" (ReportGenerator) |
List + AbstractList in the JDK). Callers depend on the interface, and implementers extend the base class for convenience.An interface with exactly one abstract method is a functional interface, and it can be implemented with a lambda:
@FunctionalInterface
interface DiscountPolicy { Money apply(Money price); }
DiscountPolicy tenPercentOff = price -> price.multiply(0.9);
@FunctionalInterface is optional, but makes the compiler enforce the single-abstract-method rule. Runnable, Comparator, Function and Predicate are all functional interfaces (see Lambda Expressions).
If a class implements two interfaces that both provide a default method with the same signature, the compiler forces the class to resolve the conflict explicitly:
interface Printer { default String describe() { return "printer"; } }
interface Scanner { default String describe() { return "scanner"; } }
class AllInOne implements Printer, Scanner {
@Override
public String describe() {
return Printer.super.describe() + "+" + Scanner.super.describe(); // choose or combine explicitly
}
}
Rules: a method inherited from a class wins over an interface default; a more specific interface wins over a less specific one; any remaining ambiguity must be resolved by overriding.
Q: Now that interfaces have default methods, why do abstract classes still exist?
A: Interfaces still can't hold instance state or constructors, and their members are public. Abstract classes remain the tool for sharing state and protected implementation, and for enforcing a fixed algorithm with final template methods. Default methods mainly exist to evolve interfaces without breaking implementers.
Q: Can an abstract class have a constructor if it can't be instantiated?
A: Yes. It runs when a concrete subclass is instantiated (through super(...)), and initializes the shared state defined in the abstract class.
Q: Can an interface have fields?
A: Only constants: every field is implicitly public static final. Using interfaces just to hold constants (the "constant interface" pattern) is discouraged. Put constants in the class or enum they belong to.
Q: Why program to interfaces?
A: Code that depends on PaymentGateway rather than StripeGateway can switch implementations, use fakes or mocks in tests, and apply patterns like Strategy or Decorator without changing callers. It's also what makes dependency injection frameworks like Spring effective.
Q: What happens if a class implements two interfaces with the same default method?
A: Compilation fails until the class overrides the method. Inside the override it can call a specific version with InterfaceName.super.method(), or provide completely new behaviour.