Interfaces

Java · Practical · 12 min

Programming to interfaces is the backbone of flexible Java design — collections, JDBC, Spring and testing all rely on it.

An interface declares what a type can do without saying how. A class implements it by providing all of its abstract methods:

interface Notifier {
    void send(String to, String message);
}
class EmailNotifier implements Notifier { ... }

A class can extend only one class but implement any number of interfaces, so interfaces are how Java models capabilities: Comparable, Runnable, AutoCloseable, Iterable.

The key habit is programming to the interface: declare variables, parameters and return types with the interface type (List<String> names = new ArrayList<>();, Notifier notifier). Your code then works with any implementation, and swapping one — or injecting a fake in a test — needs no changes.

Since Java 8 interfaces can also contain:

  • default methods — with a body, inherited by implementations, used to evolve interfaces without breaking existing classes (e.g. List.sort).
  • static methods — utilities related to the interface (e.g. List.of, Comparator.comparing).
  • private methods (Java 9+) — helpers shared by default methods.

Interfaces cannot hold instance state; fields declared in them are implicitly public static final constants.

An interface with exactly one abstract method is a functional interface and can be implemented with a lambda — a big topic of its own coming up soon.

Interface vs abstract class: use an interface to define a role many unrelated classes can play; use an abstract class when subclasses genuinely share state and implementation. In modern Java, interfaces are the default choice.

Example

import java.util.ArrayList;
import java.util.List;

public class Notify {
    interface Notifier {
        void send(String to, String message);
        default void sendAll(List<String> recipients, String message) {
            for (String r : recipients) send(r, message);
        }
    }
    static class ConsoleNotifier implements Notifier {
        public void send(String to, String message) { System.out.println("-> " + to + ": " + message); }
    }
    static class RecordingNotifier implements Notifier {    // handy as a test fake
        final List<String> sent = new ArrayList<>();
        public void send(String to, String message) { sent.add(to); }
    }
    static void remind(Notifier n) { n.sendAll(List.of("ana", "bo"), "standup!"); }
    public static void main(String[] args) {
        remind(new ConsoleNotifier());               // -> ana: standup!  -> bo: standup!
        var fake = new RecordingNotifier();
        remind(fake);
        System.out.println(fake.sent);               // [ana, bo]
    }
}

Remember

  • An interface is a contract; a class can implement many
  • Declare variables and parameters with interface types
  • default methods add behaviour without breaking implementers

Watch out for

  • Declaring parameters as concrete classes (ArrayList) instead of interfaces (List), which makes code rigid.

Check yourself

  1. Why were default methods added to interfaces?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/tutorial/java/IandI/createinterface.html