Interfaces

One of the most important guidelines you can follow in constructing complex software systems is to "separate the interface from the implementation." Apparently, interfaces are important.

What is an Interface?

The International Space Station has a docking device on it that provides an interface on which space shuttles and other ships can dock to it. If you put that device (interface) on your roof, a space shuttle can connect to your house.

The idea is that you don't really care what the docking device is is attached to: if you have the device then you are a Dockable thing. Both the ISS and your house can implement the Dockable interface.

The interface is a kind of contract that specifies behavior only; the implementation is given as a real class. Java gives you real support for interfaces.

What are interfaces good for?

Here's an example for a Stack, which is a kind of sequence that you can only add to and remove from one end, called the top. To add to the top is to push on it, to remove from the top is to pop off it.

Stack.java
/**
 * A small stack interface. You can (1) query the size of the stack, (2) ask
 * whether it is empty, (3) push an item, (4) pop an item, and (5) peek at the
 * top item.
 */
public interface Stack {

    /**
     * Adds the given item to the top of the stack.
     */
    void push(Object item);

    /**
     * Removes the top item from the stack and returns it.
     * 
     * @exception java.util.NoSuchElementException if the stack is empty.
     */
    Object pop();

    /**
     * Returns the top item from the stack without popping it.
     * 
     * @exception java.util.NoSuchElementException if the stack is empty.
     */
    Object peek();

    /**
     * Returns the number of items currently in the stack.
     */
    int size();

    /**
     * Returns whether the stack is empty or not.
     */
    default boolean isEmpty() {
        return size() == 0;
    }
}

Classes Implement Interfaces

Of course, you would need to add comments to the code to fully specify the interface.

You can declare a real class to implement the interface:

LinkedStack.java
import java.util.NoSuchElementException;

/**
 * An implementation of the stack interface using singly-linked nodes.
 */
public class LinkedStack implements Stack {
    private record Node(Object data, Node next) {
    }

    private Node top = null;
    private int size = 0;

    public void push(Object item) {
        top = new Node(item, top);
        size++;
    }

    public Object pop() {
        var item = peek();
        top = top.next;
        size--;
        return item;
    }

    @Override
    public boolean isEmpty() {
        // Override the default implementation for efficiency
        return top == null;
    }

    public Object peek() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return top.data;
    }

    public int size() {
        return size;
    }
}

Different classes can implement the same interface

BoundedStack.java
import java.util.NoSuchElementException;

/**
 * An implementation of a stack using a fixed, non-expandable array whose
 * capacity is set in its constructor. It comes with an extra method that
 * returns whether the stack is full (something that does not apply to the
 * Stack interface, since not all kinds of stacks have a size bound).
 */
public class BoundedStack implements Stack {
    private Object[] elements;
    private int size = 0;

    public BoundedStack(int capacity) {
        elements = new Object[capacity];
    }

    public void push(Object item) {
        if (isFull()) {
            throw new IllegalStateException("Cannot add to full stack");
        }
        elements[size++] = item;
    }

    public Object pop() {
        var item = peek();
        elements[--size] = null;
        return item;
    }

    public Object peek() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return elements[size - 1];
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    // This is a bonus method that is not part of the interface
    public boolean isFull() {
        return size == elements.length;
    }
}

Now you can write

    Stack s1 = new BoundedStack(100);
    Stack s2 = new UnboundedStack();

Program to an interface, not an implementation

One reason for separating interface from implementation is that the implementation can change without affecting clients who "programmed only to the interface."

This is pretty important, so, to repeat:

Program to an interface, not to an implementation

A class can implement multiple interfaces

A queue is a sequence that can only be added to at the rear and deleted from in the front. Suppose we had:

Queue.java
/**
 * A small queue interface. You can query the size of the queue and ask whether
 * it is empty, add and remove items, and peek at the front item.
 */
public interface Queue {

    /**
     * Adds the given item to the rear of the queue.
     */
    void enqueue(Object item);

    /**
     * Removes the front item from the queue and returns it.
     * 
     * @exception java.util.NoSuchElementException if the queue is empty.
     */
    Object dequeue();

