Interfaces

The key tool for abstraction that we have in Java and other Object-Oriented languages is inheritance.

Abstraction is a concept in Computer Science where we use the tools of our programming languages to organize problems in a way that we do not have to constantly think about low-level details.

The very first tools of abstraction we would have learned are functions. For instance, you may have seen the print function in Python, or System.out.println in Java. But think about how hard it must be to write those print functions, even though they're in every basic program.

  • But how do they print?
  • How do they communicate their contents to the operating system, which then shares it with the programs that show us output?
  • How do they turn numbers into the digits that we humans recognize?

Even print is a useful abstraction. In object-oriented land, we describe elements of problems that we care about as classes. From there, we may want to treat certain instances of those problems as the same.

class Rectangle

To use a new example for us, I'm going to create a class Rectangle, which will be represented internally by its top-left corner: (x,y) and a width and a height.

public class Rectangle {
    private double x;
    private double y;
    private double width;
    private double height;

    public Rectangle(double x, double y, double width, double height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    public double getX() {
        return this.x;
    }
    public double getY() {
        return this.y;
    }
    public double getCenterX() {
        return this.x + this.width / 2;
    }
    public double getCenterY() {
        return this.y + this.height / 2;
    }
    public double getWidth() {
        return this.width;
    }
    public double getHeight() {
        return this.height;
    }
    public double area() {
        return this.width * this.height;
    }
    public boolean containsPoint(double x, double y) {
        if (x > this.x && x < (this.x + this.width)) {
            if (y > this.y && y < (this.y + this.height)) {
                return true;
            }
        }
        return false;
    }
}

We can imagine many more methods to add to rectangle, but the most exciting one for our purposes right now is this boolean containsPoint(double, double) method.

class Circle

Let's say we make another shape, a Circle class:

public class Circle {
    public double x;
    public double y;
    public double radius;

