Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

Interface-Based Programming for Polymorphism

Recommended resources

Introduction

Welcome to our next lesson. In our previous sessions, you've built a solid foundation by learning to define contracts with Java interfaces and create shared blueprints with abstract classes. We established the "can-do" vs. "is-a" heuristic to choose between them. Today, we'll unlock the true power of these abstractions by exploring one of the most fundamental principles in object-oriented programming: polymorphism.

This lesson focuses on applying polymorphism through the practice of "programming to an interface." You will learn how to write more flexible and maintainable code by depending on abstract contracts rather than concrete implementations. This skill is not just a theoretical concept; it's the engine behind almost every design pattern we will study, making your code adaptable to change and easier to extend.


The Problem with Concrete Thinking

Imagine you're building a system where a developer needs a machine to work. A first, direct approach might look like this:

class Laptop {
    public void compile() {
        System.out.println("Compiling code on a Laptop...");
    }
}

class Developer {
    public void work(Laptop lap) { // Tightly coupled to Laptop
        lap.compile();
    }
}

This code works, but it has a major flaw. The Developer class is tightly coupled to the Laptop class. What happens if the company decides to provide a more powerful Desktop computer instead?

class Desktop {
    public void compile() {
        System.out.println("Compiling code on a super-fast Desktop!");
    }
}

// Now what? We can't pass a Desktop to the work() method.
// We would have to change the Developer class, maybe by adding another method:
// public void work(Desktop desk) { ... }

This approach is brittle. Every time a new type of machine is introduced, we have to modify the Developer class. This violates the Open/Closed Principle, which we'll discuss later, but the intuitive problem is clear: our Developer is too specific about what it needs.


The Polymorphic Solution: Programming to an Interface

The solution is to make the Developer depend on an abstraction, not a concrete detail. A developer doesn't fundamentally need a laptop; they need a thing that can compile code. We can represent this "thing" with an interface.

The following video from the Telusko channel provides an excellent walkthrough of this exact scenario, moving from a rigid, tightly-coupled design to a flexible, loosely-coupled one.

#66 Need of Interface in Java

This video brilliantly illustrates the "need for an interface" by showing the limitations of depending on a concrete class (Laptop) and the flexibility gained by depending on an abstraction (Computer).

Please watch the video from the beginning to the end. As you watch, focus on these key moments: The initial problem where the Developer class requires a Laptop object, and why this fails when a Desktop is introduced. This segment establishes the problem of tight coupling. The refactoring process where a Computer abstraction is introduced. Notice how the Developer class is changed to depend on Computer instead of Laptop. This section demonstrates the core principle of programming to an abstraction. The final switch from using an abstract class to an interface, which achieves the same goal of creating a flexible contract. This part reinforces that both can be used for abstraction, a concept from our previous lesson.

As the video demonstrates, the principle is: Program to an interface, not an implementation.

This means that whenever possible, your variables, method parameters, and return types should be of the interface type (e.g., Computer), not the concrete class type (e.g., Laptop).

Here's the refactored code embodying this principle:

// The Contract: what does it mean to be a computer?
interface Computer {
    void compile();
}

// Concrete Implementations of the contract
class Laptop implements Computer {
    @Override
    public void compile() {
        System.out.println("Compiling code on a Laptop...");
    }
}

class Desktop implements Computer {
    @Override
    public void compile() {
        System.out.println("Compiling code on a super-fast Desktop!");
    }
}

// The client class, now loosely coupled
class Developer {
    // This method depends ONLY on the interface
    public void work(Computer machine) {
        machine.compile(); // It just works, regardless of the actual object!
    }
}

// In our main application logic:
public class Main {
    public static void main(String[] args) {
        Developer dev = new Developer();

        Computer myLaptop = new Laptop();
        Computer myDesktop = new Desktop();

        System.out.println("Developer is given a laptop:");
        dev.work(myLaptop); // Works!

        System.out.println("\nDeveloper is given a desktop:");
        dev.work(myDesktop); // Also works, with no change to Developer!
    }
}

