You closed module 3 with a solid hierarchy: Material and its three formats, Employee, Loan and a separate presentation layer. Inheritance gave you reuse and polymorphism, but also a constraint: in Java a class can only extend one class. And BiblioTech is already starting to run into that limit. Nexus Software also wants to lend out meeting rooms, which are booked and released just like a book, but which are not catalogue materials: they have no ISBN, no title, no fine for lateness. Do you push them into the Material hierarchy knowing the "is a" relationship is false? Do you duplicate the booking code? Neither of the two.

The answer is the interface: a pure behaviour contract, without state, that any class can sign — and can sign several at once. Interfaces are the mechanism the Java standard library is built on: Comparable, Runnable, List, Iterable, AutoCloseable. Learning them properly is the step that separates writing classes from designing systems, because an interface lets you program against what something does without ever knowing what something is.

Contents

  1. What an interface is: a contract without state
  2. Syntax and implements
  3. Multiple implementation and the diamond problem
  4. An interface is a TYPE: polymorphism by contract
  5. The members of an interface and their implicit modifiers
  6. default methods: evolving an API without breaking anything
  7. default conflicts and Interface.super.method()
  8. static methods in interfaces
  9. private methods in interfaces (Java 9)
  10. Marker interfaces and @FunctionalInterface
  11. Designing with interfaces: contract, dependency inversion and testability
  12. Interface versus abstract class: a preview
  13. BiblioTech: Lendable, Notifiable and MeetingRoom
  14. Common Mistakes and Tips
  15. Exercises

  1. What an interface is: a contract without state

An interface is a declaration of capabilities: a list of operations a class commits to offering, saying nothing about how it fulfils them or what data it stores.

The key word is contract. When you write:

public interface Lendable {
    boolean lend();
    boolean returnItem();
    boolean isAvailable();
    int getLoanDays();
}

you are not writing code that runs. You are writing a promise: "any class that declares itself Lendable will know how to be lent, be returned, say whether it is available and say how long its loan lasts". Whoever receives a Lendable can call those four methods in complete safety, without knowing the concrete class behind it.

Three fundamental notes, worth pinning down from the start:

  • An interface has no instance state. It cannot declare fields that vary per object. It can declare constants, and that is all (section 5).
  • An interface cannot be instantiated. new Lendable() does not compile: there is nothing to build. What gets instantiated is a class that implements it.
  • The relationship it expresses is "can do", not "is a". Book is a Material (inheritance). Book can be lent (interface). That distinction resolves 80 % of design doubts.
Class Interface
Answers the question What is this? What can this do?
Provides State + behaviour Contract (and default behaviour)
Relationship with the subclass extends, only one implements, as many as you want
Domain example Book extends Material Book implements Lendable
JDK example String extends Object String implements Comparable, CharSequence

  1. Syntax and implements

An interface is declared in its own .java file, with the same name, exactly like a class:

package com.nexussoftware.bibliotech.domain;

/**
 * Contract for everything Nexus Software can lend to an employee:
 * catalogue materials, meeting rooms, computer equipment...
 */
public interface Lendable {

    /** @return true if the operation had an effect. */
    boolean lend();

    /** @return true if the operation had an effect. */
    boolean returnItem();

    /** @return true if the resource is free right now. */
    boolean isAvailable();

    /** @return days this resource's loan lasts. */
    int getLoanDays();
}

Notice what does not appear: there is no public on the methods (it is implicit), there is no abstract (also implicit), and the methods end in ; instead of { ... }, because they have no body.

A class signs the contract with implements:

public class MeetingRoom implements Lendable {

    private final String  code;
    private final int     capacity;
    private boolean       free;

    public MeetingRoom(String code, int capacity) {
        this.code     = code;
        this.capacity = capacity;
        this.free     = true;
    }

    @Override
    public boolean lend() {
        if (!free) { return false; }
        free = false;
        return true;
    }

    @Override
    public boolean returnItem() {
        if (free) { return false; }
        free = true;
        return true;
    }

    @Override
    public boolean isAvailable() { return free; }

    @Override
    public int getLoanDays() { return 1; }   // a room is booked by the day

    public String getCode()    { return code; }
    public int    getCapacity() { return capacity; }
}

