An interface is a contract: a list of methods a class promises to provide, with no state and (mostly) no implementation. It is the main tool Java gives you for writing code that does not depend on a specific class — and that is what makes code testable and changeable.
Declaring and implementing one
interface Shape {
double area(); // no body — implementers must supply one
double perimeter();
}
class Circle implements Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override public double area() { return Math.PI * radius * radius; }
@Override public double perimeter() { return 2 * Math.PI * radius; }
}
class Rectangle implements Shape {
private final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override public double area() { return w * h; }
@Override public double perimeter() { return 2 * (w + h); }
}
Interface methods are public and abstract automatically, so those
keywords are usually omitted from the declaration — but the implementing methods must be explicitly
public, because you cannot reduce visibility when overriding.
If a class says implements Shape and misses a method, it does not compile. That is
the contract being enforced.
Why bother — the actual payoff
interface Shape { double area(); }
class Report {
// Takes ANY Shape. Never needs changing when a new shape is written.
double totalArea(List<Shape> shapes) {
double total = 0;
for (Shape shape : shapes) {
total += shape.area();
}
return total;
}
}
Compare that with a method that took a List<Circle>. Adding triangles would mean
editing it. Here, Report depends on the contract, so new implementations arrive
without it noticing.
The same property is what makes testing possible. Depend on an interface and a test can supply a fake implementation instead of a real database or payment gateway:
interface PaymentGateway {
boolean charge(double amount);
}
class OrderService {
private final PaymentGateway gateway;
OrderService(PaymentGateway gateway) { // handed in, not constructed here
this.gateway = gateway;
}
String placeOrder(double amount) {
return gateway.charge(amount) ? "confirmed" : "declined";
}
}
class OrderServiceTest {
void chargesAreDeclinedGracefully() {
OrderService service = new OrderService(amount -> false); // fake, no network
System.out.println(service.placeOrder(20).equals("declined")); // true
}
}
That is dependency injection in its simplest form, and interfaces are what make it work.
Many interfaces, one superclass
interface Swimmer { void swim(); }
interface Flyer { void fly(); }
class Duck implements Swimmer, Flyer {
@Override public void swim() { System.out.println("paddling"); }
@Override public void fly() { System.out.println("flapping"); }
}
A class can implement any number of interfaces but extend only one class. The reason is state: two superclasses could each bring a field with the same name and Java would not know which one an object has. Interfaces have no fields, so the problem never arises.
default and static methods
Java 8 allowed interfaces to carry implementations. A default method has a body that
implementers inherit unless they override it:
interface Shape {
double area();
default String describe() { // implementers get this free
return getClass().getSimpleName() + " with area " + area();
}
static Shape unitSquare() { // a factory, called on the interface
return () -> 1.0;
}
}
This exists for a specific reason worth knowing: backwards compatibility. Adding a
method to an interface breaks every existing implementation. Adding a default method does
not. That is how List gained forEach and removeIf in Java 8
without breaking every List ever written.
Use default for that purpose — evolving an interface, or a genuine convenience derived
from the other methods. Do not use it to smuggle in shared implementation; that is what an abstract
class is for.
Functional interfaces
An interface with exactly one abstract method can be implemented by a lambda:
@FunctionalInterface
interface Validator {
boolean test(String input);
}
class Demo {
void run() {
Validator notBlank = s -> s != null && !s.isBlank();
Validator shortEnough = s -> s.length() <= 10;
System.out.println(notBlank.test("hi")); // true
System.out.println(shortEnough.test("hi")); // true
}
}
The @FunctionalInterface annotation is optional but worth adding — it makes the
compiler reject a second abstract method, so nobody accidentally breaks every lambda using your
interface. This is the foundation of
lambdas and streams, which
posts 19 and 20 cover.
Interface or abstract class?
| Interface | Abstract class | |
|---|---|---|
| State (fields) | No — only constants | Yes |
| Constructors | No | Yes |
| How many per class | Many | One |
| Access modifiers | Public (plus private helpers) | Any, including
protected |
| Says | "can do this" | "is a kind of this" |
The practical rule: reach for an interface first. It keeps the implementer's one inheritance slot free and describes a capability rather than a family. Use an abstract class only when subclasses genuinely share fields or constructor logic that would otherwise be copied.
They combine well, and often should: declare the contract as an interface, and provide an abstract class implementing the tedious parts for anyone who wants it.
Constants, and a warning
Fields in an interface are implicitly public static final. This occasionally tempts
people into an interface that holds nothing but constants, implemented purely to import them —
sometimes called the constant interface antipattern. It leaks implementation detail into your public
type. Use a final class with static fields, or an enum, instead.
Next
static and final have appeared repeatedly without a proper account.
static and final covers both.