Java Interfaces
An interface is a contract: it lists method signatures without bodies. Classes that implement an interface promise to provide bodies for every method. Interfaces give Java a controlled form of multiple inheritance — a class can implement many interfaces, even though it can extend only one class.
This tutorial covers the interface declaration, the implements keyword, default methods (since Java 8), static methods, private methods (Java 9+), sealed interfaces (Java 17+), and the patterns that interfaces enable.
1. Declaring an Interface
public interface Drawable {
class=class="tok-str">"tok-cmt">// implicit public abstract
void draw();
class=class="tok-str">"tok-cmt">// implicit public abstract
double area();
class=class="tok-str">"tok-cmt">// constant - implicit public static final
int MAX_LAYERS = class="tok-num">16;
}
Method signatures have no body, no braces, just a semicolon. They are implicitly public and (in modern Java) implicitly abstract. Fields in an interface are implicitly public static final — i.e. constants.
2. Implementing an Interface
A class declares that it implements an interface with the implements keyword. It must provide bodies for every method (or be declared abstract):
public class Square implements Drawable {
private final double side;
public Square(double side) { this.side = side; }
@Override
public void draw() {
System.out.println("drawing a square");
}
@Override
public double area() {
return side * side;
}
}
class=class="tok-str">"tok-cmt">// a class can implement MULTIPLE interfaces
public class Person
implements Comparable<Person>, java.io.Serializable {
class=class="tok-str">"tok-cmt">// must implement compareTo and the serialization contract
@Override
public int compareTo(Person other) { return class="tok-num">0; }
}
A class can implement multiple interfaces, separated by commas. This is Java's controlled multiple inheritance — safe because interfaces carry no state, only method signatures.
3. Default Methods (Java 8+)
An interface can provide a default implementation with the default keyword. Implementing classes inherit the default unless they override it:
public interface Vehicle {
default void start() {
System.out.println("Starting engine...");
}
default void stop() {
System.out.println("Stopping engine...");
}
int currentSpeed();
}
public class Car implements Vehicle {
@Override
public int currentSpeed() { return class="tok-num">60; }
class=class="tok-str">"tok-cmt">// inherits start() and stop() - no override required
}
class=class="tok-str">"tok-cmt">// override the default
public class ElectricCar implements Vehicle {
@Override
public int currentSpeed() { return class="tok-num">50; }
@Override
public void start() {
System.out.println("Silent start - electric motor");
}
}
Default methods let library authors add methods to existing interfaces without breaking existing implementations. They are also useful for providing convenience methods built on top of abstract ones — the classic example is List's sort being built on top of set and get.
4. Static Methods in Interfaces
Interfaces can hold static factory methods, which is how the Java Collections Framework organises its helpers since Java 8:
public interface StringOps {
static StringOps empty() { return new StringBuilderOps(""); }
StringOps append(String s);
String build();
}
class=class="tok-str">"tok-cmt">// the standard library uses this pattern heavily
List<String> list = List.of("a", "b", "c"); class=class="tok-str">"tok-cmt">// List.of is a static factory
Map<String, Integer> map = Map.of("x", class="tok-num">1, "y", class="tok-num">2);
5. Private Methods (Java 9+)
Default and static methods can share code via private helper methods on the interface:
public interface Validator<T> {
default boolean validate(T input) {
return validateNotNull(input) && validateContent(input);
}
private boolean validateNotNull(T input) {
return input != null;
}
class=class="tok-str">"tok-cmt">// private static helper
private static boolean isBlank(String s) {
return s == null || s.isBlank();
}
boolean validateContent(T input);
}
6. Functional Interfaces and Lambdas
A functional interface has exactly one abstract method. Such interfaces are the target type for lambda expressions:
@FunctionalInterface
public interface StringFunction {
String apply(String input);
}
class=class="tok-str">"tok-cmt">// usage with a lambda
StringFunction upper = s -> s.toUpperCase();
StringFunction lower = s -> s.toLowerCase();
StringFunction exclaim = s -> s + "!";
System.out.println(upper.apply("hi")); class=class="tok-str">"tok-cmt">// "HI"
System.out.println(exclaim.apply("hi")); class=class="tok-str">"tok-cmt">// "hi!"
class=class="tok-str">"tok-cmt">// built-in functional interfaces
java.util.function.Function<String, Integer> length = String::length;
java.util.function.Predicate<String> isEmpty = String::isEmpty;
java.util.function.Consumer<String> print = System.out::println;
java.util.function.Supplier<String> hello = () -> "Hello";
The @FunctionalInterface annotation is optional but recommended — it makes the compiler verify the single-method rule. The java.util.function package contains dozens of built-in functional interfaces: Predicate, Function, Consumer, Supplier, BiFunction, and so on.
7. Sealed Interfaces (Java 17+)
Sealed interfaces restrict which classes or interfaces may implement them. Combined with pattern matching for switch, this gives you closed hierarchies with exhaustive handling:
public sealed interface Result<T> permits Success, Failure {}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String error) implements Result<T> {}
class=class="tok-str">"tok-cmt">// exhaustive switch over a sealed interface
public <T> T unwrap(Result<T> r) {
return switch (r) {
case Success<T> s -> s.value();
case Failure<T> f -> throw new RuntimeException(f.error());
};
}
Result<Integer> r = new Success<>(class="tok-num">42);
System.out.println(unwrap(r)); class=class="tok-str">"tok-cmt">// class="tok-num">42
Sealed hierarchies are especially powerful in domain modelling and for ADT-style (algebraic data type) designs.
8. Multiple Inheritance of Behaviour
A class can implement multiple interfaces. If two interfaces provide conflicting default methods, the class must resolve the conflict explicitly:
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
public class C implements A, B {
class=class="tok-str">"tok-cmt">// must override to disambiguate
@Override
public void hello() {
A.super.hello(); class=class="tok-str">"tok-cmt">// pick A's implementation
class=class="tok-str">"tok-cmt">// or B.super.hello(); // pick B's
class=class="tok-str">"tok-cmt">// or write something entirely new
}
}
This is the only real ambiguity in interface multiple inheritance, and the compiler forces you to disambiguate. In practice it is rare.
9. Abstract Class vs Interface
| Feature | Abstract class | Interface |
|---|---|---|
| State (instance fields) | Yes | No (only constants) |
| Constructors | Yes | No |
| Multiple inheritance | No | Yes (multiple interfaces) |
| Default methods | N/A (has methods) | Since Java 8 |
| Use for | Shared state, common skeleton | Capability, contract |
Rule of thumb: prefer interfaces for capability (any class can be Comparable); use abstract classes when you have shared state or a template-method skeleton.
Exercises
- Define a
Comparable-style interfaceSmallerwith methodboolean isSmallerThan(T other)and implement it on aTemperaturerecord. - Add a
defaultmethod to an interface and have two implementing classes override it differently. - Define a sealed interface
ResultpermittingSuccessandFailure, then switch over it. - Implement two interfaces that both have a default
name()method, and resolve the conflict in the implementing class.