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:
defaultmethods — with a body, inherited by implementations, used to evolve interfaces without breaking existing classes (e.g.List.sort).staticmethods — utilities related to the interface (e.g.List.of,Comparator.comparing).privatemethods (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
defaultmethods add behaviour without breaking implementers
Watch out for
- Declaring parameters as concrete classes (
ArrayList) instead of interfaces (List), which makes code rigid.