Lesson 17 — Contracts

Interfaces

An interface defines a contract — a list of capabilities a class promises to provide. Unlike abstract classes, interfaces carry no state, making them ideal for defining shared behaviour across unrelated class hierarchies.

What is an Interface?

An interface is a collection of method signatures (and since Java 8, default implementations) that any class can choose to implement. The key word is any — a class can implement an interface regardless of where it sits in the inheritance tree. This makes interfaces the mechanism for defining cross-cutting behaviour: logging, serialisation, comparability, printing.

Declaring and Implementing

JAVA — Interface and implementation
// Interface — a contract
public interface Printable {
    void printReport();        // implicitly public and abstract
    String getSummary();       // each implementing class must provide this
}

// A class that signs the contract:
public class Learner extends Person implements Printable {

    private String course;
    private double mark;

    // ... constructor, getters ...

    @Override
    public void printReport() {
        System.out.println("=".repeat(35));
        System.out.println("LEARNER REPORT");
        System.out.println("Name:   " + getName());
        System.out.println("Course: " + course);
        System.out.printf ("Mark:   %.1f%% (%s)%n", mark, getGradeLabel());
        System.out.println("=".repeat(35));
    }

    @Override
    public String getSummary() {
        return String.format("%s | %s | %.1f%%", getName(), course, mark);
    }

    private String getGradeLabel() {
        if (mark >= 80) return "Distinction";
        if (mark >= 60) return "Merit";
        if (mark >= 50) return "Pass";
        return "Not yet competent";
    }
}

Multiple Interface Implementation

A class can implement as many interfaces as it needs. This is how Java achieves the flexibility of multiple inheritance without the ambiguity of multiple class inheritance.

JAVA — Multiple interfaces
public interface Comparable<T> {
    int compareTo(T other);
}

public interface Serializable {
    String toJson();
}

public interface Printable {
    void printReport();
}

// One class, three contracts:
public class Learner extends Person
        implements Printable, Serializable, Comparable<Learner> {

    @Override
    public void printReport() { /* ... */ }

    @Override
    public String toJson() {
        return String.format(
            "{"name":"%s","course":"%s","mark":%.1f}",
            getName(), course, mark);
    }

    @Override
    public int compareTo(Learner other) {
        return Double.compare(this.mark, other.mark); // sort by mark
    }
}

Interface as a Type

The most powerful use of interfaces: store objects through the interface type. Any object that implements the interface can be held in the variable — regardless of its actual class.

JAVA — Interface as type
// Store different types through the interface:
Printable[] printables = {
    new Learner("Thandi", "Java", 74.5),
    new Learner("Sipho", "Python", 81.0),
    new Facilitator("Ms Dube", "Dev"), // Facilitator also implements Printable
    new Learner("Lerato", "SQL", 68.0)
};

// One loop prints all types correctly:
for (Printable p : printables) {
    p.printReport();
    System.out.println();
}

// Pass any Printable to a method that needs printing:
public static void exportReports(Printable[] items) {
    for (Printable item : items) {
        System.out.println(item.getSummary());
    }
}

Default Methods (Java 8+)

Default methods provide a method body in the interface. This allows interfaces to evolve — adding a new default method does not break existing implementors.

JAVA — Default methods
public interface Printable {
    void printReport();
    String getSummary();

    // Default method — implementors inherit this unless they override it
    default void printToFile(String filename) {
        System.out.println("[Simulated] Writing report to " + filename);
        System.out.println(getSummary()); // calls the abstract method
    }
}

// Learner can use printToFile() without implementing it:
Learner l = new Learner("Sipho", "Python", 81.0);
l.printToFile("reports/sipho.txt");

Functional Interfaces and Lambdas

An interface with exactly one abstract method is a functional interface. Java's lambda syntax provides a concise way to implement them.

JAVA
@FunctionalInterface
public interface Validator {
    boolean validate(int value);
}

// Lambda implements the one abstract method:
Validator markValidator = value -> value >= 0 && value <= 100;
Validator ageValidator  = value -> value >= 15 && value <= 80;

System.out.println(markValidator.validate(74));   // true
System.out.println(markValidator.validate(150));  // false
System.out.println(ageValidator.validate(21));    // true

// Passing a lambda to a method:
public static boolean checkAll(int[] values, Validator v) {
    for (int val : values) if (!v.validate(val)) return false;
    return true;
}
System.out.println(checkAll(new int[]{65, 72, 80}, markValidator)); // trueFunctional interfaces

Practice Task

Your Turn

Create interfaces: Certifiable (generateCertificate() returns String), Gradeable (getGradeLabel() returns String, default method printGrade() that calls getGradeLabel()), Exportable (toCSV() returns String). Implement all three in Learner. Implement only Certifiable in Facilitator. Write a method exportAll(Exportable[] items) that prints every toCSV(). Write a method certifyAll(Certifiable[] items) that prints every certificate. Test both with arrays mixing learners and facilitators where applicable.

Common Mistakes

  • Method body in interface without default keyword — traditional interface methods must be abstract; add a body only with default.
  • Interface fields — all interface fields are implicitly public static final. Do not use interfaces as containers for constants.
  • Implementing an interface with extends instead of implements — classes implement interfaces, they extend classes.
  • Forgetting to implement all abstract methods — if a class implements an interface but misses a method, it must be declared abstract.
  • Not using @Override when implementing — always use it; it catches mistakes where you misspell the method name.

Professional Tip

Design to interfaces, not implementations. If a method only needs printing behaviour, declare its parameter as Printable, not Learner. This makes the method reusable for any type that implements Printable — today and in the future.

Mini Quiz

How many interfaces can a Java class implement?