    public Circle(double x, double y, double radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
    public double getCenterX() {
        return this.x;
    }
    public double getCenterY() {
        return this.y;
    }
    public double diameter() {
        return this.radius * 2;
    }
    public double area() {
        return this.radius * this.radius * Math.PI;
    }
    public boolean containsPoint(double x, double y) {
        double dx = (x - this.x);
        double dy = (y - this.y);
        return dx * dx + dy * dy < this.radius * this.radius;
    }
}

Working with our shapes:

Lets imagine we now want to handle or generate large numbers of these shapes.

public class ShapesEx {
    public static void main(String[] args) {
        Circle c = new Circle(0, 0, 50);
        Rectangle r = new Rectangle(20, 10, 25, 50);

        System.out.println(c.containsPoint(r.getX(), r.getY()));
    }
}

The two shapes we've created would look something like this (note the upside-down y-axis... at least as compared to algebra):

+y -y +x -x

With the picture in your head, it should be a little more obvious that the top-left point on the rectangle is safely inside the circle object, and that our main method code above prints true.

A list of Shapes (first try)

Let's say we want to put Circle and Rectangles into the same list; after all, they can both compute area tell us if a point is inside their shape containsPoint, and tell us where their center is located (getCenterX(), getCenterY()).

import java.util.List;

public class ShapesEx {
    public static void main(String[] args) {
        Circle c = new Circle(0, 0, 50);
        Rectangle r = new Rectangle(20, 10, 25, 50);

        List<Object> shapes = List.of(c, r);
        System.out.println(shapes);
    }
}

Here's the problem, though. We have to specify what type of objects our list contains. And unfortunately, the best answer that we have for that is java.lang.Object... the most generic of all classes.

Object is really only good for printing things out. Maybe you could convert it back to what you care about, but then we'd have to write some code like this:

Circle c = new Circle(0, 0, 50);
Rectangle r = new Rectangle(20, 10, 25, 50);

List<Object> shapes = List.of(c, r);
for (Object o : shapes) {
    if (o instanceof Circle) {
        Circle c = (Circle) o;
        System.out.println("Area: " + c.area());
    } else if (o instanceof Rectangle) {
        Rectangle r = (Rectangle) o;
        System.out.println("Area: " + r.area());
    } else {
        throw new RuntimeException(o + " is not a known shape.");
    }
}
System.out.println(shapes);

That's a lot of new stuff. Turns out you can use the instanceof keyword to tell whether a variable has a particular class (or is a subclass thereof).

But then, inside that if-statement, you still have the problem that the type of the variable o is still Object, and so, even once we confirm that it's actually a Circle, we need to tell Java to treat it like one: thus the cast to a Circle c = (Circle) o.

Think of casting as a promise to Java; you're promising that the variable o is actually of type Circle and therefore it's OK to put it in the Circle c variable. Java will complain later if this is false.

Casting is dangerous, because you're telling Java to ignore its instincts and trust you:

String nope = "Not a circle";
Circle c = (Circle) nope;
System.out.println("We'll never get here.");

A string can never be a Circle, but Java will try (and crash with a ClassCastException) and fail anyway, because you asked.

There must be a better way than instanceof and casting everywhere.

interface Shape

What we need is a way to express to Java that our Rectangle and Circle classes have some similarities (and what they are).

// interfaces declare methods without defining them.
public interface Shape {
    double area(); 
    // no curly-braces, no body
    boolean containsPoint(double x, double y);
    // each of these just declares that every shape will have these methods.
    double centerX();
    double centerY();
}

Now, we can go back and edit our classes:

public class Rectangle implements Shape {
    // ... leave the entire body the same ...
}
public class Circle implements Shape {
    // ... leave the entire body the same ...
}

And now we can edit our code in our main to use a List<Shape>, clearly explaining to Java how to unify the ideas of Circle and Rectangle:

Circle c = new Circle(0, 0, 50);
Rectangle r = new Rectangle(20, 10, 25, 50);

List<Shape> shapes = List.of(c, r);
for (Shape s : shapes) {
    System.out.println("Shape: "+ s);
    // we can freely call the methods of Shape here:
    System.out.println("Center: (" + s.getCenterX() + "," + s.getCenterY() + ")");
    System.out.println("Area: " + s.area());
    System.out.println("Has-Origin: " + s.containsPoint(0, 0));
}

List is an interface

One of the important reasons we talk about interfaces in data structures is because the standard Java collections are defined around interfaces.

In fact, the most simple interface they all implement is (abbreviated by me):

interface Collection<T> {
    // how big is it?
    int size();
    // give us a loop object:
    Iterator<T> iterator();
}

And List<T> adds a whole bunch of methods we'd expect to that:

interface List<T> extends Collection<T> {
    // get the index-th element:
    T get(int index);
    // replace the index-th element, returning the old one.
    T set(int index, T value);
    // remove the index-th element, returning its value.
    T remove(int index);
    // remove value if possible, returning false if not.
    boolean remove(T value);
    // add to back if possible (basically always returns true)
    boolean add(T value);
    // ... etc ...
}

When we construct a list, I've taught you to do the following:

List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle(10, 10, 40));

Here we're explicitly using the interface List to type our variable shapes, even though we know it's specifically an ArrayList.

What this does is prevent us from accidentally using ArrayList-specific methods or variables, and so later we can decide to use an alternate implementation of List:

List<Shape> shapes = new LinkedList<>();
shapes.add(new Circle(10, 10, 40));

It's especially useful for more general methods; imagine you're writing a sorting algorithm:

public static List<Integer> insertionSort(List<Integer> input) {
    // ... ...
}

Here, we've declared that insertionSort is a method that takes any type of List you want, and returns something that is a List. Therefore, we allow users to send us any kind of List they have, and do not commit to returning to them any specific kind of List either.

If we had our parameter be ArrayList instead, then we'd be saying our sorting method didn't work on other kinds of lists (which would be rather unfortunate).

If we committed to returning ArrayList, we might never be able to change it to something else later.

Coding to interfaces rather than concrete classes is considered good practice in modern Java.