8.5
Interfaces Deep Dive
Default methods, static methods, marker interfaces, and the one that changes everything: an interface with exactly one method left to implement.
Previously on
Section 7.2 introduced interfaces as pure contracts: a promise with no code behind it.
Section 7.4 showed you anonymous classes, and made a claim: a lambda is an anonymous class implementing a one-method interface, with the noise removed.
This section names that one-method interface, explains why interfaces stopped being pure contracts in Java 8, and finishes the arc that started in Section 1.1.
The problem
You wrote an interface in 2010. Two hundred classes implement it, across forty companies you have never heard of.
public interface Collection<E> {
boolean add(E e);
boolean remove(Object o);
int size();
}Now it is 2014, lambdas have arrived, and you want to add one method:
void forEach(Consumer<E> action);The moment you do, every one of those two hundred classes stops compiling. Each one now fails to implement a method it has never heard of.
This was a genuine crisis for Java. It meant any widely used interface was frozen for ever. The Collections Framework was in exactly that position, at the moment Java wanted to add lambdas and streams to it.
Adding a method to an interface had to become possible without breaking anyone.
The idea
Default methods
A method in an interface that comes with a body.
public interface Deployable {
boolean isDeployable(); // abstract: implementers must supply this
default String status() { // default: they get this free
return isDeployable() ? "READY" : "HOLD";
}
}Existing classes inherit status() and keep compiling. New classes can override it if they want something better.
That is how forEach, removeIf, stream() and sort() were added to interfaces the whole world had already implemented. It is a compatibility feature that turned out to be generally useful.
Static methods in interfaces
Also since Java 8, an interface can hold static helper methods.
public interface Deployable {
boolean isDeployable();
static Deployable never() {
return () -> false;
}
}
Deployable d = Deployable.never();Before this, helpers lived in a separate class named after the interface plus an “s”: Collection and Collections, Path and Paths. That naming convention exists entirely because interfaces could not hold static methods, and modern Java no longer needs it.
Under the hood
Going deeperThe diamond problem, reopened
Default methods brought back the exact problem Section 7.1 said Java had avoided.
interface Formal { default String greet() { return "Good evening"; } }
interface Casual { default String greet() { return "Hey"; } }
class Greeter implements Formal, Casual { } // will not compileTwo inherited methods with the same signature and different bodies. Java refuses to guess, and the error names both interfaces.
You resolve it by deciding:
class Greeter implements Formal, Casual {
@Override
public String greet() {
return Formal.super.greet(); // choose one explicitly
}
}Note that syntax: InterfaceName.super.method(). It exists only for this situation, and seeing it in code is a sign that two interfaces overlapped.
The resolution rules
When a method could come from several places, Java applies three rules in order:
Which method wins
- A class always beats an interfaceIf a superclass provides the method, that version wins, and no default method is considered at all.
- A more specific interface beats a less specific oneIf interface B extends A and both define it, B wins, because B is closer.
- Otherwise, you must chooseThe class does not compile until you override the method and pick.
Rule one is worth remembering: class wins. A default method can never accidentally replace something a real superclass provides.
Marker interfaces
An interface with no methods at all.
public interface Serializable { } // that is the entire thing
public interface Cloneable { } // and this one
public interface RandomAccess { } // and this oneIt looks useless. It is a label that code can check for.
if (obj instanceof Serializable) {
// now it is safe to write this object to a file
}Modern Java often uses annotations for this instead, such as @FunctionalInterface. Annotations are more flexible, since they can carry values and be applied to methods and fields. Marker interfaces remain useful in one way annotations are not: they create a real type, so a method can take Serializable as a parameter.
And now the important one
A functional interface is an interface with exactly one abstract method.
@FunctionalInterface
public interface Greeter {
void greet(String name);
}Default and static methods do not count, because they already have bodies. So an interface can have twenty methods and still be functional, as long as nineteen of them are default or static. Comparator is exactly this: one abstract compare, and a pile of default and static helpers.
Why one method changes everything
Here is the same behaviour, four ways, each shorter than the last.
// 1. A named class
class EnglishGreeter implements Greeter {
public void greet(String name) { System.out.println("Hello " + name); }
}
Greeter g = new EnglishGreeter();
// 2. An anonymous class (Section 7.4)
Greeter g = new Greeter() {
@Override
public void greet(String name) { System.out.println("Hello " + name); }
};
// 3. A lambda
Greeter g = name -> System.out.println("Hello " + name);
// 4. A method reference
Greeter g = System.out::println;All four produce an object that implements Greeter. Only the amount of typing differs.
Look at what went from 2 to 3:
| Removed | Why it was unnecessary | |
|---|---|---|
| new Greeter() | the interface name | known from the variable type |
| public void greet | the method name | there is only one. No ambiguity possible. |
| (String name) | the parameter type | known from the interface |
| @Override | the annotation | nothing else it could be overriding |
| { } and ; | the braces | a single expression needs none |
The single abstract method is what makes all of this possible. With two methods you would have to say which one you meant. And the moment you say a method name, most of the saving is gone.
The functional interfaces Java gives you
You rarely need to declare your own. java.util.function has the shapes already.
| Interface | Shape, and what it is for | |
|---|---|---|
| Predicate<T> | T in, boolean out | a test. filter uses this. |
| Function<T,R> | T in, R out | a transformation. map uses this. |
| Consumer<T> | T in, nothing out | do something. forEach uses this. |
| Supplier<T> | nothing in, T out | produce something on demand. |
| Comparator<T> | two T in, int out | ordering. sort uses this. |
Learn those five shapes and most of Phase XI is already familiar, because every stream method takes one of them.
What it costs
Default methods blurred a line that used to be clear. “Interface means no code” was easy to teach. Now the difference from an abstract class comes down to state, and to how many you are allowed, which is a subtler answer.
They also reopened the diamond problem. Java kept it out for twelve years by allowing one parent, and default methods let it back in. The compiler makes you resolve it, so it is a compile error rather than a mystery, and it is still one more thing to know.
There is a temptation to overreach with them. An interface that grows behaviour becomes a base class in disguise: all the coupling, and still no ability to hold state.
Marker interfaces are invisible. RandomAccess changes how the standard library behaves and nothing in your code says so. Powerful, and hard to discover.
Lambdas hide their identity too. Stack traces show lambda$main$0, which is far less useful than a class name, and debugging a chain of them is harder than debugging a loop.
What you get is an interface that can evolve. Collection grew forEach, removeIf and stream without breaking a single implementation anywhere in the world. That one capability is what made Java 8 possible at all.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
Java 8 added `forEach` to the `Collection` interface. Millions of classes already implemented it. Why did none of them break?
Show the answer
Because
forEachwas added as a default method: a method in an interface that comes with a body.Before Java 8, adding a method to an interface broke every implementation on earth, because each one now failed to implement something. That made widely used interfaces effectively frozen for ever.
A default method supplies an implementation, so existing classes inherit it and keep compiling. Classes that want something better override it.
This is why default methods exist. Not to make interfaces more like classes, but to let Java add lambdas and streams to collections without breaking the world. It was a compatibility feature that turned out to be generally useful.
A class implements two interfaces, and both have a default method with the same signature. What happens?
Show the answer
It does not compile. Java refuses to guess, and the error names both interfaces.
This is the diamond problem from Section 7.1, arriving through a different door. Java avoided it for classes by allowing only one parent. Default methods reopened the possibility, so the language handles it by requiring you to decide.
You resolve it by overriding the method and choosing explicitly:
public void greet() { Formal.super.greet(); }Note the syntax:
InterfaceName.super.method(). It exists only for this situation.What makes an interface functional, and why does that one property matter so much?
Show the answer
Exactly one abstract method. Default and static methods do not count, because they already have bodies. So an interface can have twenty methods and still be functional, as long as nineteen of them are default or static.
It matters because it removes ambiguity. When there is only one method left to implement, you no longer need to say which method you are implementing. And once the name is unnecessary, so is nearly everything else: the interface name comes from the parameter type, the parameter types are inferred, and the braces and
returnare ceremony.Delete all of it and what remains is a lambda. That is why every lambda in Java targets a functional interface, and why this single property is the doorway to all of Phase XI.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises110 pointsabout 120 minutes
Add a Method Without Breaking Anyone
ex-8-5-aFeel the problem before you see the solution.
Add an abstract method to the interface and count the classes that break. Three here. Now imagine two hundred, spread across companies you have never heard of, in code you cannot edit.
That was Java’s actual position in 2013, with Collection implemented everywhere and lambdas about to arrive.
Then change it to a default method and watch everything compile again with no other edits. Override it in exactly one class to confirm the mechanism works both ways.
Finish with the historical question. Say in two sentences why streams and lambdas could not have been added to Java’s collections without this feature existing first.
What your program must do
- Add status() as an abstract method and record every class that breaks
- Change it to a default method and confirm everything compiles again
- Override it in one class only, and show the other two use the default
- Explain why this feature had to exist before Java could add streams to collections
interface Deployable {
boolean isDeployable();
}
class GroundUnit implements Deployable {
public boolean isDeployable() { return true; }
}
class AirUnit implements Deployable {
public boolean isDeployable() { return false; }
}
class NavalUnit implements Deployable {
public boolean isDeployable() { return true; }
}
public class Evolve {
public static void main(String[] args) {
Deployable[] units = { new GroundUnit(), new AirUnit(), new NavalUnit() };
for (Deployable d : units) System.out.println(d.isDeployable());
// TODO: add an abstract status() to the interface. What breaks?
// TODO: now make it a default method instead. What changes?
// TODO: override it in exactly one class and confirm the others keep the default
}
}
Hint 1
String status(); first and try to compile. Three classes break at once, and imagine that number being two hundred across companies you have never heard of.Hint 2
default String status() { return isDeployable() ? "READY" : "HOLD"; }. Note that it calls an abstract method that has no body yet, which is fine because every real object has one.Hint 3almost the answer
The Diamond, Reopened
ex-8-5-bJava avoided the diamond problem for twelve years by allowing one parent class. Default methods let it back in.
Build all three cases. Predict the winner for cases 2 and 3 before you run anything, because guessing and being wrong is the fastest way to remember the rules.
Case 1 will not compile, and the error names both interfaces. Read it, then resolve it with the syntax that exists for exactly this and nothing else.
Finish by writing the three resolution rules in your own words. If your version of rule one starts with “a class always”, you have it.
What your program must do
- Show that the two-interface case does not compile, and read the error
- Resolve it explicitly and confirm your choice is used
- Predict the winner for cases 2 and 3 before running
- State the three resolution rules in your own words
interface Formal { default String greet() { return "Good evening"; } }
interface Casual { default String greet() { return "Hey"; } }
class Base { public String greet() { return "from the class"; } }
interface Sub extends Formal { default String greet() { return "more specific"; } }
public class Diamond {
// TODO 1: class A implements Formal, Casual -> will not compile. Fix it.
// TODO 2: class B extends Base implements Formal -> which greet wins? Predict first.
// TODO 3: class C implements Sub, Formal -> which greet wins? Predict first.
public static void main(String[] args) {
// TODO: build one of each and print greet()
}
}
Hint 1
greet() in the class and call one explicitly: return Formal.super.greet();. That syntax exists only for this situation.Hint 2
Base.greet() wins, and the default method is not even considered.Hint 3almost the answer
Sub extends Formal, so Sub is closer and its version wins with no ambiguity error.Four Ways, Same Object
ex-8-5-cTake one piece of behaviour from a named class down to a method reference, one deletion at a time.
Write all four and confirm they behave identically. Then compile and look at the folder, because the file listing tells you something the source does not: the anonymous class produced a real class file, and the lambda did not.
Do the same progression for sorting, ending at Comparator.comparingInt. Three forms, one result.
Finish by breaking it. Add a second abstract method to Greeter and note both where the error appears and where it would have appeared without @FunctionalInterface. That difference is the entire argument for writing the annotation.
When you can list every piece the lambda deleted and say how the compiler already knew it, Phase XI has nothing left to teach you about lambda syntax.
What your program must do
- Write all four forms and confirm all four behave identically
- Compile and list the .class files, identifying which form produced which
- Do the sorting progression and confirm all three give the same order
- Add a second abstract method to Greeter and record the exact error and its location
import java.util.*;
import java.util.function.*;
@FunctionalInterface
interface Greeter { void greet(String name); }
public class FourWays {
public static void main(String[] args) {
// TODO 1: a named class implementing Greeter
// TODO 2: an anonymous class
// TODO 3: a lambda
// TODO 4: a method reference
// Then the same progression for sorting:
List<String> units = new ArrayList<>(List.of("Cipher", "Atlas", "Drift", "Beacon"));
// TODO 5: sort by length with an anonymous Comparator
// TODO 6: the same sort with a lambda
// TODO 7: the same sort with Comparator.comparingInt
// TODO 8: add a second abstract method to Greeter. What error appears, and where?
}
}
Hint 1
Greeter g = System.out::println;. It works because println(String) has the shape the interface needs.Hint 2
ls *.class. The anonymous class produced FourWays$1.class. The lambda produced nothing extra, because lambdas use invokedynamic rather than generating a class file.Hint 3almost the answer
@FunctionalInterface, and also at every lambda. That is why the annotation is worth writing: the error arrives where the mistake is rather than everywhere it is used.A Registry Built on Interfaces
ex-8-5-dThe last exercise of Layers of Logic Phase 1. Put the whole phase into one small design.
An interface with all three kinds of method. A marker interface that changes behaviour without declaring anything. A functional interface, called with lambdas.
The filter method is the heart of it. Write it once, then call it with three completely different rules supplied by the caller. That is polymorphism, functional interfaces and lambdas doing one job together.
Then do the last step. Replace your hand-written UnitFilter with java.util.function.Predicate from the standard library, and notice that almost nothing else changes. Your interface and Java’s have exactly the same shape, which is why Java ships the shapes rather than making everyone invent them.
In Phase XI, stream().filter(...) is this method, already written for you, with the loop removed as well.
What your program must do
- Implement the interface with one abstract, one default and one static method
- Use the marker interface to change behaviour with instanceof
- Write filter once and call it with at least three different lambdas
- Replace your UnitFilter with java.util.function.Predicate and show nothing else changes
import java.util.*;
import java.util.function.*;
public class RegistryInterfaces {
// TODO: interface Deployable
// boolean isDeployable();
// default String status() -> READY or HOLD
// static Deployable never() -> always false
//
// TODO: interface Auditable {} - a marker. Some units get audited, some do not.
//
// TODO: @FunctionalInterface interface UnitFilter { boolean test(Unit u); }
//
// TODO: class Unit implements Deployable (and Auditable for some)
//
// TODO: static List<Unit> filter(List<Unit> units, UnitFilter f)
// then call it with lambdas: readiness above 60, active only, auditable only
public static void main(String[] args) {
// build a roster and run several filters through it
}
}
Hint 1
filter takes the list and a UnitFilter, loops, and keeps the units where f.test(u) is true. One method, and the caller supplies the rule.Hint 2
u -> u.getReadiness() > 60, u -> u.isActive(), and u -> u instanceof Auditable. One method, three completely different behaviours.Hint 3almost the answer
Predicate<Unit> has exactly the same shape: one abstract method taking a T and returning boolean. Swapping it in requires changing the parameter type and f.test(u) stays identical, which shows why java.util.function exists at all.After the credits
This is the last section of Phase 1 of Layers of Logic, so this After the Credits is longer. Here is what you are holding, and where all of it lands.
You are one step from lambdas. Every lambda in Java implements a functional interface. You have written one by hand three ways: a named class, an anonymous class, a lambda. You can name every piece that got deleted, and say how the compiler already knew it. Phase XI will not be teaching you a new concept. It will be showing you syntax for something you have already built.
You are holding the whole Collections Framework. Phase X is List, Set, Map and Queue, and you already own every piece it is made of:
| Inside the Collections Framework | You learned it in |
|---|---|
ArrayList is an array that copies itself into a bigger one |
4.2, and you built one |
HashMap is an array of buckets, picked with hash & (n - 1) |
4.2 and 3.1 |
It calls your hashCode() and your equals() |
8.3 |
Each entry lives in a static class Node |
7.4 |
It mixes hash bits downward with >>> |
2.2 |
| Interfaces first, classes second, all the way down | 7.2 and here |
RandomAccess is a marker that changes how binarySearch runs |
here, and 4.2 |
And one warning you have already earned. Put a mutable object in a HashMap as a key, change a field it hashes on, and the entry becomes unreachable. size() keeps counting it, get returns null, remove cannot find it. You made that happen on purpose in the Section 8.3 exercises, and you know the fix is Section 8.2.
Most people meet that bug for the first time in production.
One thread is still open. equals() answers “are these the same?”. It does not answer “which comes first?”. Sorting needs an order, and that is Comparable and Comparator, in Phase X. Both have contracts, and both fail quietly when you break them, exactly like the one you learned here.
Twenty-four sections ago, the first thing you read was that syntax is cheap and understanding is what stays. Look at that list above and decide whether it was true.
Threads you opened in this section
- Default methodComparator.thenComparing chains comparators. It is a default method.Phase XI. Functional Java
- Functional interfaceThis is the doorway. Every lambda in Java is one of these.11.1 - Lambdas and Functional Interfaces
- Functional interfacePredicate, Function, Supplier, Consumer. Four names for four shapes.Phase XI. Functional Java
- Marker interfaceRandomAccess is a marker. Collections.sort checks for it and changes strategy.Phase X. The Collections Framework
Default method will return in Phase X. The Collections Framework