Interfaces
Interfaces are first-class language constructs in Java, distinct from classes. An interface declares a contract — a set of method signatures — that implementing classes must satisfy. Java has expanded the interface mechanism substantially since Java 1.0: default methods (Java 8) admit interface evolution and a limited form of multiple inheritance; static methods (Java 8) admit utility methods directly on interfaces; private methods (Java 9) admit shared implementation between default methods; sealed interfaces (Java 17) admit closed-set type hierarchies. Combined with the functional interface mechanism — interfaces with exactly one abstract method, the substrate for lambdas — Java’s interface surface is one of the most elaborate among mainstream languages.
This page covers interface declarations, the various method kinds, multiple-interface implementation, sealed interfaces, and functional interfaces. The deeper functional-programming surface is in Methods and lambdas and Functional.
Interface declarations
An interface declares a set of method signatures:
public interface Comparable<T> {
int compareTo(T other);
}
public interface Drawable {
void draw();
boolean isVisible();
}
A class implements an interface with implements:
public class Circle implements Drawable {
private boolean visible = true;
@Override
public void draw() { /* render the circle */ }
@Override
public boolean isVisible() { return visible; }
}
The class must implement every abstract method declared in the interface (or be itself abstract). The compiler enforces.
A class may implement multiple interfaces:
public class Widget implements Drawable, Resizable, Configurable {
@Override public void draw() { /* ... */ }
@Override public void resize(int w, int h) { /* ... */ }
@Override public void configure(Map<String, String> opts) { /* ... */ }
}
Abstract methods
The original interface methods are abstract: they have no body and must be implemented by every concrete class:
public interface Vehicle {
void start();
void stop();
double speed();
}
Methods in interfaces are implicitly public abstract; the modifiers may be omitted:
public interface Vehicle {
void start(); // implicitly public abstract
public abstract void stop(); // explicit; equivalent
}
The conventional form omits the redundant modifiers.
Default methods (Java 8)
A default method provides an implementation in the interface itself:
public interface Logger {
void log(String message);
default void logError(String message) {
log("ERROR: " + message);
}
default void logWarning(String message) {
log("WARN: " + message);
}
}
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println(message);
}
// logError and logWarning use the defaults
}
The mechanism admits adding new methods to an interface without breaking existing implementations. The conventional uses:
- Interface evolution — adding methods to a published interface that already has implementations elsewhere.
- Trait-like behaviour — providing reusable functionality that any class implementing the interface gets for free.
- Convenience overloads — alternative call shapes that delegate to the abstract method.
A default method may be overridden by an implementing class:
public class CustomLogger implements Logger {
@Override
public void log(String message) { /* ... */ }
@Override
public void logError(String message) {
// override the default with a different implementation
log("[" + timestamp() + "] ERROR: " + message);
}
}
The default form is the conventional Java mechanism for what other languages call traits or mixins.
The diamond problem
When a class inherits the same default method from multiple interfaces, the compiler diagnoses ambiguity:
public interface A {
default String name() { return "A"; }
}
public interface B {
default String name() { return "B"; }
}
public class C implements A, B {
// ERROR: name() is ambiguous; must override
@Override
public String name() {
return A.super.name() + "/" + B.super.name(); // explicit super-interface
}
}
The class must override the conflicting method; the Interface.super.method() syntax admits delegating to a specific interface’s default. The mechanism is Java’s resolution of the diamond problem in multiple inheritance.
Static methods (Java 8)
Interfaces may declare static methods:
public interface Comparator<T> {
int compare(T a, T b);
static <T> Comparator<T> reverseOrder(Comparator<T> cmp) {
return (a, b) -> cmp.compare(b, a);
}
static <T> Comparator<T> nullsFirst(Comparator<T> cmp) {
return (a, b) -> {
if (a == null) return b == null ? 0 : -1;
if (b == null) return 1;
return cmp.compare(a, b);
};
}
}
Comparator<Integer> reversed = Comparator.reverseOrder(Integer::compare);
Static methods on interfaces are not inherited by implementing classes; they are accessed by the interface name (Comparator.reverseOrder(...)). The mechanism is the conventional Java idiom for utility methods that are conceptually part of the interface’s contract.
Private methods (Java 9)
Java 9 admitted private methods on interfaces:
public interface Logger {
void log(String message);
default void logError(String message) {
prefixedLog("ERROR", message);
}
default void logWarning(String message) {
prefixedLog("WARN", message);
}
private void prefixedLog(String prefix, String message) {
log("[" + prefix + "] " + message);
}
}
Private methods admit sharing implementation between default methods without exposing the helper publicly. The mechanism is the conventional way to factor out common code in default-method implementations.
Private methods are not inherited by implementing classes; they are visible only within the interface itself.
Constants
Interfaces may declare constants: fields that are implicitly public static final:
public interface MathConstants {
double PI = 3.14159265358979;
double E = 2.71828182845904;
double GAMMA = 0.57721566490153;
}
The fields are inherited by implementing classes (which can refer to them by their unqualified name), but the interface as a constant container is generally considered an anti-pattern (the constant interface anti-pattern in Effective Java). The conventional alternative is a final class with static final fields:
public final class MathConstants {
public static final double PI = 3.14159265358979;
public static final double E = 2.71828182845904;
private MathConstants() { } // prevent instantiation
}
Multiple-interface implementation
A class may implement any number of interfaces:
public class Document implements Saveable, Loadable, Cloneable, Comparable<Document> {
@Override public void save(Path path) throws IOException { /* ... */ }
@Override public void load(Path path) throws IOException { /* ... */ }
@Override public Document clone() { /* ... */ }
@Override public int compareTo(Document other) { /* ... */ }
}
The class must implement every abstract method declared in every implemented interface (or inherit a default method that satisfies the contract). The mechanism is Java’s substitute for multiple class inheritance: a class has at most one parent class but any number of implemented interfaces.
Sealed interfaces (Java 17)
A sealed interface restricts which classes may implement it:
public sealed interface Shape
permits Circle, Square, Triangle { }
public final class Circle implements Shape { /* ... */ }
public final class Square implements Shape { /* ... */ }
public final class Triangle implements Shape { /* ... */ }
Each permitted implementation must be final, sealed, or non-sealed. The mechanism admits closed type hierarchies for exhaustive pattern matching:
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Triangle t -> 0.5 * t.base() * t.height();
// exhaustive: every permitted subtype is covered
};
}
The full treatment is in Pattern matching and Classes, records, and sealed types.
Functional interfaces
A functional interface is an interface with exactly one abstract method (after default and static methods are excluded). Lambdas and method references are assignable to functional interfaces; the compiler verifies the signature match.
@FunctionalInterface
public interface IntOperation {
int apply(int x);
}
IntOperation square = x -> x * x; // lambda
IntOperation negate = i -> -i; // lambda
IntOperation absVal = Math::abs; // method reference
int n = square.apply(5); // 25
The @FunctionalInterface annotation is optional but conventional; it informs the compiler to verify the single-abstract-method property.
The standard library provides a substantial set of functional interfaces in java.util.function. The full treatment is in Methods and lambdas and Functional.
What counts as a functional interface
A functional interface has exactly one abstract method. The following do not count toward the abstract count:
- Methods inherited from
Object(equals,hashCode,toString). - Default methods.
- Static methods.
Therefore an interface like Comparator<T>:
@FunctionalInterface
public interface Comparator<T> {
int compare(T a, T b); // abstract
boolean equals(Object obj); // from Object; doesn't count
default Comparator<T> reversed() { /* ... */ } // default; doesn't count
static <T> Comparator<T> reverseOrder() { /* ... */ } // static; doesn't count
default Comparator<T> thenComparing(Comparator<T> other) { /* ... */ }
}
is a functional interface despite having multiple methods, because only compare is abstract.
Marker interfaces
A marker interface is an interface with no methods, used to tag types for runtime or compile-time purposes:
public interface Serializable { } // tag for serialisation
public interface Cloneable { } // tag for clone() support
public interface RandomAccess { } // tag for fast random access
Marker interfaces are tested with instanceof:
if (obj instanceof Serializable) {
// safe to serialise
}
The mechanism predates annotations; modern code typically uses annotations for the same purpose, with the exception of:
Serializable— entrenched in the standard library; the conventional way to mark a class as serialisable.Cloneable— required forObject.clone()to succeed (rather than throwingCloneNotSupportedException).
When to use interfaces vs abstract classes
The conventional decision tree:
| Need | Choose |
|---|---|
| Multiple-interface satisfaction | Interface |
| Interface evolution without breaking implementations | Interface (with default methods) |
| Closed type hierarchy for pattern matching | Sealed interface or sealed class |
| Significant shared state | Abstract class (interfaces have no instance fields) |
| Constructor logic | Abstract class (interfaces have no constructors) |
| A pure contract | Interface |
Interfaces should be the default for declaring types whose principal use is polymorphism. Abstract classes are appropriate when the type needs significant shared implementation, instance fields, or constructor logic.
For maximum design flexibility, libraries often provide both:
- An interface (
List<T>) that declares the contract. - An abstract base class (
AbstractList<T>) that provides shared implementation. - One or more concrete classes (
ArrayList<T>,LinkedList<T>) that extend the abstract base.
Users implement the interface for full control or extend the abstract class for the shared implementation. The standard collections framework follows this pattern throughout.
Common patterns
Strategy
public interface PriceStrategy {
double price(Item item);
}
public class StandardPricing implements PriceStrategy {
@Override
public double price(Item item) { return item.basePrice(); }
}
public class DiscountedPricing implements PriceStrategy {
private final double discount;
public DiscountedPricing(double discount) { this.discount = discount; }
@Override
public double price(Item item) { return item.basePrice() * (1 - discount); }
}
public class Cart {
private final PriceStrategy strategy;
public Cart(PriceStrategy strategy) { this.strategy = strategy; }
public double total(List<Item> items) {
return items.stream().mapToDouble(strategy::price).sum();
}
}
The pattern admits configurable behaviour through dependency injection.
Observer
public interface Listener<T> {
void onEvent(T event);
}
public class Publisher<T> {
private final List<Listener<T>> listeners = new ArrayList<>();
public void subscribe(Listener<T> l) { listeners.add(l); }
public void unsubscribe(Listener<T> l) { listeners.remove(l); }
public void publish(T event) {
for (var l : listeners) l.onEvent(event);
}
}
The pattern admits decoupled event-driven programming.
Command
public interface Command {
void execute();
}
public class Macro implements Command {
private final List<Command> commands;
public Macro(List<Command> commands) { this.commands = commands; }
@Override
public void execute() {
for (var c : commands) c.execute();
}
}
The pattern admits encapsulating actions as objects.
These patterns predate Java 8’s lambdas; with lambdas, simple cases can be expressed as Runnable, Consumer<T>, or Function<T, R> directly without a custom interface. Custom interfaces remain useful when the role carries semantic meaning beyond the bare signature.