Two compilation rules you must know:

  1. You must implement ALL the abstract methods of the interface. If you forget one, the compiler says MeetingRoom is not abstract and does not override abstract method getLoanDays() in Lendable. The alternative is to declare the class abstract, and then the obligation passes to its subclasses (lesson 04-02).
  2. The implemented methods must be public. Since the interface method is implicitly public, reducing visibility when implementing it is an error: attempting to assign weaker access privileges.

@Override is not mandatory, but always use it: it is the same safety net you learned in 03-05, and here it catches that you wrote isAvailable() with one letter too many.

A class can also extend a class and implement interfaces at the same time, and the order in the declaration is fixed: first extends, then implements.

public class Book extends Material implements Comparable<Book> { ... }

  1. Multiple implementation and the diamond problem

This is the whole reason interfaces exist. A class can implement as many interfaces as it wants, separated by commas:

public class Material implements Lendable, Notifiable {
    // it must fulfil both contracts
}

But it can only extend one class. Why the asymmetry? The answer is called the diamond problem.

Imagine Java allowed multiple inheritance of classes and that these two existed:

class PhysicalResource {
    protected int code = 100;
    public String locate() { return "Shelf " + code; }
}

class DigitalResource {
    protected int code = 200;
    public String locate() { return "Server " + code; }
}

// THIS DOES NOT EXIST IN JAVA:
class HybridBook extends PhysicalResource, DigitalResource { }
classDiagram
    class Object
    class PhysicalResource {
        +code int
        +locate() String
    }
    class DigitalResource {
        +code int
        +locate() String
    }
    class HybridBook
    Object <|-- PhysicalResource
    Object <|-- DigitalResource
    PhysicalResource <|-- HybridBook
    DigitalResource <|-- HybridBook

The drawing is diamond-shaped — hence the name — and it raises questions with no single answer:

  • Does hybrid.locate() run the physical version or the digital one?
  • Is hybrid.code 100 or 200? Or does the object have two code fields?
  • If PhysicalResource and DigitalResource both inherit from the same base class with state, is that state stored once or twice?

Languages that allow multiple inheritance (C++, for instance) solve this with complex rules: virtual inheritance, explicit scope qualification, linearisation order. Java took the opposite decision in 1995: a single superclass, full stop. The complexity is eliminated at the root.

Interfaces escape the problem because, in their original form, they provide neither state nor implementation. If ten interfaces declare int getLoanDays();, the implementing class writes a single body that satisfies all ten at once. No ambiguity is possible: there are not two versions to choose between, there are zero.

Conflict Multiple inheritance of classes Multiple implementation of interfaces
Duplicated state Yes: two code fields Impossible: there is no instance state
Two bodies for the same method Yes: ambiguity No: the class writes the only body
Chained constructors Which one runs first? Interfaces have no constructor

That was exactly true until Java 8, when default methods arrived and with them a small residual diamond. You will see it resolved in section 7.

  1. An interface is a TYPE: polymorphism by contract

This is the section that gives real value to everything above. An interface, even though it cannot be instantiated, is a valid Java type: you can declare variables, parameters, return values and arrays with it.

Lendable resource = new MeetingRoom("ROOM-A", 12);   // upcasting to the interface
resource.lend();
System.out.println(resource.isAvailable());          // false

The variable resource does not know there is a room behind it. It only knows the four methods of the contract. It is exactly the polymorphism of 03-06 (the declared type decides what you can call, the actual type decides what runs), but now the declared type is not a superclass but a contract that classes with no kinship whatsoever can sign.

And here is the benefit, in a method that serves the whole Nexus Software inventory:

/** Availability report valid for books, magazines, DVDs and rooms. */
public static void reportAvailability(Lendable[] resources) {
    int free = 0;
    for (Lendable l : resources) {
        System.out.printf("  %-12s term %2d days  %s%n",
                          l.getClass().getSimpleName(),
                          l.getLoanDays(),
                          l.isAvailable() ? "FREE" : "BUSY");
        if (l.isAvailable()) { free++; }
    }
    System.out.printf("Available: %d of %d%n", free, resources.length);
}
Lendable[] inventory = {
    new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
    new Dvd("Refactoring Live", "DVD-0007", 95),
    new MeetingRoom("ROOM-A", 12)
};
reportAvailability(inventory);
  Book         term 15 days  FREE
  Dvd          term  3 days  FREE
  MeetingRoom  term  1 days  FREE
