Polyglot
Languages Java interfaces
Java § interfaces

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 for Object.clone() to succeed (rather than throwing CloneNotSupportedException).

When to use interfaces vs abstract classes

The conventional decision tree:

NeedChoose
Multiple-interface satisfactionInterface
Interface evolution without breaking implementationsInterface (with default methods)
Closed type hierarchy for pattern matchingSealed interface or sealed class
Significant shared stateAbstract class (interfaces have no instance fields)
Constructor logicAbstract class (interfaces have no constructors)
A pure contractInterface

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.