intermediate22 min

Interfaces, Abstract Classes, and Polymorphism

How Java lets you write code against a contract instead of a concrete type — and the practical difference between an interface and an abstract class.

What you'll learn

  • Define and implement an interface with multiple implementing classes
  • Explain when an abstract class is appropriate instead of a plain interface
  • Use polymorphism to write code that works uniformly across several concrete types

Prerequisites

Explanation

An interface (interface Gradeable { double score(); }) declares a contract — method signatures with no implementation (plus, since Java 8, optional default methods that do have a body) — that any class can agree to fulfill via implements. Unlike a class, a Java class can implements any number of interfaces, which is exactly how Java gives you a form of "this thing plays several roles" without the ambiguity that multiple class inheritance would create — an interface has no state (fields) of its own to conflict with another interface's, only behavior it promises.

An abstract class (abstract class Content { abstract String render(); void logAccess() { ... } }) sits between a plain class and an interface: it can declare abstract methods (no body, must be implemented by subclasses) and hold real fields and fully-implemented methods that subclasses inherit for free, but it can never be instantiated directly (new Content() is a compile error). Choose an abstract class over an interface when subclasses genuinely share common state or reusable implementation, not just a shared contract; choose a plain interface when you only need to guarantee "this type can do X," especially across otherwise-unrelated classes.

Polymorphism is what makes both of these useful in practice: code written against the interface or abstract-class type (Gradeable g = quizOrAssignmentOrExam;) can call g.score() without knowing or caring which concrete class g actually is at runtime — the JVM dispatches to the actual object's implementation automatically (this is called dynamic/virtual dispatch). This is the mechanism behind writing one for (Gradeable g : allGradeableItems) total += g.score(); loop that correctly handles quizzes, assignments, and exams alike, with zero if (item instanceof Quiz) ... else if ... branching — new implementations of Gradeable can be added later without ever touching that loop, which is the real payoff: code that depends only on the contract doesn't need to change when new implementations of that contract appear.

Example

Polymorphism modeled with JS classes implementing a shared 'contract' (duck-typed in JS, compiler-enforced in Java) -- the loop over mixed types is the real payoff either way.

class Quiz {
  constructor(pointsEarned, pointsPossible) { this.pointsEarned = pointsEarned; this.pointsPossible = pointsPossible; }
  score() { return this.pointsEarned / this.pointsPossible; } // implements the "Gradeable" contract
}
class PassFailAssignment {
  constructor(passed) { this.passed = passed; }
  score() { return this.passed ? 1 : 0; } // a totally different internal shape, same contract
}

const items = [new Quiz(8, 10), new PassFailAssignment(true), new Quiz(5, 10)];

// This loop works uniformly across every "Gradeable" type, with zero branching on the concrete class:
let total = 0;
for (const item of items) {
  total += item.score(); // dynamic dispatch: calls whichever score() belongs to the actual object
}
console.log(total.toFixed(2)); // 2.30

Try it yourself

Add a third type, ExtraCreditAssignment, whose score() always returns 1.2, then include an instance in the loop.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Model a Java interface Shape with area(). Implement it with Square and Rectangle classes, then write totalArea(shapes) that sums area() across a mixed array -- with NO type-checking branches, only the polymorphic call.

Checks: Square.area() is correct · Rectangle.area() is correct · totalArea sums across mixed shape types without branching on type

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Model an abstract-class-like pattern: class Notification with a shared, fully-implemented method formatTimestamp(date) (returns date.toISOString()), and an abstract-style method body() that MUST be overridden (throw an Error if called directly on the base class). Implement EmailNotification and SmsNotification subclasses overriding body().

Checks: the base class's unimplemented method throws (models 'abstract') · EmailNotification overrides body() correctly · SmsNotification overrides body() correctly · formatTimestamp is shared, fully-implemented behavior inherited by both

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Guided local lab

Model a Domain Using Encapsulation, Composition, Interfaces, and Polymorphism

Runs on your computer
This lab runs on your own computer, in your own terminal and editor — not in your browser. VisaSparkSchools does not execute, run, or verify these commands for you. Follow the verification steps yourself to confirm your result.

Build a small library-loan domain, entirely in real Java, that uses every OOP tool from this module together: encapsulated classes, a has-a relationship, an interface with multiple implementations, and a polymorphic loop.

Required tools

  • JDK (21 LTS or newer)
  • A terminal (any)

Setup

  1. Create a project folder named library-loans.
  2. Inside it, create src/ for your .java files.
  3. You'll add: a Borrowable interface, two classes implementing it (Book, Magazine), an encapsulated Loan class composing a Borrowable, and a Main that demonstrates polymorphism.

Project structure

library-loans/
  src/
    Borrowable.java
    Book.java
    Magazine.java
    Loan.java
    Main.java

Starter files

src/Borrowable.java

public interface Borrowable {
    // TODO: declare a method String describe() and a method int loanPeriodDays()
}