Available: 3 of 3

Book and MeetingRoom share no superclass (beyond Object), share no fields, share nothing. And even so they travel together in the same array and respond to the same call. That is what inheritance could not give you.

An array is still being used because collections (ArrayList and friends) arrive in module 5. It is provisional, as it has been since module 3.

  1. The members of an interface and their implicit modifiers

Interfaces apply default modifiers that are not written out. Knowing them avoids surprises.

Member Implicit modifiers Can they be omitted? Since
Method without body public abstract Yes, and you must omit them Java 1.0
Field public static final Yes Java 1.0
default method public Yes (default is mandatory) Java 8
static method public Yes (static is mandatory) Java 8
private method none (private explicit) No Java 9
Nested type (class, interface, enum) public static Yes Java 1.1

Two consequences that surprise everybody:

Every field of an interface is a constant. This:

public interface Lendable {
    int MAX_TERM_DAYS = 30;     // really: public static final int
}

does not declare an instance field but a shared constant accessed as Lendable.MAX_TERM_DAYS. You cannot assign it another value, neither in the constructor nor anywhere else. If you try MAX_TERM_DAYS = 40; you get cannot assign a value to final variable.

Instance members do not exist. An interface never stores per-object data. If your design needs the contract to carry associated state, the contract is not what you are looking for: you need an abstract class (04-02).

Antipattern: the "constant interface". Before enums it was common to create an interface just to group constants and implement it in order to "inherit" them unqualified. It is a recognised bad practice: it pollutes the class's public API with implementation details. For constants use a final class with static final members, or better an enum (04-07).

  1. default methods: evolving an API without breaking anything

Until Java 7, adding a method to a published interface was a catastrophe: every single class implementing it in the world stopped compiling at once. Think about the scale of the problem: when the Java team wanted to add forEach and stream to java.util.Collection in 2014, there were millions of classes implementing List outside their control.

The solution was default methods: interface methods with a body, which implementing classes inherit for free and can override if they want.

public interface Lendable {

    boolean lend();
    boolean returnItem();
    boolean isAvailable();
    int     getLoanDays();

    /**
     * Days left in the term. The default implementation is enough
     * for most resources; those with their own rules override it.
     */
    default int daysRemaining(int elapsedDays) {
        return Math.max(0, getLoanDays() - elapsedDays);
    }

    /** @return true if the term has been exceeded. */
    default boolean isOverdue(int elapsedDays) {
        return daysRemaining(elapsedDays) == 0 && elapsedDays > 0;
    }
}

Adding these two methods does not break MeetingRoom or Material: both keep compiling without touching a line and gain the two new methods.

Four key points about defaults:

  • A default can only use the contract itself. Notice that daysRemaining calls getLoanDays(), an abstract method of the interface. It cannot access fields, because there are no fields.
  • They can be overridden. A class that needs another formula writes its own daysRemaining with @Override, and its version wins by dynamic dispatch.
  • They are not a replacement for the abstract class. They are an API evolution mechanism, not a way to push business logic into interfaces. If you find yourself writing long defaults full of logic, review the design.
  • Object always wins. You cannot declare a default for toString, equals or hashCode: the compiler rejects it. Object's implementations have structural priority over any default.

  1. default conflicts and Interface.super.method()

With defaults, the diamond returns in a reduced version. If a class implements two interfaces that provide the same default, there is real ambiguity:

public interface Lendable {
    default String describeTerm() { return "Standard loan term"; }
}

public interface Reservable {
    default String describeTerm() { return "Booking term in time slots"; }
}

public class MeetingRoom implements Lendable, Reservable { }   // DOES NOT COMPILE
error: class MeetingRoom inherits unrelated defaults for describeTerm()
       from types Lendable and Reservable

Java does not guess: it forces you to decide. The class must override the method, and inside it can explicitly invoke a specific interface's version with the syntax InterfaceName.super.method():

