Layers of Logic

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.

Core20 min read4 exercises
01

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.

02

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.

03

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.

04

Under the hood

Going deeper

The 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 compile

Two 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

  1. A class always beats an interfaceIf a superclass provides the method, that version wins, and no default method is considered at all.
  2. A more specific interface beats a less specific oneIf interface B extends A and both define it, B wins, because B is closer.
  3. 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 one

It 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:

RemovedWhy it was unnecessary
new Greeter()the interface nameknown from the variable type
public void greetthe method namethere is only one. No ambiguity possible.
(String name)the parameter typeknown from the interface
@Overridethe annotationnothing else it could be overriding
{ } and ;the bracesa single expression needs none
Every deleted piece was information the compiler already had. That is the entire design of a lambda.

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.

InterfaceShape, and what it is for
Predicate<T>T in, boolean outa test. filter uses this.
Function<T,R>T in, R outa transformation. map uses this.
Consumer<T>T in, nothing outdo something. forEach uses this.
Supplier<T>nothing in, T outproduce something on demand.
Comparator<T>two T in, int outordering. sort uses this.

Learn those five shapes and most of Phase XI is already familiar, because every stream method takes one of them.

05

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.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. Java 8 added `forEach` to the `Collection` interface. Millions of classes already implemented it. Why did none of them break?

    Show the answer

    Because forEach was 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.

  2. 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.

  3. 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 return are 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.

07

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

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Add a Method Without Breaking Anyone

Real work·25 min·20 points

ex-8-5-a

Feel 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
Evolve.java
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
Add 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
As a default method: 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
Java 8 wanted to add forEach, removeIf and stream to Collection, which is implemented by an enormous amount of code worldwide. Without default methods every one of those additions would have broken every implementation, so lambdas and streams could not have been fitted to the existing collections at all.
What this is really testing

Whether you understand the problem default methods were invented to solve. Feeling the breakage first is what makes the solution land.

B

The Diamond, Reopened

Real work·25 min·25 points

ex-8-5-b

Java 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
Diamond.java
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
To resolve case 1, override greet() in the class and call one explicitly: return Formal.super.greet();. That syntax exists only for this situation.
Hint 2
Case 2 is rule one: a class always beats an interface. Base.greet() wins, and the default method is not even considered.
Hint 3almost the answer
Case 3 is rule two: a more specific interface beats a less specific one. Sub extends Formal, so Sub is closer and its version wins with no ambiguity error.
What this is really testing

Whether you can resolve a default method conflict and explain why Java refuses to guess. This is the diamond problem from Section 7.1 arriving through a door Java thought it had closed.

C

Four Ways, Same Object

Real work·30 min·30 points

ex-8-5-c

Take 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
FourWays.java
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
The method reference is Greeter g = System.out::println;. It works because println(String) has the shape the interface needs.
Hint 2
After compiling, run 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
Adding a second abstract method fails on the interface, because of @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.
What this is really testing

Whether you can go from a named class to a lambda by deletion, understanding every step. Doing this once by hand means Phase XI teaches you nothing new about lambdas.

D

A Registry Built on Interfaces

Hard·40 min·35 points·The Registry

ex-8-5-d

The 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
RegistryInterfaces.java
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
The three lambdas are 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.
What this is really testing

Whether you can put the whole phase together. Interfaces, defaults, a marker, a functional interface and lambdas, in one small design that hangs together.

08

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 method will return in Phase X. The Collections Framework