Interfaces and subtyping

The word “interface” has more than one meaning. In the context of computer science generally, it refers to a description or specification of the way that program modules interact with one another, or the way that client code interacts with a program module. In the context of Java, “interface” refers to a language feature that allows programmers to create interfaces for classes, which are a module mechanism. An interface describes a set of public methods that must be provided by the class; when using the class via the interface; only these public methods can be used. The interface includes the name and signature of the methods; it's also a good place to write specifications for those methods.

Example

For example, suppose that we want to implement the 2048 game, a popular game for cell phones consisting of movable numbered tiles in a 4×4 grid. We want to keep track of the state of the game at any given point and allow the user to make moves in the game.

Let's define an interface to the game. We'll call it Puzzle:

Puzzle.java

Notice that this interface says nothing about how the various methods are implemented or about how the puzzle information is represented inside objects. The interface doesn't say that the methods are public, because interface methods are always public.

An implementation

The Puzzle interface can be implemented by defining a class that is declared to implement it using the implements keyword. Now we get to make some implementation choices, such as how to represent the tiles of the puzzle. One obvious representation is as a two-dimensional array of integers, with 0 representing blank tiles:

APuzzle.java

There are few interesting aspects of this code. First, notice that each method declared in the interface must be implemented as a public method in the class. The class has some other components as well. For example, the instance variables tiles and score are declared private to hide them from clients. A class can also add new methods not declared in the interface, such as the APuzzle() constructor.

We can define an interface (in the general sense) to a class either by declaring which of its methods are public, or by declaring a Java interface as above. One advantage of using the interface mechanism is that it allows multiple implementations of the interface.

For example, here is a sketch of a second implementation of the Puzzle interface, in which the tiles are represented as characters in a string:

SPuzzle.java

Using objects at type Puzzle

We now have two implementations of Puzzle. The important new capability that is added is that client code can be written that does not care which implementation is being used. For example, suppose we want to write a method that displays a puzzle. That code can be written so it works on objects from either implementation:

display.java

The client code can use display() on either kind of object, because both APuzzle and SPuzzle are implementations of Puzzle.

As long as APuzzle and SPuzzle are implemented correctly, the code of display() cannot tell which implementation is being used. That is why display() can be used on objects of either kind. The abstraction barrier imposed by the Puzzle interface allows the programmer to start with one implementation and later to replace it with a different implementation, with confidence that it won't break the program.

When compiling the method call p.tile(r, c) in the method display(Puzzle p), the compiler does not know which method code it is going to run. The variable p can refer to either an APuzzle object or an SPuzzle object (or maybe some other implementation of Puzzle altogether). In general, the compiler cannot figure out ahead of time which one it will be. The call to p.tile(r, c) must find the correct method implementation at runtime. It does this by checking the dynamic type (aka runtime type) of the object p. The dynamic or runtime type of an object is the class that was named when the object was created via new. This is called dynamic dispatch.

Classes vs. types

In Java programs, each object has a dynamic or runtime type. The dynamic type is the name of the class that was specified in the new statement when the object was created. The dynamic type is associated with the object at birth and stays with it unchanged throughout its lifetime.

On the other hand, expressions in the Java language have static types that they obtain by declaration of variables. The static type of a variable is the type given to it in its declaration. (Note that this usage of the term static is not related to the Java keyword static used to declared static fields and methods.)

Both interface names like Puzzle and class names like APuzzle and SPuzzle may be used as static types in Java programs, but only class names may be used as dynamic types. The dynamic type of an object can never be Puzzle; it is always the class of the object that was provided to the new operator when the object was created.

For example, consider the following code. The first line creates an object with dynamic type APuzzle and assigns it to a variable of static type APuzzle. The second line assigns the same object to a variable of static type Puzzle. This is allowed because APuzzle is an implementation of Puzzle. The third line tries to use the interface Puzzle with the operator new. This is illegal, because Puzzle is not a class; the compiler doesn't know what implementation to use.

APuzzle a = new APuzzle(); // ok
Puzzle p = a; // ok
Puzzle p = new Puzzle();  // illegal

This shows that both class and interface names may be used as static types in Java, but only class names may be used as dynamic types.

Subtyping and subtype polymorphism

Because APuzzle implements Puzzle, an expression of static type APuzzle can always be used where a Puzzle is expected. This is an example of a subtype relationship between two types. We write APuzzle <: Puzzle to mean that the type APuzzle is a subtype of the type Puzzle. (Sometimes you will see this written as APuzzlePuzzle.) Since an SPuzzle can also be used wherever a Puzzle is expected, the subtype relationship SPuzzle <: Puzzle also holds.

The subtyping relationships among the various types form a type hierarchy, an example of which is shown in this figure:

subtype hierarchy

In addition to the subtype relationships we've discussed so far, this diagram shows a few more, such as Puzzle <: Object. The subtyping relation is reflexive (T<:T for all types T) and is also transitive (if T<:U and U<:V, then T<:V). Thus by transitivity, we also have APuzzle <: Object. We can also notice that array types like int[] are subtypes of Object. Standing alone in the diagram are the primitive types (int, boolean, char, ...). These types are not subtypes of any other type.

In the diagram above, every type has at most one parent, making this diagram a collection of trees (a forest). However, a class is allowed to implement more than one interface, as in the following definition:

class SPuzzle implements Puzzle, Collection<Integer> {
    ...
}

so in general the subtype diagram is a graph in which some nodes have more than one ancestor.