public class MeetingRoom implements Lendable, Reservable {

    @Override
    public String describeTerm() {
        // Option A: keep one of them
        return Reservable.super.describeTerm();

        // Option B: combine them
        // return Lendable.super.describeTerm() + " / " + Reservable.super.describeTerm();

        // Option C: write something entirely your own
        // return "Room bookable in half-day slots";
    }
}

The resolution rules the compiler applies, in order:

  1. The class beats the interface. A method inherited from a superclass has priority over any default (class wins).
  2. The most specific interface wins. If Reservable extends Lendable and both define the default, Reservable wins.
  3. If there is no winner, compilation error, and the class must resolve it with Interface.super.method().

The difference from the classic diamond is decisive: the conflict is detected at compile time and only affects behaviour, never state. There are never two copies of a field.

  1. static methods in interfaces

Java 8 also allowed static methods with a body inside an interface. They are utilities tied to the contract, invoked through the interface name and not inherited by implementing classes.

public interface Lendable {

    int MAX_TERM_DAYS = 30;

    boolean lend();
    boolean returnItem();
    boolean isAvailable();
    int     getLoanDays();

    /** Checks that a proposed term respects the company policy. */
    static boolean isValidTerm(int days) {
        return days > 0 && days <= MAX_TERM_DAYS;
    }

    /** Counts how many resources in the array are free. */
    static int countAvailable(Lendable[] resources) {
        int total = 0;
        for (Lendable l : resources) {
            if (l.isAvailable()) { total++; }
        }
        return total;
    }
}
System.out.println(Lendable.isValidTerm(15));               // true
System.out.println(Lendable.isValidTerm(45));               // false
System.out.println(Lendable.countAvailable(inventory));     // 3

// MeetingRoom.isValidTerm(15);   // DOES NOT COMPILE: interface statics are not inherited

Their usefulness: keeping the interface and its utilities together, instead of creating a separate helper class. Before Java 8 this was not possible, and that is why the JDK is full of pairs like Collection/Collections or Path/Paths. With static methods in interfaces that split is no longer needed: that is why modern APIs offer List.of(...), Comparator.comparing(...) or Path.of(...) directly.

  1. private methods in interfaces (Java 9)

With several defaults in an interface, duplicated code between them appears quickly. Pulling it out into a public method would make it part of the contract, which is not what you want. Java 9 allowed private methods in interfaces for exactly that:

public interface Notifiable {

    String getNoticeChannel();
    String buildNotice(int elapsedDays);

    default String urgentNotice(int elapsedDays) {
        return header("URGENT") + buildNotice(elapsedDays);
    }

    default String routineNotice(int elapsedDays) {
        return header("INFO") + buildNotice(elapsedDays);
    }

    /** Shared implementation detail: it is NOT part of the contract. */
    private String header(String level) {
        return "[" + level + " / " + getNoticeChannel() + "] ";
    }
}

There are two variants:

  • private: can be called from the default methods (it has access to this).
  • private static: can be called from the defaults and from the statics, but has no access to this.

Neither is visible from outside nor from the implementing classes. They are pure internal detail.

  1. Marker interfaces and @FunctionalInterface

A marker interface is an interface with no methods at all. It promises no behaviour: it promises a property, which the compiler or the library checks at run time.

public interface Auditable { }   // no methods: just a marker

The three from the JDK you will come across:

Marker interface What it marks Where it is studied
java.io.Serializable The object can be turned into bytes and stored Module 7
java.lang.Cloneable Object.clone() can copy it (discouraged, 03-09) —
java.util.RandomAccess The list supports efficient indexed access Module 5

Today annotations (@Deprecated, @Override, and your own in 10-02) cover a good part of these cases with more flexibility, but markers are still there because they have an advantage annotations do not: they create a type, and therefore the compiler can require them in a signature (void save(Serializable object)).

A separate case is @FunctionalInterface: it is not a marker interface but an annotation applied to interfaces with exactly one abstract method, which enables the use of lambdas. It is mentioned here so you recognise the word; its full meaning, the java.util.function catalogue and method references are the content of lesson 04-06.

  1. Designing with interfaces: contract, dependency inversion and testability

