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 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.
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
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.
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.
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 APuzzle
≤ Puzzle
.)
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:
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.
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.
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.
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.
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.
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.
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.