(Coursenotes for CSC 203 Project-based Object-oriented Programming and Design)

Interfaces (part 1)

In this lesson, you will learn:

  • About interfaces, the general concept.
  • About interfaces, embodiment of that concept in the Java language.
  • About how they are useful, using lists as an example.
  • About the meaning of polymorphism.

What is an interface?

What does the word “interface” mean to you?

You may have heard the term in various contexts. For example, a graphical user interface (GUI) is the “frontend” that a computer application presents to the user, usually with buttons to click, text or images on screen, and information dialogues to convey information.

An interface is the surface at which two systems interact. A system here can be a person, an organization, a computer systems, etc.

For example, consider the file system on your computer. You have data on your computer organised into a hierarchy of files and folders. You can interact with this file system using your operating system’s file explorer GUI, e.g., the Finder app in macOS. What you can do is dictated by the behaviours exposed by the interface. That is, you can drag-and-drop files to move them, you can right-click on files or folders to copy, paste, rename, or delete them, and so on.

A screenshot of the Finder app in macOS, depicting a home folder.
You can interact with the file system using this graphical user interface. What you can do with these files is dictated by the behaviours exposed by the interface.

You can also interact with the same underlying system using another interface: the command line. The command line exposes the same behaviours and more — again, what you can accomplish is determined by the behaviours exposed by the interface.

A screenshot of the iTerm app in macOS, depicting the contents of a home folder.
You can also interact with the file system using the command line interface.

In both cases, the interfaces above hide many details from you, the user, like the actual organisation of bits and bytes on disk, the permissions system that dictates who can see or use what files, etc.

Those interfaces are the surface at which the two systems—the user and the file system—interact with each other. Similarly, classes in Java (or, more generally, modules in software systems) expose behaviours that other classes or modules can use to interact with them.

We’ve already seen interfaces in action

Though there is an interface keyword and program construct in Java, we aren’t talking about that just yet.

Even though we haven’t used that word explicitly, we’ve already been thinking about interfaces. When we create classes in Java, we tend to make our instance variables private and we make most of our instance methods public. This is us defining the “public surface” of our class, or the “public surface” that instances of that class will expose to other classes that want to interact with them.

The “public surface” here would include:

  • Method signatures for the public instance methods. I.e., their names, return types, and parameter types. These three give a sense of what the method does, what inputs it expects, and what outputs it will give back.
  • Documentation. Often, there is more detail needed to describe a method’s purpose. E.g., simply knowing that a method returns an int doesn’t necessarily tell us much about what that int means. There may be preconditions without which the method would not work as expected. There may be postconditions, i.e., effects that method will have that are not necessarily reflected in the returned value (for example, a setter method that mutates an object’s state). So we also write documentation that accompanies every public method.1

In sum:

The interface of a class exposes what the class does, but not how it does it.

Interfaces in Java

The interface keyword in Java takes this principle and makes it a language-level construct. An interface is a type of limited class. Unlike a class,

  • An interface declares instance methods, but does not define them. That is, it only specifies method signatures, and nothing else.2
  • An interface cannot declare instance variables. This makes sense from two perspectives.
    • First, if the interface dictated the available instance variables, it’s straying closer to saying how things should be done, i.e., implementation details. This is not the goal of interfaces.
    • Second, with no method definitions, there wouldn’t be much use for those instance variables.
  • An interface cannot have constructors, nor can it have any data or behaviour. So, unlike a class, an interface cannot be used as a blueprint from which to create objects.
  • An interface can have static variables and static methods.

Why do you think it’s okay for interfaces to have static members? Why do you suppose that would be useful?

By omitting all implementation details—like instance variables, and the bodies (definitions) of instance methods—the interface takes the “skeleton” of what a class should be able to do, and places that in a separate Java file.

Then, you can write a separate Java class that implements an interface by providing implementations for the methods that the interface declared. That class can have whatever instance variables it needs for its implementations.

Just like a class can be used as a blueprint from which to create objects, and interface is a blueprint for a class.

Let’s look at some examples!

A data structures example: Lists

Consider the List data structure. I don’t mean lists in any particular programming language—I just mean the abstract notion of a list. Abstractly, we expect lists to support the following basic operations:

  • get(index): Object: We should be able to get the item at the specified index.
  • add(item): We should be able to add an item to the back of the list.
  • add(index, item): We should be able to add an item to the specified index in the list.
  • remove(item): boolean: We should be able to remove the specified item from the list, and tell the user if something was removed or not.
  • remove(index): Object: We should be able to remove an item from a specified index in the list, and return the item that was removed.
  • contains(item): boolean: We should be able to check if the list contains the specified item.
  • size(): We should be able to get the number of items in the list.

In most cases, we don’t actually care how those operations are defined, as long as they accomplish their goals. A list is a list regardless of how the operations above are implemented.

The Java standard library provides the List interface to capture these expected behaviours. The Java List declares many more behaviours than what I’ve said above, but the idea remains the same: it declares the signatures for those methods, and provides documentation describing what the methods should accomplish, but gives no restrictions on how those tasks should be implemented. The List interface does this through the use of abstract methods.

Abstract methods are method declarations without definitions. I.e., they only declare the method signature, but do not include a method body. To put it in terms we’ve been talking about, abstract methods dictate what a List should do, but not how it should do it.