Interfaces are not just a trick to dodge multiple inheritance. They are Java's central design tool, and these three ideas explain why.

Program against the contract, not against the implementation. Always declare with the most general type that works for you:

Lendable resource = new MeetingRoom("ROOM-A", 12);   // good: you depend on the contract
MeetingRoom room  = new MeetingRoom("ROOM-A", 12);   // only if you need getCapacity()

With the first form, switching to another implementation tomorrow costs one line. With the second, it costs a review of everything that uses the variable.

Dependency inversion, in one sentence. The important modules (the business rules) must not depend on the detail modules (the database, the console, the network): both must depend on an interface defined by the important module. In BiblioTech, LoanManager should not depend on ConsoleReceipt but on a Receipt interface that ConsoleReceipt implements; that way, switching to PdfReceipt does not touch the manager. That is the mechanism Spring and dependency injection rest on in module 11.

Testability. If LoanManager depends on the Receipt interface, in a test you can pass it a fake implementation that only counts how many times it was called, printing nothing. Without the interface, testing the manager forces you to capture System.out. This technique — mocks — is JUnit and Mockito in module 11, and its feasibility is decided here, the moment you choose to depend on a contract or on a concrete class.

  1. Interface versus abstract class: a preview

The question arrives inevitably: if a default can carry a body, how does an interface differ from an abstract class? In essence:

  • An interface has no instance state and no constructor, and several can be implemented.
  • An abstract class does have fields, a constructor and protected members, but you can only extend one.

Starting rule: the interface defines what can be done; the abstract class shares how it is done and what data is needed. The full comparison table, with the six dimensions that matter and a practical decision rule, is the content of lesson 04-02, where you will also see that the usual thing is not to choose but to combine them.

  1. BiblioTech: Lendable, Notifiable and MeetingRoom

Let us apply all of this to the project. Material today has two groups of responsibilities mixed together: being lent and issuing notices. We are going to extract them as two independent contracts.

Lendable

package com.nexussoftware.bibliotech.domain;

/** Contract for every resource Nexus Software lends to its employees. */
public interface Lendable {

    /** Maximum term the company policy allows. */
    int MAX_TERM_DAYS = 30;

    boolean lend();
    boolean returnItem();
    boolean isAvailable();
    int     getLoanDays();

    /** Days of term left; 0 if it has already expired. */
    default int daysRemaining(int elapsedDays) {
        return Math.max(0, getLoanDays() - elapsedDays);
    }

    /** @return true if the term has been exceeded. */
    default boolean isOverdue(int elapsedDays) {
        return elapsedDays > getLoanDays();
    }

    static boolean isValidTerm(int days) {
        return days > 0 && days <= MAX_TERM_DAYS;
    }

    static int countAvailable(Lendable[] resources) {
        int total = 0;
        for (Lendable l : resources) {
            if (l.isAvailable()) { total++; }
        }
        return total;
    }
}

Notifiable

package com.nexussoftware.bibliotech.domain;

/** Contract for everything BiblioTech can issue notices about. */
public interface Notifiable {

    /** Channel used for notices: "email", "chat", "phone"... */
    String getNoticeChannel();

    /** Notice text for the given number of elapsed days. */
    String buildNotice(int elapsedDays);

    /** Notice with a level prefix, built on top of the contract. */
    default String urgentNotice(int elapsedDays) {
        return header("URGENT") + buildNotice(elapsedDays);
    }

    default String routineNotice(int elapsedDays) {
        return header("INFO") + buildNotice(elapsedDays);
    }

    private String header(String level) {
        return "[" + level + " / " + getNoticeChannel() + "] ";
    }
}

Material signs both contracts

The change in Material is a single line, because the methods already exist since module 3:

public class Material implements Lendable, Notifiable {

    // ... constants, fields and constructor unchanged ...

    @Override public boolean lend()            { /* unchanged */ }
    @Override public boolean returnItem()      { /* unchanged */ }
    @Override public boolean isAvailable()     { return available; }
    @Override public int     getLoanDays()     { return LOAN_DAYS; }

    @Override public String  getNoticeChannel() { return "email"; }
    @Override public String  buildNotice(int elapsedDays) { /* unchanged */ }