The line dev.work(myDesktop) is polymorphism in action. We are passing an object of type Desktop to a method that expects an object of type Computer. Because Desktop implements Computer, this is perfectly valid. The Java Virtual Machine (JVM) determines at runtime which specific compile() method to call.


How Java Achieves This: Interface Reference Variables

Let's zoom in on the mechanism that makes this possible. You can declare a variable of an interface type, and this variable can refer to any object of any class that implements that interface.

The "Keep On Coding" channel has a very clear, concise video demonstrating this exact feature.

Java Interface Tutorial

This short segment explains and shows how an interface variable can hold references to different objects and how Java correctly invokes the method for the object being referenced at that moment.

Please watch the section on interface reference variables. The Series interface with ByTwos and ByThrees implementations is a simple, effective example of this core concept.

As shown in the video, a variable of type Series can point to a ByTwos object and call its getNext() method. A moment later, the same variable can be reassigned to point to a ByThrees object, and a call to getNext() will now invoke the method from the ByThrees class.

The key takeaway is that the code calling the method (ob.getNext()) doesn't change and doesn't need to know or care which concrete object is currently being referenced. This is the essence of decoupling.


A Practical Design Example: The Strategy Pattern

This principle isn't just for simple examples. It's the foundation for many powerful design patterns. Let's look at a slightly more complex design that shows the real-world benefits.

The following text from the SDE Coursepack breaks down a program for apportioning congressional representatives into three distinct, abstract behaviors. This is an excellent, practical demonstration of programming to interfaces.

SDE | Benefits Of Polymorphism

This document shows how to design a flexible system by identifying abstract behaviors and defining interfaces for them before writing the concrete implementations.

Please read through the "Apportionment" example. Start with the introduction to the problem. Notice the distinction between the Java interface keyword and the conceptual interface. Read the StateSupplier section carefully, from the interface definition through the first concrete implementation. This is the core idea. Now, read the crucial part under "What if things change?". This shows the payoff: a new data source (DeptOfCommerceStateSupplier) is added, and the client code only needs one line changed. Skim through the next two sections on ApportionmentMethod and ApportionmentFormat. You don't need to understand the algorithms, just see how the same pattern of "interface -> implementation" is repeated to make the calculation and formatting steps interchangeable.

This apportionment example is a classic illustration of the Strategy design pattern. We will study this pattern formally later, but you've just learned its core structure. The idea is to define a family of algorithms (e.g., HamiltonMethod, HuntingtonHillMethod), encapsulate each one, and make them interchangeable.

The UML diagram for the Strategy pattern visually represents what you've just learned.

This diagram shows a 'Context' class that works with a 'Strategy' interface. Different algorithms are encapsulated in 'ConcreteStrategyA' and 'ConcreteStrategyB' classes, which implement the interface. The Context doesn't know or care which concrete strategy it's using.

In our apportionment example:

  • Context: The Apportioner class that executes the overall logic.
  • Strategy: The ApportionmentMethod interface.
  • ConcreteStrategyA: The HamiltonMethod class.
  • ConcreteStrategyB: The HuntingtonHillMethod class.

The Apportioner (Context) depends only on the ApportionmentMethod (Strategy) interface, achieving the flexibility and loose coupling we've been discussing.


Conclusion

In this lesson, you've connected your knowledge of interfaces and abstract classes to the powerful principle of polymorphism. By programming to an interface, you can create systems that are significantly more flexible, maintainable, and extensible.

Here are the key takeaways:

  • Polymorphism allows an object to take many forms. In this context, it means a variable of an interface type can refer to an object of any class that implements the interface.
  • Programming to an interface, not an implementation is a fundamental design principle that promotes loose coupling.
  • By depending on abstractions, your code is shielded from changes in concrete implementations. You can add new implementations (new Computer types, new ApportionmentMethod algorithms) without breaking the client code that uses the interface.
  • This principle is the structural foundation for many design patterns, such as the Strategy pattern.

In our next lesson, we will continue exploring how to structure object-oriented systems by comparing two fundamental relationships: composition and inheritance. You'll learn why modern software design often favors composition, a technique that leverages the very principles of polymorphism and programming to an interface that you learned today.