Interfaces, Default Methods & Functional Interfaces
What are you? vs What can you do?
In the last post you built class hierarchies: an Animal base class, Dog and Cat subclasses, behavior overridden up and down the tree. That machinery answers one question: "what are you?" A Dog is an Animal. The hierarchy carries shared state and shared behavior, and everything in it is related by birth.
But real programs keep running into a second question: "what can you do?" A bird can fly. A drone can fly. A commercial airliner can fly. Birds, drones, and airliners share no meaningful "is-a" relationship — shoving them under one base class just to share a fly() method would be nonsense. What they share is a capability, and Java expresses capabilities with interfaces.
All examples in this post target modern Java — Java 17+ idioms, current with Java 25, the latest LTS (September 2025).
Your first interface
An interface declares a capability as a contract: "any class that claims this capability must provide these methods." Here is the whole thing:
interface Flyable {
void fly(); // implicitly: public abstract void fly();
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("flapping wings to stay airborne");
}
}
class Drone implements Flyable {
@Override
public void fly() {
System.out.println("spinning rotors to stay airborne");
}
}
And using it through the interface type — this is polymorphism from the last post, applied to a capability instead of a family:
public class Sky {
public static void launchAll(Flyable[] fleet) {
for (Flyable f : fleet) {
f.fly();
}
}
public static void main(String[] args) {
launchAll(new Flyable[]{ new Bird(), new Drone() });
}
}
// Output:
// flapping wings to stay airborne
// spinning rotors to stay airborne
Notice that launchAll knows nothing about Bird or Drone. It only knows the contract. That is the entire point of an interface: code written against the capability works with any class that provides it — including classes written years later, by people you've never met.
The compiler enforces the contract
implements is a promise, and the compiler collects on it. If a class declares implements Flyable but forgets the fly() method, the code does not compile — unless the class is declared abstract, in which case it passes the obligation down to its subclasses (the abstract-class mechanics from post 05 apply exactly as before).
Two details that trip people up:
- Interface methods are implicitly
public abstract. Your implementation must bepublic— you cannot narrow the visibility. Writingvoid fly()withoutpublicin the class is a compile error, because package-private is narrower than public. - Interface fields are implicitly
public static final. Interfaces can hold constants (int MAX_ALTITUDE = 12000;) but never mutable instance state. An interface describes what implementers do, never what they remember. Remember this — it drives the whole decision rule below.
The decision rule: interface or abstract class?
This is the question that actually matters in real code, and the rule is simple:
- Reach for an interface when the capability cuts across unrelated hierarchies. Flying birds, drones, and airliners.
Comparable— strings, dates, and your ownInvoiceclass can all be compared, and they share no ancestor worth naming.Runnable— anything that can be executed by a thread. - Reach for an abstract class when the subclasses share state, constructors, or a template method — the "is-a" relationship with shared machinery.
Animalwith itsnamefield, a constructor that sets it, and a concreteeat()method is the textbook case.
Visually, the difference looks like this:
A class extends exactly one class but can implement any number of interfaces. That asymmetry is deliberate, and it's the reason the "can-do" side scales: unrelated classes opt into a capability without distorting their real family tree.
One class, many interfaces: multiple inheritance of type
Java famously forbids extending two classes. The reason is state: if both parents had a field called id, or both had constructors demanding different arguments, the child would inherit an unresolvable conflict. Interfaces dodge this entirely because they carry no instance state — so Java happily lets a class implement many of them:
interface Swimmable {
void swim();
}
class Duck extends Animal implements Flyable, Swimmable {
Duck(String name) { super(name); }
@Override public void fly() { System.out.println(name + " takes off"); }
@Override public void swim() { System.out.println(name + " paddles"); }
@Override void makeSound() { System.out.println("quack"); }
}
This is multiple inheritance of type: the Duck is a Flyable and is a Swimmable as far as the type system is concerned, so it can be passed anywhere either capability is expected. It inherits behavior contracts from many sources but state from only one — and that single-source-of-state rule is what keeps the whole thing coherent.
Interfaces can also extend other interfaces — and unlike classes, an interface can extend several:
interface Amphibious extends Swimmable, Flyable { }
Again: safe, because there is no state to collide.
Why interfaces can grow: default methods
Here is the problem default methods were invented to solve. Imagine it is 2013 and you maintain the JDK. You want to add sorting and streaming to every List in existence — that means adding methods to the List and Collection interfaces. But adding an abstract method to an interface breaks every existing implementer: thousands of classes in the wild would suddenly fail to compile because they don't implement the new method. Evolving a widely-implemented interface was effectively impossible.
Java 8's answer: a default method — an interface method with a body. Implementers inherit it automatically, so the interface can grow without breaking anyone, and any class that wants different behavior overrides it like a normal method:
// Simplified version of the real Java 8 story:
interface TaskList {
void add(String task);
int size();
default void addAll(String... tasks) { // new capability, zero breakage
for (String t : tasks) add(t);
}
}
class MyTasks implements TaskList {
private final java.util.List<String> items = new java.util.ArrayList<>();
@Override public void add(String task) { items.add(task); }
@Override public int size() { return items.size(); }
// addAll comes free from the default — no code needed
}
That is exactly how List.sort(Comparator) and Collection.stream() arrived in Java 8: as default methods, so every ArrayList, LinkedList, and custom list implementation ever written gained them overnight without a single recompile. When you call myList.sort(...) today, you are calling a default method.
The diamond problem — and its one-line fix
Default methods reintroduce a mild version of the old multiple-inheritance headache: what if a class implements two interfaces that both define the same default method? Java refuses to guess and makes it a compile error — then gives you a precise tool to resolve it:
interface A {
default void greet() { System.out.println("hello from A"); }
}
interface B {
default void greet() { System.out.println("hello from B"); }
}
class C implements A, B {
@Override
public void greet() {
A.super.greet(); // explicit: use A's version
}
}
public class Main {
public static void main(String[] args) {
new C().greet();
}
}
// Output: hello from A
The rule: if two inherited defaults collide, the class must override and pick — with A.super.greet() syntax to delegate to one of them. No silent winner, no ambiguity. (If instead one interface extends the other and overrides the default, the more specific one wins automatically — the compiler only complains about genuinely unrelated collisions.)
Static methods in interfaces
Interfaces can also carry static methods — pure utility functions namespaced to the interface. They belong to the interface itself, not to implementers: you call them as InterfaceName.method(), they cannot be overridden, and implementing classes do not inherit them:
interface TextUtils {
static boolean isBlank(String s) {
return s == null || s.isBlank();
}
}
public class Main {
public static void main(String[] args) {
System.out.println(TextUtils.isBlank(" ")); // true
System.out.println(TextUtils.isBlank("hi")); // false
}
}
The JDK uses this pattern constantly — List.of("a", "b"), Map.entry(k, v), Comparator.comparing(...) are all static interface methods. They are factory and helper functions living exactly where you'd look for them.
Functional interfaces: the "exactly one" rule
Some interfaces describe a single action — run this, compare these, validate that. Java gives these a special name and a special role: a functional interface is an interface with exactly one abstract method. That single method is what makes the interface usable as a lambda target (more on that in post 14).
The definition is precise, and the precision matters:
- Default methods don't count. An interface can have one abstract method plus any number of defaults and still be functional.
- Static methods don't count. Same reason.
- Methods matching
Object's public methods don't count. Every class already hasequals,hashCode, andtoString, so redeclaring them in an interface adds no new obligation. This is whyjava.util.Comparator— which declares bothint compare(T, T)andboolean equals(Object)— is still a functional interface.
The @FunctionalInterface annotation asks the compiler to verify the count. It is optional, but you should always write it: if someone later adds a second abstract method, the build breaks immediately instead of silently destroying every lambda that used the interface:
import java.math.BigDecimal;
@FunctionalInterface
interface PriceRule {
BigDecimal apply(BigDecimal price); // the ONE abstract method
default PriceRule then(PriceRule next) { // defaults don't count...
return price -> next.apply(this.apply(price));
}
static PriceRule none() { // ...statics don't count either
return price -> price;
}
}
This compiles cleanly, which proves the rule: one abstract method, plus defaults and statics, is still a functional interface. The JDK ships dozens of them in java.util.function — Predicate, Function, Consumer, Supplier — and you will meet all of them in the lambdas and streams posts.
A one-line teaser of what's next
Because PriceRule has exactly one abstract method, you can implement it with a lambda — a compact function literal — instead of writing a whole class. Post 14 will go deep; for now, just see how the contract and the lambda line up:
public class Pricing {
public static void main(String[] args) {
PriceRule student = price -> price.multiply(new BigDecimal("0.90"));
PriceRule coupon = price -> price.subtract(new BigDecimal("5"));
System.out.println(student.then(coupon).apply(new BigDecimal("100")));
}
}
// Output: 85.00
The lambda price -> price.multiply(...) is the implementation of apply — no class, no @Override, no ceremony. The interface defines the shape; the lambda fills it in. That is the whole idea post 14 will unpack.
Marker interfaces, in one line
An interface with no methods at all — like Serializable or Cloneable — is a marker interface: it tags a class with a capability ("my instances may be serialized") where the behavior is provided by the runtime rather than by methods you write.
The trap: an interface begging to be an abstract class
Here is the misuse to watch for. You define an interface, five classes implement it — and every single one starts with the same three fields, the same constructor assigning them, and the same equals logic over those fields. The implementations are copy-pasted with different method bodies on top.
That duplication is the code telling you something: this is not a capability, it is a family with shared state — an abstract class screaming to exist. The fix is to pull the shared fields, constructor, and common behavior into an abstract class, keep the genuinely varying behavior abstract, and — if the capability still cuts across other hierarchies — have that abstract class implement the interface. The two tools compose; the decision rule just tells you which one owns the state.
A quick gut-check before you commit: "Will every implementer duplicate the same fields?" If yes, start with the abstract class. "Could two completely unrelated classes both need this?" If yes, it must be an interface — possibly implemented by an abstract class in the middle.
Recap
- Interface = "can-do." A contract of capabilities across unrelated classes.
implements, many allowed per class, no instance state. - Abstract class = "is-a" with shared machinery. State, constructors, template methods for one family. One parent per class.
- Default methods let interfaces evolve without breaking implementers (
List.sort,Collection.stream). Colliding defaults must be resolved explicitly withInterfaceName.super.method(). - Static methods in interfaces are namespaced helpers (
List.of), called on the interface, never overridden. - Functional interface = exactly one abstract method. Defaults, statics, and
Objectmethods don't count. Mark it@FunctionalInterfaceso the compiler guards the count — and so lambdas can target it. - Marker interfaces (
Serializable) tag a capability with zero methods.
In post 07 you'll learn why == and equals() disagree — and why hashCode() must agree with equals() — the rules that HashMap, HashSet, and records all depend on.
Continue: Java Learning Roadmap 2026
Comments
Post a Comment