    // ... everything else the same ...
}

That the refactoring costs one line is the best possible sign: it means that in module 3 you identified the responsibilities correctly. The only new thing is that now those responsibilities have a name and are a type.

MeetingRoom: the advantage over inheritance

package com.nexussoftware.bibliotech.domain;

/** Nexus Software meeting room. It is booked, but it is NOT a material. */
public class MeetingRoom implements Lendable {

    private final String code;
    private final int    capacity;
    private boolean      free;
    private String       occupiedBy;

    public MeetingRoom(String code, int capacity) {
        this.code       = (code == null || code.isBlank()) ? "ROOM-000" : code.trim();
        this.capacity   = Math.max(1, capacity);
        this.free       = true;
        this.occupiedBy = null;
    }

    @Override
    public boolean lend() {
        if (!free) {
            System.out.println("WARNING: room " + code + " is already booked.");
            return false;
        }
        free = false;
        return true;
    }

    @Override
    public boolean returnItem() {
        if (free) { return false; }
        free       = true;
        occupiedBy = null;
        return true;
    }

    @Override public boolean isAvailable() { return free; }
    @Override public int     getLoanDays() { return 1; }

    /** A room has no fine: it overrides the default because its rule is different. */
    @Override
    public boolean isOverdue(int elapsedDays) {
        return false;
    }

    public String getCode()     { return code; }
    public int    getCapacity() { return capacity; }
}

Notice what has been achieved:

  • MeetingRoom inherits from nothing. It has no title, no reference, no fine, no rate. Its only debt is the Lendable contract.
  • It does not implement Notifiable, because no notices are sent about rooms. The contracts are independent: they are signed separately.
  • It overrides a default (isOverdue) because its business rule differs.
  • And even so, it travels in the same array as a Book and responds to reportAvailability.

With inheritance this was impossible without lying: either MeetingRoom extends Material (false: it is not a material and would drag along ISBN and fines), or you duplicated the booking code.

classDiagram
    class Lendable {
        <<interface>>
        +MAX_TERM_DAYS int
        +lend() boolean
        +returnItem() boolean
        +isAvailable() boolean
        +getLoanDays() int
        +daysRemaining(int) int
        +isOverdue(int) boolean
    }
    class Notifiable {
        <<interface>>
        +getNoticeChannel() String
        +buildNotice(int) String
        +urgentNotice(int) String
        +routineNotice(int) String
    }
    class Material {
        -title String
        -reference String
        -available boolean
        +calculateFine(int) double
        +describe() String
    }
    class Book
    class Magazine
    class Dvd
    class MeetingRoom {
        -code String
        -capacity int
        +getCapacity() int
    }
    Lendable <|.. Material
    Notifiable <|.. Material
    Lendable <|.. MeetingRoom
    Material <|-- Book
    Material <|-- Magazine
    Material <|-- Dvd

In the diagram, the dashed line is implements and the solid one is extends. It reads at a glance: two contracts, two families signing them to different degrees, and no forced kinship.

Common Mistakes and Tips

Believing an interface can have instance fields. int counter; inside an interface is not a mutable field: it is public static final int counter, and without an initialiser it does not even compile. If you need state shared between implementations, you need an abstract class (04-02).

Reducing visibility when implementing. If you write boolean lend() without public in the class, the compiler fails: interface methods are public and cannot be restricted. It is the most frequent mistake in a first implementation.

Forgetting @Override. Without the annotation, writing isAvailables() does not produce an immediate error: it creates a new method and the compiler complains much later, with a message about unimplemented abstract methods. @Override points at the exact spot of the failure.

Using the interface as a bag of constants. It is a well-known antipattern. For grouped constants use an enum (04-07) or a final class with static final members.

Filling the interface with default methods. defaults exist to evolve APIs, not to sneak business logic into the contract. A thirty-line default is almost always a misplaced abstract class.

Interfaces that are too big. If Lendable accumulated fifteen methods, no class could implement it without empty methods. Several small, cohesive interfaces — Lendable, Notifiable, Reservable — are preferable to one giant one. The principle is called interface segregation and you will formalise it in 12-02.

