Interfaces & Abstract Classes

Implementing Multiple Interfaces

15 min Lesson 6 of 14

Implementing Multiple Interfaces

One of the most powerful features of interfaces in Java is that a single class can implement as many interfaces as it likes. This sidesteps the single-inheritance limitation of classes and lets you compose behaviour from several independent contracts. In this lesson you will learn how to wire up multiple interfaces and, importantly, how to deal with the conflicts that arise when two of them declare a default method with the same signature.

Why Implement Multiple Interfaces?

Think about a real-world object like a SmartDevice. It might need to be printable, serialisable, and remotely controllable — three completely unrelated capabilities. None of those is a parent class; they are all roles the class plays. Interfaces model roles perfectly because they declare a contract without forcing a class hierarchy.

The key design principle: favour small, focused interfaces over large monolithic ones. A class that implements three small interfaces is far easier to understand and test than one that extends a massive base class.

Basic Syntax

List every interface after the implements keyword, separated by commas:

interface Printable { void print(); } interface Exportable { void exportToCsv(); } interface Auditable { String lastModifiedBy(); } class Report implements Printable, Exportable, Auditable { private final String author; Report(String author) { this.author = author; } @Override public void print() { System.out.println("Printing report..."); } @Override public void exportToCsv() { System.out.println("Exporting report to CSV..."); } @Override public String lastModifiedBy() { return author; } }

The compiler enforces that every abstract method from every interface is implemented. Miss one and the class will not compile.

Polymorphism Across Multiple Interfaces

A Report object can now be assigned to variables of any of the three interface types. Each reference exposes only the methods of its declared type:

Report report = new Report("Alice"); Printable p = report; p.print(); // OK Exportable e = report; e.exportToCsv(); // OK Auditable a = report; System.out.println(a.lastModifiedBy()); // "Alice"

This is standard polymorphism. The actual object is always a Report, but the interface reference constrains which capabilities the caller can see — a handy tool for limiting scope.

Default Method Conflicts

Problems start when two interfaces define a default method with the same name and parameter list. The compiler cannot silently pick one, so it forces you to resolve the ambiguity explicitly.

interface Logger { default String tag() { return "[LOG]"; } } interface Tracer { default String tag() { return "[TRACE]"; } } // This will NOT compile as-is; you must override tag(): class Instrumented implements Logger, Tracer { @Override public String tag() { // Option A — write your own implementation: return "[INSTRUMENTED]"; } }

If you prefer to delegate to one of the existing default implementations instead of writing a fresh one, use the special InterfaceName.super.method() syntax:

class Instrumented implements Logger, Tracer { @Override public String tag() { // Option B — delegate to Logger's default: return Logger.super.tag() + " " + Tracer.super.tag(); } }
Forgetting to resolve the conflict is a compile-time error. Java will never silently pick one interface over the other. This is deliberate — silent choice would make code unpredictable. The override requirement makes the decision visible in the source.

A Practical Example: A Plugin System Component

Here is a more realistic scenario. Imagine a plugin system where each plugin must support lifecycle hooks and also be version-aware:

interface Lifecycle { void onLoad(); void onUnload(); default String status() { return "active"; } } interface Versioned { String version(); default String status() { return "v" + version(); } } class DatabasePlugin implements Lifecycle, Versioned { @Override public void onLoad() { System.out.println("DB plugin loaded"); } @Override public void onUnload() { System.out.println("DB plugin unloaded"); } @Override public String version() { return "2.1.0"; } // Must override because both interfaces define status(): @Override public String status() { return Lifecycle.super.status() + " | " + Versioned.super.status(); // prints "active | v2.1.0" } }

Rules Summary

  • A class may implement any number of interfaces — there is no limit.
  • Every abstract method from every interface must be implemented (or the class itself must be declared abstract).
  • If two interfaces provide a default method with the same signature, the implementing class must override it.
  • Use InterfaceName.super.method() to call a specific interface's default from inside the override.
  • If only one interface has the default and the other has an abstract declaration, the default wins (no conflict).
Design guidance: keep each interface small and single-purpose (Interface Segregation Principle). When interfaces are narrow, the chance of accidental method name collisions drops sharply, and the code signals its intent more clearly.

Summary

Implementing multiple interfaces is Java's answer to multiple inheritance. You list all interfaces after implements, provide bodies for every abstract method, and explicitly resolve any default-method name collision by overriding the conflicting method. The InterfaceName.super.method() syntax lets you call either interface's default as a building block inside your override. Together these rules give you flexible, composable designs without the ambiguity problems that plagued multiple class inheritance in other languages.