    /**
     * Returns the front item from the queue without popping it.
     * 
     * @exception java.util.NoSuchElementException if the queue is empty.
     */
    Object peek();

    /**
     * Returns the number of items currently in the queue.
     */
    int size();

    /**
     * Returns whether the queue is empty or not.
     */
    default boolean isEmpty() {
        return size() == 0;
    }
}

and, for fun, here's another:

    interface Rotatable {
        void rotateLeft();
        void rotateRight();
    }

Then you could have

    class Deque implements Stack, Queue, Rotatable {
        ...
        void enqueue(Object item) {...}
        Object dequeue() {return pop();}
        void push(Object item) {...}
        Object pop() {...}
        Object removeLast() {...}
        Object peek() {...}
        void rotateLeft() {...}
        void rotateRight() {...}
        int getSize() {...}
        boolean isEmpty() {...}
        ...
      }

Implementing multiple interfaces is no problem, even if more than one interface specifies the same method, like Stack.getSize() and Queue.getSize(), because there is only one Deque.getSize(). But suppose Stack and Queue were two classes and they had their own implementations of getSize, and we tried to make Deque extend Stack and Queue. Which getSize would get inherited? Because there is a little bit of confusion here, Java prohibits extending more than one class.

A Larger Example

Here is an interface for things that can be outlined and filled:

Drawable.java
import java.awt.Graphics;

public interface Drawable {
    public void outline(Graphics g);

    public void fill(Graphics g);
}

Let's use this interface with a class we saw earlier.

DrawableCircle.java
import java.awt.Graphics;
import java.awt.Point;

public class DrawableCircle extends Circle implements Drawable {
    public DrawableCircle(Point center, double radius) {
        super(center, radius);
    }

    public void outline(Graphics g) {
        g.setColor(color);
        g.drawOval(center.x - (int) radius, center.y - (int) radius, (int) radius * 2, (int) radius * 2);
    }

    public void fill(Graphics g) {
        g.setColor(color);
        g.fillOval(center.x - (int) radius, center.y - (int) radius, (int) radius * 2, (int) radius * 2);
    }
}

And with another class:

DrawableSquare.java
import java.awt.Graphics;
import java.awt.Point;

public class DrawableSquare extends Square implements Drawable {
    public DrawableSquare(Point upperLeft, double sideLength) {
        super(upperLeft, sideLength);
    }

    public void outline(Graphics g) {
        g.setColor(color);
        g.drawRect(upperLeft.x, upperLeft.y, (int) sideLength, (int) sideLength);
    }

    public void fill(Graphics g) {
        g.setColor(color);
        g.fillRect(upperLeft.x, upperLeft.y, (int) sideLength, (int) sideLength);
    }
}

And here is a client that uses the interface to access objects. It has an array of Drawables and iterates through the array.

FigureDemo.java
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;

import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * Demos both plain figures and drawable figures.
 */
public class FigureDemo extends JPanel {

    /**
     * An arbitrary array of drawables just to show off polymorphism.
     */
    Drawable[] myFigures = new Drawable[] { new DrawableCircle(new Point(50, 50), 40),
            new DrawableCircle(new Point(100, 200), 17), new DrawableSquare(new Point(90, 143), 35),
            new DrawableCircle(new Point(50, 90), 20), new DrawableSquare(new Point(0, 0), 15) };

    /**
     * To paint, just fill all the shapes in the array one by one, then outline
     * another one just for fun.
     */
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (Drawable d : myFigures) {
            d.fill(g);
        }
        new DrawableCircle(new Point(60, 60), 180).outline(g);
    }

    /**
     * Constructs a panel, and set some colors, just because we can.
     */
    public FigureDemo() {
        ((DrawableCircle) myFigures[3]).setColor(Color.green);
        ((DrawableCircle) myFigures[0]).setColor(Color.red);
    }

    /**
     * Allows us to see the panel within an application.
     */
    public static void main(String[] args) {
        JFrame frame = new JFrame("Figures");
        frame.getContentPane().add(new FigureDemo());
        frame.setSize(400, 400);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}