Tip: name interfaces after the capability. The Java convention favours adjectives or participles (Lendable, Notifiable, Comparable, Runnable, Serializable) over nouns, precisely because they describe what something can do. And avoid ILendable-style prefixes: they are not a convention in Java.

Tip: declare with the contract type. Lendable resource = ... instead of MeetingRoom room = ... whenever you do not need the class's own methods. It is the practice that makes changing the implementation tomorrow possible.

Exercises

Exercise 1: the Catalogable interface

Create an interface Catalogable in com.nexussoftware.bibliotech.domain with:

  • A constant String CARD_PREFIX = "CARD-".
  • Two abstract methods: String getReference() and String getTitle().
  • A method default String buildCard() returning CARD-<reference>: <title>.
  • A method static boolean isValidReference(String ref) returning true if the reference is not null and has at least 5 characters.

Then make Material implement it (check that not one new method has to be written) and test buildCard() with "Effective Java".

Exercise 2: resolving a default conflict

Create two interfaces, Lendable and Rentable, both with a default String policy() returning different texts ("Free loan for employees" and "Rental with a daily rate"). Create a class PortableProjector that implements both. Check that it does not compile, and resolve it with Interface.super.method() returning the concatenation of both policies.

Exercise 3: a heterogeneous inventory

Write a method static void inventorySummary(Lendable[] resources) that walks the array and shows, for each resource: its class name, whether it is available, and its term. At the end it must print how many are Notifiable (using instanceof) and the total available with Lendable.countAvailable. Test it with two books, one DVD and two rooms.

Solutions

Solution 1

package com.nexussoftware.bibliotech.domain;

/** Contract for every item appearing in the BiblioTech catalogue. */
public interface Catalogable {

    // implicit public static final
    String CARD_PREFIX = "CARD-";

    // implicit public abstract
    String getReference();
    String getTitle();

    /**
     * Catalogue card. It only uses the contract itself: that is why it can
     * have a body without accessing any field.
     */
    default String buildCard() {
        return CARD_PREFIX + getReference() + ": " + getTitle();
    }

    /** Utility associated with the contract; NOT inherited by the classes. */
    static boolean isValidReference(String ref) {
        return ref != null && ref.trim().length() >= 5;
    }
}

Material only changes in its declaration:

public class Material implements Lendable, Notifiable, Catalogable {
    // getReference() and getTitle() ALREADY EXIST since 03-07: nothing to add
}
Catalogable c = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(c.buildCard());
System.out.println(Catalogable.isValidReference("978-0000000001"));
System.out.println(Catalogable.isValidReference("123"));
CARD-978-0000000001: Effective Java
true
false

The important part of the exercise: implementing the interface did not cost a single line of body. When the methods already exist with the right signature, signing the contract is declarative. And buildCard() reaches Book, Magazine and Dvd for free, all at once.

Solution 2

public interface Lendable2 {
    default String policy() { return "Free loan for employees"; }
}

public interface Rentable {
    default String policy() { return "Rental with a daily rate"; }
}

Without overriding, the error is:

error: class PortableProjector inherits unrelated defaults for policy()
       from types Lendable2 and Rentable

The resolution:

public class PortableProjector implements Lendable2, Rentable {

    private final String code;

    public PortableProjector(String code) { this.code = code; }

    /**
     * The compiler forces a decision. Here both policies are combined:
     * the projector is free for employees, but rented out to externals.
     */
    @Override
    public String policy() {
        return Lendable2.super.policy() + "; " + Rentable.super.policy();
    }

    public String getCode() { return code; }
}
System.out.println(new PortableProjector("PRO-01").policy());
Free loan for employees; Rental with a daily rate

Note the syntax Lendable2.super.policy(): it is the only way to explicitly invoke a specific interface's default, and it is only valid if the class implements that interface directly. It is the mechanism with which Java keeps multiple implementation without the diamond problem: the conflict is not resolved by hidden rules, the programmer resolves it, and it only affects behaviour, never state.

Solution 3

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domain.*;

public class InventoryApp {

