“Conducting” is when you draw “designs” in the nowhere—with your stick, or with your hands—which are interpreted as “instructional messages” by guys wearing bow ties who wish they were fishing. | ||
--Frank Zappa |
The fundamental unit of programming in the Java programming language is the class, but the fundamental unit of object-oriented design is the type. While classes define types, it is very useful and powerful to be able to define a type without defining a class. Interfaces define types in an abstract form as a collection of methods or other types that form the contract for that type. Interfaces contain no implementation and you cannot create instances of an interface. Rather, classes can expand their own types by implementing one or more interfaces. An interface is an expression of pure design, whereas a class is a mix of design and implementation.
A class can implement the methods of an interface in any way that the designer of the class chooses. An interface thus has many more possible implementations than a class. Every major class in an application should be an implementation of some interface that captures the contract of that class.
Classes can implement more than one interface. The Java programming language allows multiple inheritance of interface but only single inheritance of implementation—a class can extend only one other class. Classes can use inheritance of interfaces to expand their type and then use, for example, composition to provide an implementation for those interfaces. This design allows the typing flexibility of multiple inheritance while avoiding the pitfalls of multiple implementation inheritance, at the cost of some additional work for the programmer.
In a given class, the classes that are extended and the interfaces that are implemented are collectively called the supertypes, and from the viewpoint of the supertypes, the new class is a subtype. The new class includes all its supertypes, so a reference to an object of the subtype can be used polymorphically anywhere a reference to an object of any of its supertypes (class or interface) is required. Interface declarations create type names just as class declarations do; you can use the name of an interface as the type name of a variable, and any object whose class implements that interface can be assigned to that variable.
Many simple interfaces define a property that is ascribable to a variety of different objects from different classes. These properties are often defined in terms of an object being “able” to do something. For example, in the standard packages there are a number of “ability” interfaces, such as:
Cloneable
—. Objects of this type support cloning, as you learned in detail on page 101.
Comparable
—. Objects of this type have an ordering that allows them to be compared.
Runnable
—. Objects of this type represent a unit of work, that can often execute in an independent thread of control (see Chapter 14).
Serializable
—. Objects of this type can be written to an object byte stream for shipping to a new virtual machine, or for storing persistently and then reconstituting into a live object (see “Object Serialization” on page 549).
Let's look at the Comparable
interface in more detail. This interface can be implemented by any class whose objects can be compared to each other according to the class's “natural ordering.” The interface contains a single method:
public interface Comparable<T> { int compareTo(T obj); }
An interface declaration is similar to a class declaration, except that the keyword interface
is used instead of class
. There are also special rules concerning the members of an interface, as you will soon learn.
The compareTo
method takes a single object argument of type T
and compares it to the current object (expected to also be of type T)
, returning a negative, zero, or positive integer if the current object is less than, equal to, or greater than the argument, respectively.
Consider a variation of the Point
class we introduced in Chapter 1. The natural ordering for points could be their distance from the origin. We could then make Point
objects Comparable
:
class Point implements Comparable<Point> { /** Origin reference that never changes */ private static final Point ORIGIN = new Point(); private int x, y; // ... definition of constructors, setters and accessors public double distance(Point p) { int xdiff = x - p.x; int ydiff = y - p.y; return Math.sqrt(xdiff * xdiff + ydiff * ydiff); } public int compareTo(Point p) { double pDist = p.distance(ORIGIN); double dist = this.distance(ORIGIN); if (dist > pDist) return 1; else if (dist == pDist) return 0; else return -1; } }
First we declare that Point
is a Comparable
class. A class identifies the interface types that it implements by listing them after the keyword implements
, before the class body is defined (and after any extends
clause). All such interfaces are the superinterfaces of the class. The class must provide an implementation for all of the methods defined in its superinterfaces, or else the class must be declared abstract
, thereby requiring that any non-abstract subclass implement them.
The only method we need to implement for the Comparable
interface is compareTo
, and to actually compare two points we simply compare their distance from the origin.
Interfaces introduce type names just as classes do, so you can declare variables of those types. For example:
Comparable<Point> p1;
In fact much of the power of interfaces comes from declaring and using only variables of interface type rather than of some specific class type. For example, you can define a general-purpose sort
routine that can sort any array of Comparable
objects without regard to what the class of those objects actually is—all the objects in the array should be the same type of course:[1]
class Sorter { static Comparable<?>[] sort(Comparable<?>[] list) { // implementation details ... return list; } }
References of interface type, however, can be used only to access members of that interface. For example, the following will produce a compile-time error:
Comparable<Point> obj = new Point(); double dist = obj.distance(p1); // INVALID: Comparable has // no distance method
If you want to treat obj
as a Point
object you must explicitly cast it to that type.
You can invoke any of the Object
methods using a reference of an interface type because no matter what interfaces the object implements, it is always an Object
and so has those methods. In fact, any interface that does not extend some other interface implicitly has members matching each of the public methods of Object
(unless the interface explicitly overrides them). Hence, the following is legal:
String desc = obj.toString();
as is assigning an interface reference to an Object
reference.
An interface is declared using the keyword interface
, giving the interface a name and listing the interface members between curly braces.
An interface can declare three kinds of members:
constants (fields)
methods
nested classes and interfaces
All interface members are implicitly public, but, by convention, the public
modifier is omitted. Having non-public members in an interface would make little sense; where it does make sense you can use the accessibility of the interface itself to control access to the interface members.
We defer a discussion of nested classes and interfaces until Chapter 5.
An interface can declare named constants. These constants are defined as fields but are implicitly public
, static
, and final
—again, by convention, the modifiers are omitted from the field declarations. These fields must also have initializers—blank finals are not permitted. Annotations can also be applied to the fields—see Chapter 15.
Because interfaces contain no implementation details, they cannot define normal fields—such a definition would be dictating implementation policy to the classes that choose to implement the interface. Interfaces can define named constants because these are useful in the design of types. For example, an interface that had differing levels of verbosity in its contract might have the following:
interface Verbose { int SILENT = 0; int TERSE = 1; int NORMAL = 2; int VERBOSE = 3; void setVerbosity(int level); int getVerbosity(); }
SILENT
, TERSE
, NORMAL
, and VERBOSE
can be passed to the setVerbosity
method, giving names to constant values that represent specific meanings. In this particular case, the verbosity levels could be more effectively represented as the constants of an enumeration type nested within the Verbose
interface. Enumeration types are discussed in Chapter 6.
If you need shared, modifiable data in your interface you can achieve this by using a named constant that refers to an object that holds the data. A nested class is good for defining that object, so we defer an example until Chapter 5.
The methods declared in an interface are implicitly abstract
because no implementation is, or can be, given for them. For this reason the method body is simply a semicolon after the method header. By convention, the abstract
modifier on the method declaration is omitted.
No other method modifiers are permitted on an interface method declaration, except for annotations—see Chapter 15. They are implicitly public
and so can have no other access modifier. They cannot have modifiers that define implementation characteristics—such as native
, synchronized
, or strictfp
—because an interface does not dictate implementation, and they cannot be final
because they haven't been implemented yet. Of course, the implementation of these methods within a specific class can have any of these modifiers. Interface methods can never be static
because static
methods can't be abstract
.
An interface declaration can be preceded by interface modifiers:
annotations—. Annotations and annotation types are discussed in Chapter 15.
public—. A public
interface is publicly accessible. Without this modifier an interface is only accessible within its own package.
abstract—. All interfaces are implicitly abstract
because their methods are all abstract—they have no implementation. Again, by convention, the abstract
modifier is always omitted.
strict floating point—. An interface declared strictfp
has all floating-point arithmetic, defined within the interface, evaluated strictly. This applies to initialization expressions for constants and to all nested types declared in the interface. In contrast to classes, this does not imply that each method in the interface is implicitly strictfp
because that is an implementation detail. See Section 9.1.3 on page 203 for details.
When multiple modifiers are applied to the same interface declaration, we recommend using the order listed above.
Interfaces can be extended using the extends
keyword. Interfaces, unlike classes, can extend more than one other interface:
public interface SerializableRunnable extends java.io.Serializable, Runnable { // ... }
The SerializableRunnable
interface extends both java.io.Serializable
and Runnable
, which means that all methods and constants defined by those interfaces are now part of the SerializableRunnable
contract, together with any new methods and constants it defines. The interfaces that are extended are the superinterfaces of the new interface and the new interface is a subinterface of its superinterfaces.
Because interfaces support multiple inheritance, the inheritance graph can contain multiple paths to the same superinterface. This means that constants and methods can be accessed in different ways. However, because interfaces define no implementation of methods, and provide no per-object fields, there are no issues regarding the semantics of this form of multiple inheritance.
An extended interface inherits all the constants declared in its superinterfaces. If an interface declares a constant of the same name as an inherited constant, regardless of their types, then the new constant hides the inherited one; this is the same hiding of fields described in “Hiding Fields” on page 86. In the subinterface and in any object implementing the subinterface, any reference to the constant using its simple name will refer to the constant defined in the subinterface. The inherited constant can still be accessed with the qualified name of the constant, that is, the interface name followed by dot and then the constant name—the usual way of referring to static members.
interface X { int val = 1; } interface Y extends X { int val = 2; int sum = val + X.val; }
Interface Y
has two constants: val
and sum
. From inside Y
, to refer to the hidden val
in its superinterface you must qualify it as X.val
. Externally, you can access the constants of Y
by using the normal static forms of Y.val
and Y.sum
, and of course you can access X
's val
constant by using X.val
.
These rules are, of course, identical to those concerning the inheritance of static fields in classes.
When a class implements Y
you can access the constants in Y
as though they were constants declared in the class. For example, given
class Z implements Y { }
you can do
System.out.println("Z.val=" + Z.val + ", Z.sum=" + Z.sum);
but there is no way to refer to X.val
via Z
. However, given an instance of Z
you can use an explicit cast to access X.val
:
Z z = new Z(); System.out.println("z.val=" + z.val + ", ((Y)z).val=" + ((Y)z).val + ", ((X)z).val=" + ((X)z).val);
which prints out
z.val=2, ((Y)z).val=2, ((X)z).val=1
as you would expect. Again these are the same rules that apply to static fields in extended classes—it doesn't matter whether a class inherits a static field from a superclass or a superinterface.
While all these rules are necessary from a language perspective, they have little practical consequence—there are few reasons to hide existing fields, and all accesses to static fields, whether class or interface, should be via the name of the type in which that field is declared.
If an interface inherits two or more constants with the same name, then any simple reference to the constant is ambiguous and results in a compile-time error. For example, given the previous interface declarations and the following:
interface C { String val = "Interface C"; } interface D extends X, C { }
then the expression D.val
is ambiguous—does it mean the integer val
or the String
reference val
? Inside D
you would have to explicitly use X.val
or C.val
.
A class that implements more than one interface, or that extends a class and implements one or more interfaces, can experience the same hiding and ambiguity issues as an interface that extends more than one interface. The class's own static fields can hide the inherited fields of the interfaces it implements or the class it extends, and simple references to multiply-inherited non-hidden fields will be ambiguous.
A subinterface inherits all the methods declared in its superinterfaces. If a declared method in a subinterface has the same signature (name and parameter list) as an inherited method and the same, or covariant, return type, then the new declaration overrides any and all existing declarations. Overriding in interfaces, unlike overriding in classes, has no semantic effect—the interface effectively contains multiple declarations of the same method, but in any one implementing class there can only be one implementation of that method.
Similarly, if an interface inherits more than one method with the same signature, or if a class implements different interfaces containing a method with the same signature, there is only one such method. The implementation of this method is ultimately defined by the class implementing the interfaces, and there is no ambiguity there. If the methods have the same signature but different return types, then one of the return types must be a subtype of all the others, otherwise a compile-time error occurs. The implementation must define a method that returns that common subtype.
The real issue is whether a single implementation of the method can honor all the contracts implied by that method being part of the different interfaces. This may be an impossible requirement to satisfy in some circumstances. For example:
interface CardDealer { void draw(); // flip top card void deal(); // distribute cards void shuffle(); } interface GraphicalComponent { void draw(); // render on default device void draw(Device d); // render on 'd' void rotate(int degrees); void fill(Color c); } interface GraphicalCardDealer extends CardDealer, GraphicalComponent { }
Here it is difficult to write an implementation of draw()
that can satisfy the two different contracts independently. If you try to satisfy them simultaneously, you are unlikely to achieve the desired results: flipping a card each time the screen gets repainted.
As with overriding in class extension, the overriding method is not permitted to throw more checked exceptions than the method it overrides. If two or more method declarations are inherited, without overriding, and differ in the exceptions they throw, then the implementation of that method must satisfy all the throws
clauses of those declarations. Again the main issue is whether such distinct methods can have a single implementation that honors all contracts. We look further at the issues of overriding and exception throwing in Chapter 12.
If a declared method has the same name but different parameters from an inherited method, then the declared method is an overloaded form of the inherited method. The eventual class implementation will provide a method body for each of the overloaded forms.
If a declared method differs only in return type from an inherited method, or if two inherited methods differ only in return type where one type is not a subtype of the other, you will get a compile-time error.
The previous chapter introduced the Attr
class and showed how to extend it to make specialized types of attribute objects. Now all you need is the ability to associate attributes with objects.
The first decision to make is whether having attributes is reflected in the type of the object. An object could, if you chose, contain a set of attributes and allow programmers access to that set. Or you could say that being able to store attributes on an object is a part of its type and so should be part of the type hierarchy. Both positions are legitimate. We believe that representing the ability to hold attributes in the type hierarchy is most useful. We will create an Attributed
type to be used for objects that can be attributed by attaching Attr
objects to them.
To create an Attributed
type you could define a class that would form the superclass for all attributed objects. But then programmers must decide whether to inherit from Attributed
or from some other useful class. Instead we make Attributed
into an interface:
public interface Attributed { void add(Attr newAttr); Attr find(String attrName); Attr remove(String attrName); java.util.Iterator<Attr> attrs(); }
This interface declares four methods: one for adding a new attribute to an Attributed
object; one for finding whether an attribute of a given name has been added to that object; one for removing an attribute from an object; and one for accessing all of the attributes currently attached to the object.
When we add an attribute to an object, that object considers itself the owner of that Attr
instance until it is removed. If the attribute has its value changed or is shared among a number of objects, then the expectation is that the programmer makes such changes and performs such sharing in a manner that makes sense to the application. If the programmer is not to be trusted in this, then methods should specify that they make defensive copies of parameters, and/or return values, such that no harmful changes can occur.
The attributes are accessed through an Iterator
object returned from the attrs
method. Iterator
is a generic interface defined in java.util
for collection classes to use to provide access to their contents (see “Iteration” on page 571). In effect, the Attributed
interface defines a collection type—a set of attributes—so we use the normal mechanism for accessing the contents of a collection, namely, the Iterator
type. Using Iterator
has another benefit: It is easy to implement Attributed
with a standard collection class (such as HashMap
) that uses Iterator
, as you'll soon see.
Many classes that provide an Iterator
declare that they implement the Iterable
interface, which defines the single method iterator
to return an Iterator
instance. Although an Attributed
object does provide an Iterator
, it would be wrong to have Attributed
extend Iterable
or to rename attrs
as iterator
, because that would restrict the ability of the class implementing Attributed
to control its own iteration behavior. For example, it would mean that an Attributed
collection class would not be able to provide an iterator for its elements rather than its attributes.
Interfaces describe contracts in a pure, abstract form, but an interface is interesting only if a class implements it.
Some interfaces are purely abstract—they do not have any useful general implementation but must be implemented afresh for each new class. Most interfaces, however, may have several useful implementations. In the case of our Attributed
interface, we can imagine several possible implementations that use various strategies to store a set of attributes.
One strategy might be simple and fast when only a few attributes are in a set; another one might be optimized for attribute sets that are queried more often than they are changed; yet another design might be optimized for sets that change frequently. If there were a package of various implementations for the Attributed
interface, a class might choose to implement the Attributed
interface through any one of them or through its own implementation.
As an example, here is a simple implementation of Attributed
that uses the utility java.util.HashMap
class. The class AttributedImpl
declares that it implements
the interface Attributed
, so the class must implement all the interface's methods. AttributedImpl
implements the methods using a HashMap
, described in “HashMap” on page 590. Later, this implementation is used to implement the Attributed
interface for a specific set of objects to which you would want to add attributes. First, here is the AttributedImpl
class:
import java.util.*; class AttributedImpl implements Attributed, Iterable<Attr> { protected Map<String, Attr> attrTable = new HashMap<String, Attr>(); public void add(Attr newAttr) { attrTable.put(newAttr.getName(), newAttr); } public Attr find(String name) { return attrTable.get(name); } public Attr remove(String name) { return attrTable.remove(name); } public Iterator<Attr> attrs() { return attrTable.values().iterator(); } public Iterator<Attr> iterator() { return attrs(); } }
The initializer for attrTable
creates a HashMap
object to hold attributes. This HashMap
object does most of the actual work. The HashMap
class uses the key object's hashCode
method to hash any object it is given as a key. No explicit hash method is needed since String
already provides a good hashCode
implementation.
When a new attribute is added, the Attr
object is stored in the hash map under its name, and then you can easily use the hash map to find and remove attributes by name. The attrs
method returns the Iterator
for the hash map's values, giving access to all the attributes of the current object.
We chose to make this implementation of Attributed
also implement Iterable<Attr>
, as the attributes are the only things an AttributedImpl
contains. To do this, we had to define the iterator
method to return the same value as the attrs
method.
You can use an implementing class like AttributedImpl
by simply extending the class. This is the simplest tool when it is available because all the methods and their implementations are inherited. But if you need to support more than one interface or extend a different class, you must use a different approach. The most common approach is to create an object of an implementing class and forward all the methods of the interface to that object, returning any values—this is often called composition.
In composition and forwarding, each method in the class that is inherited from the interface invokes the implementation from another object and returns the result. Here is an implementation of the Attributed
interface that uses an AttributedImpl
object to build an attributed version of our previously defined celestial body class Body
:
import java.util.Iterator; class AttributedBody extends Body implements Attributed { private AttributedImpl attrImpl = new AttributedImpl(); public AttributedBody() { super(); } public AttributedBody(String name, Body orbits) { super(name, orbits); } // Forward all Attributed methods to the attrImpl object public void add(Attr newAttr) { attrImpl.add(newAttr); } public Attr find(String name) { return attrImpl.find(name); } public Attr remove(String name) { return attrImpl.remove(name); } public Iterator<Attr> attrs() { return attrImpl.attrs(); } }
The declaration that AttributedBody
extends Body
and implements Attributed
defines the contract of AttributedBody
. The implementations of all Body
's methods are inherited from the Body
class itself. Each method of Attributed
is implemented by forwarding the invocation to the AttributedImpl
object's equivalent method, returning its value (if any). This also means that you must add a field of type AttributedImpl
to use in the forwarding methods and initialize that field to refer to an AttributedImpl
object.
Forwarding is both straightforward and much less work than implementing Attributed
from scratch. Forwarding also enables you to quickly change the implementation you use, should a better implementation of Attributed
become available at some future date. However, forwarding must be set up manually and that can be tedious and sometimes error prone.
Some interfaces do not declare any methods but simply mark a class as having some general property. The Cloneable
interface is such a marker interface—it has neither methods nor constants, but marks a class as partaking in the cloning mechanism (see page 101).
Marker interfaces are the degenerate case of a contract because they define no language-level behavior—no methods or values. All their contract is in the documentation that describes the expectations you must satisfy if your class implements that interface. The interfaces Serializable
and Externalizable
(described in “Object Serialization” on page 549) are marker interfaces, as are both java.rmi.Remote
(see “java.rmi — Remote Method Invocation” on page 727) and java.util.EventListener
(see “java.awt — The Abstract Window Toolkit” on page 717).
Marker interfaces can have a profound impact on the behavior of the classes that implement them—consider Cloneable
. Do not be fooled into thinking that they are unimportant merely because they have no methods.
An interface defines a type with an abstract contract. An abstract class also defines a type with an abstract contract. Which should you use and when?
There are two major differences between interfaces and abstract classes:
Interfaces provide a form of multiple inheritance, because you can implement multiple interfaces. A class can extend only one other class, even if that class has only abstract methods.
An abstract class can have a partial implementation, protected parts, static methods, and so on, whereas interfaces are limited to public constants and public methods with no implementation.
These differences usually direct the choice of which tool is best to use in a particular implementation. If multiple inheritance is important or even useful, interfaces are used. However, an abstract class enables you to provide some or all of the implementation so that it can be inherited easily, rather than by explicit forwarding. Additionally, an abstract class can control the implementation of certain methods by making them final
—for example, our SortDouble
class in Chapter 3 ensures that sorting is done correctly. However, if you find yourself writing an abstract class with all abstract methods, you're really writing an interface.
Any major class you expect to be extended, whether abstract or not, should be an implementation of an interface. Although this approach requires a little more work on your part, it enables a whole category of use that is otherwise precluded. For example, suppose we had created an Attributed
class instead of an Attributed
interface with an AttributedImpl
implementation class. In that case, programmers who wanted to create new classes that extended other existing classes could never use Attributed
, since you can extend only one class—the class AttributedBody
could never have been created. Because Attributed
is an interface, programmers have a choice: They can extend AttributedImpl
directly and avoid the forwarding, or, if they cannot extend, they can at least use composition and forwarding to implement the interface. And if the general implementation provided is incorrect, they can write their own implementation. You can even provide multiple possible implementations of the interface to prospective users. Whatever implementation strategy programmers prefer, the objects they create are Attributed
.
Exercise 4.1: Rewrite your solution to Exercise 3.6 on page 99 using an interface for EnergySource
instead of an abstract class.
Exercise 4.2: Rewrite your solution to Exercise 3.12 on page 114 using an interface if you didn't write it that way in the first place.
Exercise 4.3: Should the LinkedList
class from previous exercises be an interface? Rewrite it that way with an implementation class before you decide.
Exercise 4.4: Design a collection class hierarchy using only interfaces.
Exercise 4.5: Think about whether the following types should be represented as interfaces, abstract classes, or concrete classes: (a) TreeNode
to represent nodes in an N-ary tree; (b) TreeWalker
to walk the tree in a particular order (such as depth-first or breadth-first); (c) Drawable
for objects that can be drawn by a graphics system; (d) Application
for programs that can be run from a graphical desktop.
Exercise 4.6: What changes in your assumptions about each of the problems in Exercise 4.5 would make you change your answers?
There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies; the other is to make it so complicated that there are no obvious deficiencies. | ||
--C.A.R. Hoare |
18.117.99.152