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 be public — you cannot narrow the visibility. Writing void fly() without public in 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 own Invoice class 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. Animal with its name field, a constructor that sets it, and a concrete eat() method is the textbook case.

Visually, the difference looks like this:

Animal (abstract) Bird Bat Flyable (interface) Drone Airliner solid = "is-a" (one parent) dashed = "can-do" (many allowed) Drone is NOT an Animal — but it implements the same capability.

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 has equals, hashCode, and toString, so redeclaring them in an interface adds no new obligation. This is why java.util.Comparator — which declares both int compare(T, T) and boolean 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 with InterfaceName.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 Object methods don't count. Mark it @FunctionalInterface so 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

Popular posts from this blog

JSP Servlet Interview Questions For Freshers Series 1

Java Banking Finance Services and Insurance (BFSI) domain interview questions

Java program to check even or odd number