    /** Summary valid for any lendable resource, whatever its type. */
    public static void inventorySummary(Lendable[] resources) {
        System.out.println("=== INVENTORY OF LENDABLE RESOURCES ===");

        int notifiables = 0;

        for (Lendable l : resources) {
            System.out.printf("  %-15s %-10s term %2d days%n",
                              l.getClass().getSimpleName(),
                              l.isAvailable() ? "FREE" : "BUSY",
                              l.getLoanDays());

            // instanceof with type pattern (03-06): checks and converts
            if (l instanceof Notifiable n) {
                notifiables++;
                System.out.printf("      notice channel: %s%n", n.getNoticeChannel());
            }
        }

        System.out.printf("Notifiable resources: %d of %d%n", notifiables, resources.length);
        System.out.printf("Available now:        %d of %d%n",
                          Lendable.countAvailable(resources), resources.length);
    }

    public static void main(String[] args) {

        Lendable[] inventory = {
            new Book("Effective Java",  "Joshua Bloch",  "978-0000000001", 2018),
            new Book("Design Patterns", "Erich Gamma",   "978-0000000002", 1994),
            new Dvd("Refactoring Live", "DVD-0007", 95),
            new MeetingRoom("ROOM-A", 12),
            new MeetingRoom("ROOM-B", 4)
        };

        inventory[0].lend();              // Marta Ruiz takes Effective Java
        inventory[3].lend();              // and books ROOM-A

        inventorySummary(inventory);
    }
}
=== INVENTORY OF LENDABLE RESOURCES ===
  Book            BUSY       term 15 days
      notice channel: email
  Book            FREE       term 15 days
      notice channel: email
  Dvd             FREE       term  3 days
      notice channel: phone
  MeetingRoom     BUSY       term  1 days
  MeetingRoom     FREE       term  1 days
Notifiable resources: 3 of 5
Available now:        3 of 5

Three observations about the solution:

  1. The method does not mention a single concrete class. It only knows Lendable and Notifiable. Adding ComputerEquipment implements Lendable tomorrow does not force you to touch a line of inventorySummary.
  2. instanceof with a pattern picks out those that also sign Notifiable, and on the same line declares the variable n already converted. It is not a design smell like the type switches of 03-06: here behaviour is not chosen by type, an optional capability is checked.
  3. Lendable.countAvailable is invoked on the interface, not on an object. It is a static interface method: utility and contract live in the same file.

Conclusion

You now hold Java's central design tool. You know that an interface is a behaviour contract without state, declared with interface and signed with implements, and that it expresses a "can do" relationship as opposed to inheritance's "is a". You understand why Java lets you implement as many interfaces as you want but extend only one class: the diamond problem, with its ambiguity of state and implementation, disappears when the contract provides neither fields nor bodies. You know that an interface is a type, and you have seen the practical consequence in an array where a Book and a MeetingRoom — with no kinship at all — respond to the same call.

You have mastered the members of an interface and their implicit modifiers: public abstract on methods, public static final on fields, without exception. You know default methods and, above all, why they were added: to let the JDK evolve its interfaces without breaking millions of classes; and you know how to resolve their only possible conflict with Interface.super.method(). You know that static methods keep utilities next to the contract — that is why List.of(...) exists today where the Collections class used to be needed — and that Java 9's private methods let you share code between defaults without widening the contract. You recognise marker interfaces like Serializable (module 7) and you know that @FunctionalInterface is the door to lambdas, which you will open in 04-06.

And beyond the syntax, you take away three design criteria: program against the contract, invert the dependencies so that business rules do not depend on details, and remember that the testability of your code in module 11 is decided today, every time you choose to depend on an interface or on a concrete class. BiblioTech already has its contracts, Lendable and Notifiable, and a MeetingRoom that proves you can be lent without being a material.

One obvious crack remains. Material is still an instantiable class: nothing stops you writing new Material("Something", "REF-1", true) and getting an object with no type, no author, no number and no duration, one that returns "Material" from getType() and applies generic terms. It is an object that should not exist, and its getType() and getLoanDays() are nothing but filler values the subclasses always override. In lesson 04-02, Abstract Classes, you will close that door: Material will become abstract, its filler methods will turn into abstract methods that force every format to declare its rules, and you will discover that an abstract class offers what an interface cannot — state, constructor and shared code — and why professional design almost never chooses between the two, but combines them.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved