Week 2

Inheritance

Interfaces

Polymorphism

Error Handling in Java

Practice

Assignment

Back end Track

🎯 Learning Objectives

By the end of this module, you will be able to:


1. What is an Interface?

In the last module, inheritance let one class reuse the behaviour of another. But sometimes you don't want to share implementation — you just want to guarantee that certain classes all provide a specific set of methods, regardless of how they implement them.

This is what an interface does. It is a contract: any class that signs the contract (implements the interface) promises to provide specific methods. The interface says what must be done; the implementing class decides how.

A real-world analogy: a power socket is a contract. It guarantees that anything plugged into it will receive power. It doesn't care whether it's a phone, a lamp, or a laptop — as long as the plug fits the socket, it works. The socket defines the contract; each device implements it in its own way.

🔍 JavaScript doesn't have interfaces as a language feature — you might have encountered them through TypeScript (interface Printable { print(): void }). Java's interfaces work the same way conceptually, but are enforced at compile time for all classes, not just typed ones.


2. Defining an Interface

An interface is defined with the interface keyword instead of class. It contains method signatures — the name, parameters, and return type — but no body (no implementation).

public interface Printable {
    void print();                      // no body — just the signature
    String getDocumentName();          // every implementor must provide this
}

By default, all methods in an interface are:

You don't need to write these modifiers explicitly — they are implied:

public interface Printable {
    void print();                  // implicitly public abstract
    String getDocumentName();      // implicitly public abstract
}

💡 An interface cannot have instance fields. It can only have constants (public static final) and method signatures. This is what keeps it a pure contract with no state of its own.


3. The implements Keyword

A class signs the interface contract using the implements keyword. It must then provide a concrete implementation of every method declared in the interface — or the compiler refuses to compile.

public class Invoice implements Printable {

    private String clientName;
    private double amount;

    public Invoice(String clientName, double amount) {
        this.clientName = clientName;
        this.amount     = amount;
    }

    @Override
    public void print() {
        System.out.printf("Invoice for %s — €%.2f%n", clientName, amount);
    }

    @Override
    public String getDocumentName() {
        return "Invoice-" + clientName;
    }
}
public class Report implements Printable {

    private String title;

    public Report(String title) {
        this.title = title;
    }

    @Override
    public void print() {
        System.out.println("=== " + title + " ===");
    }

    @Override
    public String getDocumentName() {
        return "Report-" + title;
    }
}

Both Invoice and Report honour the Printable contract, but each implements it differently. Now you can treat them through the same interface type:

Printable doc1 = new Invoice("Alice", 1250.00);
Printable doc2 = new Report("Q3 Summary");

doc1.print(); // Invoice for Alice — €1250.00
doc2.print(); // === Q3 Summary ===

💡 Use @Override on every implemented method — same reason as with inheritance: the compiler will catch any signature mismatch immediately.


4. Implementing Multiple Interfaces

This is where interfaces solve a problem that inheritance cannot. Java allows a class to extend only one superclass, but it can implement as many interfaces as needed.

public interface Printable {
    void print();
}

public interface Exportable {
    String exportToCsv();
}

public interface Archivable {
    void archive(String destination);
}
public class Report implements Printable, Exportable, Archivable {

    private String title;
    private String content;

    public Report(String title, String content) {
        this.title   = title;
        this.content = content;
    }

    @Override
    public void print() {
        System.out.println("=== " + title + " ===\\\\n" + content);
    }

    @Override
    public String exportToCsv() {
        return "title,content\\\\n" + title + "," + content;
    }

    @Override
    public void archive(String destination) {
        System.out.println(title + " archived to: " + destination);
    }
}

A Report is simultaneously Printable, Exportable, and Archivable. Each interface is an independent contract — mixing and matching them is what gives Java's type system its flexibility.

🔍 This is how Java compensates for not having multiple inheritance. Instead of inheriting behaviour from many parents, a class can implement many contracts and provide its own behaviour for each.


5. Default Methods (Java 8+)

Before Java 8, interfaces could only contain method signatures. Adding a new method to a widely-used interface would break every class that implemented it — a massive problem for library authors.

Java 8 introduced default methods — methods with a body inside an interface. They provide a fallback implementation that implementing classes automatically inherit, but can override if they need different behaviour.

public interface Printable {
    void print();
    String getDocumentName();

    // Default method — provides a base implementation
    default void printWithBorder() {
        System.out.println("─".repeat(40));
        print();
        System.out.println("─".repeat(40));
    }
}

A class that implements Printable automatically gets printWithBorder() for free:

Invoice invoice = new Invoice("Alice", 1250.00);
invoice.printWithBorder();
// ────────────────────────────────────────
// Invoice for Alice — €1250.00
// ────────────────────────────────────────

But a class can override the default if it needs different behaviour:

public class Report implements Printable {
    @Override
    public void printWithBorder() {
        System.out.println("===[ " + getDocumentName() + " ]===");
        print();
        System.out.println("=".repeat(40));
    }
    // ... other methods
}

⚠️ If a class implements two interfaces that both define a default method with the same name, the class must override that method to resolve the conflict — otherwise the compiler throws an error.


6. Static Methods in Interfaces

Java 8 also introduced static methods in interfaces. These belong to the interface itself — they cannot be overridden by implementing classes and are called on the interface name directly.

public interface Printable {
    void print();

    static void printAll(Printable[] documents) {
        for (Printable doc : documents) {
            doc.print();
        }
    }
}
Printable[] docs = {
    new Invoice("Alice", 1250.00),
    new Report("Q3 Summary", "Revenue up 12%")
};