An invariant that always holds throughout execution is:

The dynamic type of the value of an expression is always a subtype of the static type of the expression.

In particular, whenever a variable references an object, the dynamic type of the object is a subtype of the declared type of the variable.

Subtyping vs. coercion

Java lets us write the following declaration, which might make us think incorrectly that int <: Object:

Object x = 2;

Although this looks like subtyping, actually Java is automatically inserting a coercion (conversion) to make the types work. The variable x is not being assigned the value appearing on the right-hand side, but rather an object of type Integer that is created automatically. The declaration is syntactic sugar for this one: Object x = Integer.valueOf(2). This mechanism is known as autoboxing.

Casting

With subtyping, a given value can be treated as though it has more than one type. (When a value can have more than one type, it is called polymorphism, and the kind of polymorphism we get with subtyping is called subtype polymorphism.)

Java's cast operator can be used to control the type at which we are using a value. As an example, suppose we have a variable a of type APuzzle. To force it to be treated as a Puzzle, we write (Puzzle) a. Since APuzzle is a subtype of Puzzle, this cast will always succeed at runtime: it is typesafe. We refer to this kind of cast as an upcast because it shifts the type upward in the type hierarchy.

It is also possible to cast downward in the subtype hierarchy. This gives us a downcast, an unsafe cast that can fail at runtime. For example, consider this code:

APuzzle a = new APuzzle();
Puzzle p = a;
APuzzle a2 = (APuzzle) p;

Here, all three variables refer to the same underlying APuzzle object. The downcast to a2 succeeds at run time because the dynamic type of the object is a subtype of the type it is being cast to. On the other hand, if we had reassigned the variable p to refer to an SPuzzle object, the downcast would fail with a ClassCastException.

A downcast should ordinarily be used only when it is guaranteed to succeed, so a failed downcast generally means that the programmer has made a mistake. It is possible to test in advance whether a downcast will succeed by using the instanceof operator to test the runtime type of the object.

Puzzle p = ...;
if (p instanceof APuzzle) {
    APuzzle a2 = (APuzzle) p;
    ...
}

Downcasting and instanceof are sometimes necessary, but excessive use of these operations is a danger sign that your code is not well designed. If you find yourself using them a lot, it is worth thinking about whether there is a way to redesign your code to avoid them.

Upcasting to a supertype can sometimes be used as a form of information hiding. Upcasting to a supertype hides operations not present in the supertype. The corresponding downcast can be used to restore access to these operations if desired. In general it is good form to work at the highest level possible that still allows the desired operations to be performed. Often this means accessing objects only through the interfaces they implement.

Conformance

For one type to be a subtype of another, the methods of the first type must have signatures that conform to the signatures of those in the supertype. The conformance requirement has implications for the types and visibility of methods.

For conformance, Java requires that the types of method formal parameters in the subtype be exactly the same as the types of the corresponding formal parameters in the supertype. However, the return type of a method in the subtype may be a subtype of the corresponding return type in the supertype.

In addition, the visibility of instance methods in the subclass is not allowed to be any less than the visibility in the superclass. Visibility modifiers can be ordered as follows:

private < (package) < protected < public

To see why, suppose that a method is declared public in the supertype but is private in the subtype. If we have a reference to an object of the subtype, the method will be inaccessible, but will become accessible if the object is upcast to the supertype. Therefore, the private visibility modifier is not meaningful. On the other hand, it is legal to have a method that is private in the supertype but public in the subtype.

Interface subtyping

Interfaces can extend each other, creating a subtyping relationship. For example, we might want a puzzle interface that adds a method for automatically solving the puzzle. If we declare the interface to "extend" Puzzle, it will be a subtype:

interface SolvablePuzzle extends Puzzle {
    Move[] solve();
}

The definition of SolvablePuzzle makes it a subtype of Puzzle: that is, SolvablePuzzle <: Puzzle, and SolvablePuzzle has all of the methods of Puzzle in addition to the new solve() method that it adds.

The ability to extend interfaces gives even more control over how much of a class is exposed to client code. Clients that do not need the solve() method can be given the Puzzle view of the puzzle, avoiding unnecessarily coupling of the implementing class and its clients.

It is worth noting that subtype relationships need to be explicitly declared in Java. If we declared another separate interface Q that had all the same methods as Puzzle, plus some additional ones, but did not declare Q to extend Puzzle, it might appear harmless to allow Q <: Puzzle. Unlike some languages, Java does not allow this structural subtype relationships, but only nominal subtyping, in which subtypes are explicitly declared. There are two reasons for this design: first, structural subtyping is hard to implement efficiently, and second, just because two methods have the same name and signature does not mean that they actually mean the same thing.

Factory methods

The use of interfaces allows us to write code that is independent of the choice of implementation—except when the objects are actually created. At some point an actual implementing class must be provided to new. This might seem to break the principle of separating interfaces from implementations.

When this stronger separation is desired, a common solution is to use factory methods to build objects. A factory method is one that creates objects, typically with an interface type. For example, we could (in some class) declare a method that creates Puzzle objects:

static Puzzle createPuzzle();

This method can be used to create Puzzle objects without committing to using any particular implementation of the Puzzle interface.

Summary

Java interfaces are a very useful mechanism for writing code in which module clients and module implementations are strongly separated. In addition, Java's support for subtyping means that it is possible to write multiple implementations of the same interface. These different implementations can even coexist and operate within the same program. This is a valuable feature for building large software systems.