Interfaces

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

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.

package edu.lmu.cs.rtoal.collections;

/**
 * A small stack interface.  You can query the size of the stack and
 * ask whether it is empty, push items, pop items, and 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.
     */
    boolean isEmpty();
}

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:

package edu.lmu.cs.rtoal.collections;

import java.util.NoSuchElementException;

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

    private Node top = null;

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

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

    public boolean isEmpty() {
        return top == null;
    }

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

    public int size() {
        int count = 0;
        for (Node node = top; node != null; node = node.next) {
            count++;
        }
        return count;
    }
}

Different classes can implement the same interface

package edu.lmu.cs.rtoal.collections;

import java.util.NoSuchElementException;

/**
 * An implementation of a stack using a fixed, non-expandable array whose
 * capacity is set in its constructor.
 */
public class BoundedStack implements Stack {
    private Object[] array;
    private int size = 0;

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

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

    public Object pop() {
        if (size == 0) {
            throw new NoSuchElementException("Cannot pop from empty stack");
        }
        Object result = array[size-1];
        array[--size] = null;
        return result;
    }

    public Object peek() {
        if (size == 0) {
            throw new NoSuchElementException("Cannot peek into empty stack");
        }
        return array[size - 1];
    }

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

    public int size() {
        return size;
    }
}

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:

package edu.lmu.cs.rtoal.collections;

/**
 * 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.
     */
    boolean isEmpty();
}

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:

package edu.lmu.cs.rtoal.oop.figures;

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.

package edu.lmu.cs.rtoal.oop.figures;

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:

package edu.lmu.cs.rtoal.oop.figures;

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.

package edu.lmu.cs.rtoal.oop.figures;

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);
    }
}