src/Book.java

public class Book implements Borrowable {
    private final String title;
    private final String author;

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    // TODO: implement describe() (e.g. "title by author") and loanPeriodDays() (return 21)
}

src/Magazine.java

public class Magazine implements Borrowable {
    private final String name;
    private final int issueNumber;

    public Magazine(String name, int issueNumber) {
        this.name = name;
        this.issueNumber = issueNumber;
    }

    // TODO: implement describe() (e.g. "name, issue #N") and loanPeriodDays() (return 7)
}

src/Loan.java

public class Loan {
    private final Borrowable item; // composition: Loan HAS-A Borrowable, it doesn't extend it
    private final String borrowerName;
    private boolean returned;

    public Loan(Borrowable item, String borrowerName) {
        // TODO: store item and borrowerName; returned starts false
    }

    // TODO: add a public method summary() returning something like
    // "<borrowerName> borrowed <item.describe()> for <item.loanPeriodDays()> days"

    // TODO: add markReturned() (sets returned = true) and isReturned() (getter)
}

src/Main.java

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Loan> loans = new ArrayList<>();
        // TODO: create at least one Book loan and one Magazine loan, add both to loans

        // TODO: loop over loans polymorphically -- print each summary() -- with
        // NO instanceof checks or branching on whether the item is a Book or Magazine
    }
}

Requirements

  • Borrowable is an interface with describe() and loanPeriodDays().
  • Book and Magazine both implement Borrowable with genuinely different internal fields.
  • Loan composes a Borrowable (a field, not inheritance) plus a borrower name and a returned flag, all private.
  • Loan exposes summary(), markReturned(), and isReturned() as its only public surface.
  • Main builds a List<Loan> containing at least one Book-backed and one Magazine-backed loan, then prints every summary() via one polymorphic loop with no type-checking branches.

Commands to run

  • Compile every source file into out/

    javac -d out src/Borrowable.java src/Book.java src/Magazine.java src/Loan.java src/Main.java
  • Run the program

    java -cp out Main

Expected behavior

The program compiles with no errors or warnings, and running it prints one summary line per loan — one mentioning a book title/author and a 21-day period, one mentioning a magazine name/issue and a 7-day period — produced by the same loop, with no branch checking which concrete type each loan wraps.

Verify it yourself

  • javac -d out src/*.java

    Expected: Compiles cleanly with no errors

  • java -cp out Main

    Expected: Prints at least two summary lines, one for a Book-backed loan and one for a Magazine-backed loan

  • grep -n instanceof src/Main.java

    Expected: No output — Main.java should contain no instanceof checks in the printing loop

Troubleshooting

  • `error: Loan is not abstract and does not override abstract method describe() in Borrowable`Book or Magazine is missing an implementation of one of the interface's methods — every method declared in an interface must be implemented by a non-abstract implementing class.
  • Compiles, but the loop in Main only prints Book-shaped outputCheck that both a Book and a Magazine were actually constructed and added to the loans list before the loop runs.
  • `cannot find symbol: method summary()`Confirm Loan's summary() method is declared public, and that you're calling loan.summary() (a Loan method) rather than trying to call it on the Borrowable directly.

Stuck? Get a hint.

Extension challenge

Add a third Borrowable implementation, DvdBoxSet, with its own fields and a 14-day loan period, and confirm the existing Main loop prints it correctly with zero changes to Main.java itself — that's the payoff of coding against the interface.

When you've verified this locally, use the "Mark lesson complete" button below to record your progress.

Common mistakes

  • Trying to `new` an interface or an abstract class directly -- both are compile errors; only a concrete class implementing/extending them can be instantiated.
  • Writing `if (item instanceof Book) ... else if (item instanceof Magazine) ...` when a polymorphic method call would let the JVM dispatch correctly on its own -- this branching defeats the entire purpose of the interface and must be extended every time a new implementation is added.
  • Reaching for an abstract class when a plain interface would do -- if there's no shared state or shared implementation to inherit, an interface (which also allows a class to implement several contracts at once) is the better, more flexible fit.

Knowledge check

Knowledge check

1. A class needs to satisfy both a Comparable contract and a Serializable-style contract at once. Why does Java favor interfaces for this over class inheritance?
2. for (Gradeable g : items) { total += g.score(); } works correctly for Quiz, Exam, and Assignment objects in the same list, with no type-checking. What's this called?
3. When should you choose an abstract class over a plain interface?

Takeaway

Interfaces declare a contract any number of unrelated classes can fulfill; abstract classes add shared state and implementation on top of that, at the cost of single inheritance; polymorphism is what lets code written against either one work correctly, unchanged, as new implementations are added later.

Summary

implements fulfills an interface's contract; extends inherits an abstract or concrete class's state and behavior. A class can implement many interfaces but extend only one class. Polymorphism means code written against the shared type dispatches to the correct concrete implementation automatically, with no type-checking branches.

References

Your notes

Notes save automatically.