Below is a super-pared-down example of what the List interface might look like.

View in new tab

Because the interface contains no data and no behaviour, it cannot be initialised. That is, you cannot do the following:

List myList = new List(); // This will not compile

The left-hand-side of the above statement is okay. The right-hand-side will cause a compiler error—you cannot use new to create an instance of an interface, because the interface is just “meta-data”. There’s nothing there to initialise.

Using the List interface

Armed with the interface above, we can create list implementations. You have already seen two of them: ArrayList and LinkedList.

Both those data structures implement the behaviours required by the List interface. The ArrayList uses a backing array to keep track of the list’s contents, and the LinkedList uses a chain of nodes and pointers. But the “contracts” for the add, remove, contains, and size methods are fulfilled by both implementations.

A class can use the implements keyword to declare that it is an implementation of an interface. The class must contain implementations for all the abstract methods that were declared in the interface.

For example, here is what the ArrayList class might look like. It declares that it implements the List interface, and once it does so, it must include implementations (i.e., non-abstract, or “concrete” methods) for all the abstract methods that were declared in the interface.

public class ArrayList implements List {
  public E get(int index) {
    // omitted for brevity
  }

  public void add(E e) {
    // omitted for brevity
  }

  public void add(int index, E e) {
    // omitted for brevity
  }

  public boolean remove(E e) {
    // omitted for brevity
  }

  public E remove(int index) {
    // omitted for brevity
  }

  public boolean contains(E e) {
    // omitted for brevity
  }

  public int size() {
    // omitted for brevity
  }
}

The ArrayList and LinkedList classes must contain implementations for all the abstract methods declared in the List interface. If any of the methods are omitted (or if the names are misspelled, parameter lists are different, etc.) you will get a compiler error that won’t go away until you implement all of the methods.

The two list implementations can contain:

  • Instance variables — these can be different for each class.
  • Additional methods — they can (and do) specify additional behaviours in addition to what is required by the List interface.

The relationship between an interface and its implementing subclasses

When a class implements an interface, the class is said to be an implementing subclass of the interface. Recall from our lesson on method dispatch that the Object class is the root of the Java type hierarchy. All reference types in Java are subtypes or child types of the Object class.

When we create an interface (say, the List interface), we are introducing that interface into the type hierarchy. And when we create implementations of the List interface, we are saying that ArrayList and LinkedList are subtypes of List.

A List is an Object. An ArrayList is a List, and a LinkedList is a List.

Our updated Java type hierarchy now looks like this:3

flowchart BT
  Other[All reference types in Java, including...]
  Other & Point & String & List --is a--> Object 
  ArrayList & LinkedList --is a--> List 

This means that we can declare and initialise lists like this:

// For example, say we are working with a list of Points
List<Point> points = new ArrayList<>();

What’s the static type of the points variable above? What’s its dynamic type? When might we be faced with a situation where its useful or necessary to have these be different?

Why is this useful?

Recall our discussion on coupling and cohesion. Our goal is to write cohesive classes that are loosely coupled with each other. If classes are tightly coupled, they have to change together, because they depend on each other’s implementation details. When classes are loosely coupled, they are mostly independent. They still work together to enable the system as a whole to function, but it means that a class in a larger system can be modified without other classes noticing, as long as its public interface stays the same.

Usually, we achieve this by making all the data inside a class private. The idea is simple: if we hide internal implementation details (like the instance variables and methods that are only used internally), then other classes can’t depend on those implementation details.

The interface construct in Java codifies this by creating a whole new “type”, just for the public interface.

So for example, suppose you are writing a function in a statistics package that computes a mean from a collection of data. You would ideally make your function take a List as a parameter, instead of the more specific ArrayList or LinkedList.

public static double mean(List<Integer> data) {
  if (data.size() == 0) {
    return 0.0;
  }

  int sum = 0;
  for (int current : data) {
    sum = sum + current;
  }
  
  return sum / data.size();
}

Some other module that is using this function is now free to use whatever list implementation it wants — the ArrayList or the LinkedList. When that module calls your mean function, the input will be a List at compile time, but will be the specific list subtype at run tme. Dynamic dispatch will help the runtime figure out whether to call ArrayList methods or LinkedList methods while the program runs.

This is an example of polymporphism. Polymorphism literally means “many forms” — the data variable in the function above can take (one of) many forms at run time.

Your mean function is decoupled from the specifics of how the other module is handling its data. It can use a LinkedList, an ArrayList, or some newfangled list type that we haven’t heard of, as long as it exposes a List-like interface; your function would never know the difference.


  1. The exact format of that documentation is quite simple and beyond the scope of this lesson. Documentation in Java is written in a format that can be understood by a tool called JavaDoc. JavaDoc can read your comments and generate a web-page containing documentation. For example, this ArrayList documentation was generated by JavaDoc based on the comments in this Java file. Here is a nice concise overview of the JavaDoc format 

  2. This is a temporary simplification. We’ll talk about default methods in the next lesson. If you already know about them, allow me this sin of omission for the time being. 

  3. In reality, there are a few more “intermediate” layers between Object and List — but this gets the point across. We will talk in the next couple of lessons about interfaces that are subtypes of other interfaces.