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.
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 Board
:
Board.java
Notice that this interface says nothing about how the various methods are implemented or about how the game information is represented inside objects. The interface doesn't say that the methods are public, because interface methods are always public.
The method move()
uses a feature we haven't seen yet:
enum
(enumeration) types.
The type Direction
is declared
as follows:
Direction.java
This type has exactly the four specified values UP
,
DOWN
, LEFT
, and RIGHT
. Of course,
we could encode the four directions using another type such as
numbers or characters, but when used appropriately, using an enum
makes code easier to read and less error-prone.
The Board
interface can be implemented by defining a class
that is declared to implement it using the implements
keyword. Now we must make some implementation choices, such as how
to represent the tiles of the game. One obvious representation is as a
two-dimensional array of integers, with 0 representing blank tiles:
ABoard.java
This code has a few notable aspects. 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 ABoard()
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.
The code also shows how to use a switch
statement to
execute different code based on the value of an expression. These
statements are particularly useful in combination with enum
types, because they make it easy to check that all possible values have
been covered by some case
. Notice that each case
should be terminated by a break
statement; Java unfortunately
inherits the C semantics of switch
, in which each case
falls through into the code of the next case unless some statement,
such as a break
or return
,
stops the execution of the switch
.
For example, here is a sketch of a second implementation of the Board
in which the Board
keeps track of the positions of the tiles
rather than what tile is located at each position.
TBoard.java
The method tile()
becomes more complicated because the
code has to search for a title at the specified location. When it finds
such a tile, the method returns immediately. If no matching tile is
found, it returns zero.
And here is a third implementation which the tiles are represented as characters in a string, aiming for efficient use of the computer's memory:
SBoard.java
As we can see, even a simple abstraction like Board
has a variety
of implementations, with different tradeoffs in simplicity of code, running
time, and space used. If the rest of the code accessing the board through the
interface Board
, the code has loose coupling. We can easily change
to another implementation of Board if we gain better understanding of these
tradeoffs. For example, a search algorithm to suggest the next move might need
to allocate a large number of board objects; in that case, we would likely
prefer the space-efficient implementation SBoard
.
We now have three implementations of Board
. 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 game board. 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 ABoard
and SBoard
are implementations of Board
.
As long as ABoard
and SBoard
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 Board
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(Board b)
, the compiler does not know which method code it
is going to run. The variable p
can refer to either an
ABoard
object or an SBoard
object (or maybe some
other implementation of Board
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 run
time. It does this by checking the dynamic type (aka run-time type) of the
object p
. The dynamic or run-time type of an object is the class
that was named when the object was created via new
. This is called
dynamic dispatch.
In Java programs, each object has a dynamic or
run-time 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 Board
and class names like
ABoard
and SBoard
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 Board
; 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
ABoard
and assigns it to a variable of static type ABoard
. The second
line assigns the same object to a variable of static type Board
. This is allowed
because ABoard
is an implementation of Board
. The third line tries to
use the interface Board
with the operator new
. This is illegal, because
Board
is not a class; the compiler doesn't know what implementation to use.
ABoard a = new ABoard(); // ok Board b = a; // ok Board p = new Board(); // 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.
Because ABoard
implements Board
, an expression of
static type ABoard
can always be used where a
Board
is expected. This is an example of a subtype relationship
between two types. We write ABoard
<: Board
to
mean that the type ABoard
is a subtype of the type
Board
. (Sometimes you will see this written as ABoard
≤ Board
.)
Since an SBoard
can also be used wherever a
Board
is expected, the subtype relationship
SBoard
<: Board
also holds.
The subtyping relationships among the various types form a type hierarchy (or subtype hierarchy), an example of which is shown in this figure:
In addition to the subtype relationships we've discussed so far, this diagram shows a few
more, such as Board
<: 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 ABoard
<: 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 TBoard implements Board, Collection<Integer> { ... }
so in general the subtype diagram is a graph in which some nodes have more than one ancestor.
If static type checking is doing its job, then the following invariant should always hold 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.
However, Java has some unsafe features that can break this invariant. These features should be avoided when possible!
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 (a 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.
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 compile-time type at
which a value is used. A cast does not affect the run-time type of the value,
only the type which the compiler will treat the value as belonging to.
As an example, suppose we have a variable a
of type
ABoard
. To force it to be treated as a Board
, we
write a cast expression: (Board) a
. Since ABoard
is a subtype of
Board
, this cast will always succeed at run time: it is
type-safe. We refer to this kind of cast as an upcast because it
shifts the type upward in the type hierarchy. In fact, it is not normally necessary to
write a cast operation, since the language will automatically treat a subtype
value as belonging to the supertype when necessary. Occasionally it is useful
to write an explicit upcast when debugging.
It is also possible to cast downward in the subtype hierarchy. This gives us a downcast, an unsafe cast that can fail at run time. For example, consider this code:
ABoard a = new ABoard(); Board p = a; ABoard a2 = (ABoard) p;
Here, all three variables refer to the same underlying ABoard
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 SBoard
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 run-time type of the object.
Board p = ...; if (p instanceof ABoard) { ABoard a2 = (ABoard) p; ... }
In recent versions of Java, it is possible to avoid writing an explicit cast by introducing a variable in the instanceof expression itself:
Board p = ...; if (p instanceof ABoard a2) { ... }
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, although not as completely as using private visibility. The corresponding downcast can be used to restore access to these operations if desired, so the abstraction barrier is porous. In general, it is good form to work with the narrowest interface that still allows the desired operations to be performed. Often this means accessing objects only through the interfaces they implement rather than through their classes.
An interface can extend
another interface, creating a subtyping relationship.
For example, we might want a board interface that adds a method for automatically solving the board.
If we declare the interface to extend Board
, it will be a subtype:
interface SolvableBoard extends Board { Move[] solve(); }
The definition of SolvableBoard
makes it a subtype of
Board
: that is, SolvableBoard
<: Board
,
and SolvableBoard
has all of the methods of Board
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
Board
view of the board, avoiding unnecessarily coupling of the implementing
class and its clients. However, the possibility of downcasting means that the abstraction barrier
is more permeable when interfaces are in this way: we get a form of information hiding but not
full encapsulation.
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 Board
, plus some additional ones, but did not declare
Q
to extend Board
, it might appear harmless to
allow Q
<: Board
. 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.
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 were declared public in the supertype but was private in the subtype. If we had a reference to an object of the subtype, the method would be inaccessible, but would become accessible after an upcast to the supertype. That private visibility modifier would be misleading and useless, so it is disallowed in Java. On the other hand, it makes sense and is legal to have a method that becomes more accessible in the subtype.
Conformance between subtypes and supertypes extends beyond merely the signatures of methods. Subtyping should govern the behavior of the subtype as well. If it is safe to use a subtype \(S\) of type \(T\) wherever \(T\) is expected, then the specification of the subtype operations must also conform to the specification of the \(T\) operations; otherwise, client code expecting the behavior specified by \(T\) will be unpleasantly surprised by the behavior of \(S\).
In other words, subtyping used correctly is behavioral subtyping; it ensures not only that types are compatible but also that behavior is. The principle that objects of a subtype can always be safely substituted for supertypes without surprising clients of those supertypes is called the Liskov substitution principle, after Barbara Liskov.
With respect to the components of the specification, the implication of this conformance is that the postcondition of an \(S\) operations must be at least as strong as (that is, imply) the postcondition of the corresponding \(T\) operation. Conversely, the caller using the \(S\) object as if it is a \(T\) would call the operations of the object while only satisfying the preconditions specified by \(T\). Therefore, the preconditions of \(S\) must be implied by the preconditions of \(T\).
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 Board
objects:
static Board createBoard();
This method can be used to create Board
objects without committing
to using any particular implementation of the Board
interface. A
factory method need not be static; it may also be a method of some object that
determines which implementation to create.
Java interfaces are a 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.