Printable.printAll(docs); // called on the interface, not an object

💡 Interface static methods are useful for factory methods and utility logic directly related to the interface's contract — keeping related code together without needing a separate utility class.


7. Interfaces vs Abstract Classes

Both interfaces and abstract classes support polymorphism and prevent direct instantiation, but they serve different purposes. Choosing the wrong one leads to design problems.

Interface Abstract Class
Purpose Defines a capability — what something can do Defines an identity — what something is
Fields Constants only (static final) Any fields allowed
Methods Signatures + default + static Any mix of abstract and concrete methods
Constructor ❌ None ✅ Can have constructors
Inheritance A class can implement many A class can extend one only
extends/implements implements extends
Best for Cross-cutting capabilities shared across unrelated classes Shared base behaviour for closely related classes

When to use an Interface

Use an interface when you want to define a capability that unrelated classes can share:

// A Dog, a Robot, and a Car have nothing in common —
// but all three can be "driveable" or "remote-controlled"
public interface Driveable {
    void accelerate(double amount);
    void brake(double amount);
}

public class Car        implements Driveable { ... }
public class GoKart     implements Driveable { ... }
public class DroneBoat  implements Driveable { ... }

When to use an Abstract Class

Use an abstract class when classes are closely related and share actual implementation — fields, constructors, concrete methods — that would be duplicated otherwise:

// Dog, Cat, Bird are all animals — they share real state and behaviour
public abstract class Animal {
    private String name;   // shared field

    public Animal(String name) { this.name = name; } // shared constructor

    public String getName() { return name; }          // shared implementation

    public abstract String makeSound();               // each animal implements differently
}

public class Dog extends Animal {
    public Dog(String name) { super(name); }

    @Override
    public String makeSound() { return "Woof!"; }
}

A useful rule of thumb: if you find yourself duplicating fields and constructors across an interface's implementing classes, you probably need an abstract class instead.


8. Interface Segregation — Keep Interfaces Small

A common mistake is packing too many unrelated methods into a single interface. This forces implementing classes to provide methods they don't need — a violation of the Interface Segregation Principle: no class should be forced to implement methods it doesn't use.

// ❌ Too broad — a Printer shouldn't need to save or transmit
public interface DocumentHandler {
    void print();
    void saveToDatabase();
    void sendByEmail();
    void exportToCsv();
    void archive();
}

A class that only prints is now forced to write empty or stub implementations for saveToDatabase, sendByEmail, and the rest — polluting the code with meaningless methods.

The fix: split into small, focused interfaces:

// ✅ Each interface does exactly one thing
public interface Printable    { void print(); }
public interface Persistable  { void saveToDatabase(); }
public interface Mailable     { void sendByEmail(String recipient); }
public interface Exportable   { String exportToCsv(); }
public interface Archivable   { void archive(String destination); }

Now each class implements only what it actually supports:

public class Invoice implements Printable, Mailable, Exportable { ... }
public class Report  implements Printable, Archivable { ... }
public class Receipt implements Printable { ... }

💡 Small interfaces are also easier to test, combine, and reason about. When you see an interface growing beyond 3–5 methods, ask whether it's really doing one thing or whether it should be split.


9. Real-World Interfaces — Comparable and Iterable

The Java standard library is built on interfaces. Two you'll encounter immediately are Comparable and Iterable.

Comparable<T> — natural ordering

Comparable defines how objects of a class compare to one another. Implementing it allows your objects to be sorted by Arrays.sort(), Collections.sort(), and similar methods.

public interface Comparable<T> {
    int compareTo(T other);
    // returns: negative if this < other, 0 if equal, positive if this > other
}
public class Student implements Comparable<Student> {
    private String name;
    private double gpa;

    public Student(String name, double gpa) {
        this.name = name;
        this.gpa  = gpa;
    }

    @Override
    public int compareTo(Student other) {
        // Sort by GPA descending — highest first
        return Double.compare(other.gpa, this.gpa);
    }

    @Override
    public String toString() {
        return name + " (" + gpa + ")";
    }
}
Student[] students = {
    new Student("Alice", 8.2),
    new Student("Bob",   6.5),
    new Student("Carol", 9.1)
};

Arrays.sort(students); // uses compareTo() automatically

for (Student s : students) {
    System.out.println(s);
}
// Carol (9.1)
// Alice (8.2)
// Bob (6.5)

Iterable<T> — use in for-each loops

Iterable is what allows an object to be used in an enhanced for loop. Every collection you'll use (ArrayList, List, etc.) implements this interface.

// This only works because ArrayList implements Iterable
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");

for (String name : names) {   // possible because of Iterable
    System.out.println(name);
}

You won't implement Iterable yourself right now, but knowing it's an interface — and that the for-each loop is just syntax sugar over it — demystifies how Java collections work under the hood.


✏️ Exercises

Exercise 1 — Concept: Interface or Abstract Class?

For each scenario, decide whether you'd use an interface or an abstract class. Justify each answer in two sentences.

  1. Dog, Cat, and Bird all need a name, age, and a makeSound() method. Each makes a different sound.
  2. EmailNotification, SmsNotification, and PushNotification all need to implement a send(String message) method, but share no fields or state.
  3. Shape has subclasses Circle, Rectangle, and Triangle. All need an area() method and all share a colour field.
  4. A payment system needs CreditCardPayment, PayPalPayment, and CryptoPayment to all support processPayment(double amount) and refund(double amount).

Exercise 2 — Bug Fixing: Broken Contract