Sponsored Link •
|
Advertisement
|
There are two good reasons to learn the meaning of polymorphism. First, using such a fancy word in casual conversation makes you sound intelligent. Second, polymorphism provides one of the most useful programming techniques of the object-oriented paradigm. Polymorphism, which etymologically means "many forms," is the ability to treat an object of any subclass of a base class as if it were an object of the base class. A base class has, therefore, many forms: the base class itself, and any of its subclasses.
If you need to write code that deals with a family of types, the code can ignore type-specific details and just interact with the base type of the family. Even though the code thinks it is sending messages to an object of the base class, the object's class could actually be the base class or any one of its subclasses. This makes your code easier for you to write and easier for others to understand. It also makes your code extensible, because other subclasses could be added later to the family of types, and objects of those new subclasses would also work with the existing code.
To see how to use polymorphism in a Java program, consider the family of types shown in Figure 8-1.
To use an object of type Liquid
, you must create a Liquid
object
with new
and store the returned reference in a variable:
Liquid myFavoriteBeverage = new Liquid();The
myFavoriteBeverage
variable holds a reference to a
Liquid
object. This is a sensible arrangement; however, there is another possibility
brought to you courtesy of polymorphism. Because of polymorphism, you can assign a reference to any
object that is-a Liquid
to a variable of type Liquid
.
So, assuming the inheritance hierarchy shown in Figure 8-1, either of the following assignments will also
work:
Liquid myFavoriteBeverage = new Coffee(); // or... Liquid myFavoriteBeverage = new Milk();Therefore, you can sprinkle some polymorphism in your Java program simply by using a variable with a base type to hold a reference to an object of a derived type.
Figure 8-1. The Liquid Family
To get the full benefit of polymorphism in your programs, however, you'll need to go further. To fully realize the wonders of polymorphism, you must send a message to an object without knowing the actual class of the object. To do this in Java, you just invoke a method defined in a base type on an object referenced by a variable of the base type. As you saw above, the object referred to by a base class reference might be of the base class or any of its subclasses. Therefore, when you write the code to invoke the method, you don't necessarily know the actual class of the object. Likewise, when you compile the code, the compiler doesn't necessarily know the actual class of the object. At run-time, the Java Virtual Machine determines the actual class of the object each time the method invocation is requested by your program. Based on this information, the Java Virtual Machine invokes the method implementation belonging to the object's actual class. Letting the Java Virtual Machine determine which method implementation to invoke, based on the actual class of the object, is how you realize the full power of polymorphism in your programs.
As an example, consider the family of types for Liquid
shown in Figure 8-1.
Assume the base class, Liquid
, defines a method swirl()
which takes a boolean
parameter clockwise
. When
swirl()
is invoked on a Liquid
object, the object simulates a
swirling motion. If the clockwise
parameter is true
, the
Liquid
object swirls clockwise. Otherwise, the Liquid
object
swirls counterclockwise. Assume also that the subclasses Coffee
and
Milk
override the default implementation of swirl()
to account
for the unique viscosity of those specific kinds of Liquid
. Milk
,
for instance, might swirl more slowly because it is thicker. This arrangement can be expressed in Java
code as follows:
// NEED TO ADD add() TO Liquid // In Source Packet in file inherit/ex2/Liquid.java class Liquid { void swirl(boolean clockwise) { // Implement the default swirling behavior for liquids System.out.println("Swirling Liquid"); } } // In Source Packet in file inherit/ex2/Coffee.java class Coffee extends Liquid { void swirl(boolean clockwise) { // Simulate the peculiar swirling behavior exhibited // by Coffee System.out.println("Swirling Coffee"); } } // In Source Packet in file inherit/ex2/Milk.java class Milk extends Liquid { void swirl(boolean clockwise) { // Model milk's manner of swirling System.out.println("Swirling Milk"); } }
To carry the example further, take another look at the addLiquid()
method of
the Cup
family of types shown in Figure 8-1. Assume class Cup
defines an addLiquid()
method that you have overridden in class
CoffeeCup
, much like the example on the right hand side of Figure 7-2. In the right
hand version of Figure 7-2, class CoffeeCup
overrides
addLiquid()
so that the liquid can be made to swirl counterclockwise as it is added
to the cup. Now that you have a Liquid
class with an actual
swirl()
method, you could implement the addLiquid()
method in CoffeeCup
as follows:
// In Source Packet in file inherit/ex2/CoffeeCup.java class CoffeeCup { private Liquid innerLiquid; void addLiquid(Liquid liq) { innerLiquid.add(liq); // Swirl counterclockwise innerLiquid.swirl(false); } }
Given the above definition of CoffeeCup
, you can reap the benefit of
polymorphism by invoking addLiquid()
with different kinds of liquid:
// In Source Packet in file inherit/ex2/Example2.java class Example2 { public static void main(String[] args) { // First you need a coffee cup CoffeeCup myCup = new CoffeeCup(); // Next you need various kinds of liquid Liquid genericLiquid = new Liquid(); Coffee coffee = new Coffee(); Milk milk = new Milk(); // Now you can add the different liquids to the cup myCup.addLiquid(genericLiquid); myCup.addLiquid(coffee); myCup.addLiquid(milk); } }
Note that the definition of addLiquid()
treats the object passed in as a
parameter as if it is of type Liquid
, yet the code above passes types
Coffee
and Milk
as well as Liquid
. The
code of the addLiquid()
method, therefore, doesn't know the exact class of the
object it is being passed. When swirl()
is invoked on the object at run-time, the
implementation of swirl()
that gets executed depends upon the actual class of the
object. In the first case, when genericLiquid
is passed,
Liquid
's implementation of swirl()
will be executed. When
coffee
is passed, Coffee
's implementation of
swirl()
will be executed. In the last case, when milk
is passed,
Milk
's implementation of swirl()
will be executed.
Therefore, when milk is added to the cup, it will swirl like milk. When coffee is added, it will swirl
like coffee. All this is accomplished even though the code of the addLiquid()
method does not know the actual class of the object passed. This is the beauty of polymorphism.
Had you not known about polymorphism, you might have designed the Liquid
family and the addLiquid()
method differently. Instead of taking advantage of
polymorphism's ability to figure out which method to call, you could have used a brute force method:
// In Source Packet in file inherit/ex3/UglyCoffeeCup.java // This version doesn't take advantage of polymorphism. class UglyCoffeeCup { Liquid innerLiquid; void addLiquid(Liquid liq) { innerLiquid = liq; if (liq instanceof Milk) { ((Milk) innerLiquid).swirlLikeMilk(false); } else if (liq instanceof Coffee) { ((Coffee) innerLiquid).swirlLikeCoffee(false); } else { innerLiquid.swirlLikeGenericLiquid(false); } } }
The creation of if-else
constructs like the one shown above are possible in Java
because of the instanceof
operator, which allows you to check whether an object is
an instance of a certain class. Here the instanceof
operator is being abused, and
the code should be reorganized to use polymorphism. The instanceof
operator, and
the situations in which it should be used, will be discussed later in this chapter.
The above code is more difficult to read and less extensible than the code (shown earlier) that took full
advantage of polymorphism. If later you added a new type to the Liquid
family, say
Tea
, you would have to add another if-else
statement to the
above code. You would not, however, need to make any changes to the earlier
addLiquid()
implementation that took advantage of polymorphism. That
implementation of addLiquid()
would still work, even though you wrote it before
you knew about Tea
. Whenever you find yourself writing a series of if-
else
statements, where the condition is a test of type, you should try and see if your program
can't be redesigned to take advantage of polymorphism.
Programming down the path of polymorphism implies that you write code that lets an object figure
out how it should behave. In the previous example, you told an object to swirl, and expected it to swirl in
the manner objects of its class are supposed to swirl. You didn't tell it explicitly whether it should swirl
like milk or coffee, you just told it to swirl. If the object was actually milk, you expected it to behave like
milk and swirl in a milky way. This attitude towards objects fits well with the object-oriented mindset,
because the customary mission of a method is to manipulate the internal data of the object. Objects are
supposed to know best how to manipulate their own data, which is the reason data is usually
private
. Keeping data private
gives full responsibility for
proper manipulation of the internal state of an object to the object's methods. Polymorphism gives you
another way in which you can give objects responsibility for their own behavior--the object's behavior
matches its class.
The underlying mechanism that makes polymorphism possible is dynamic binding.
Except for three special cases, all instance methods in Java programs are dynamically bound. The instance
method that is invoked at run-time will be determined by the actual class of the object, not by the type of
the reference. This differs from C++, in which you must declare an instance method
virtual
to get polymorphism and dynamic binding. If you don't declare an instance
method virtual
in a C++ class, you get static binding, in which the
method called is determined by the type of the reference, not the class of the object referred to by the
reference. Polymorphism plays no role in static binding. In C++, therefore, a programmer can, wielding
the power of the virtual
keyword, create instance methods that are either statically
or dynamically bound. In Java, however, "virtual
" is not a keyword, and all
instance methods, except for the three special cases, are dynamically bound. The three special cases are
private methods, instance method invocations with the super
keyword, and
invocation of instance initialization methods. These cases will be described further later in this chapter.
Static binding also plays a role in class methods (those declared with the static
modifier), which are always statically bound. You can redefine a class method in a subclass, just like an
instance method; however, a redefined class method will not participate in the rewards of polymorphism
and dynamic binding. Because class methods do not operate on specific objects, you don't even need an
object to call them. Class methods can be invoked even when no instances of the class exist.
Polymorphism requires an object, because it enables a method to be dynamically selected based on the
actual class of the object. Thus, polymorphism does not apply to class methods. If you invoke a class
method on an object, the implementation of the class method is chosen based not on the class of the object
at run-time, but on the type of the object reference at compile-time.
For example, imagine you want to be able to simulate an earth tremor in your cafe, by sending a
message to all liquids in the cafe, telling the liquids to gurgle. The clientele of the cafe will know
something is happening when, as a result of the pan-liquid gurgle command, they see their coffees and
teas producing bubbles and generally sloshing about. One way you could model this in your Java program
is by declaring a static
method, gurgle()
, in the
Liquid
class:
// In Source Packet in file inherit/ex4/Liquid.java class Liquid { void swirl(boolean clockwise) { System.out.println("One Liquid object is swirling."); } static void gurgle() { System.out.println("All Liquid objects are gurgling."); } }
Suppose you also wish to model an unusual kind of earth tremor that affects only milk, but not any
other type of liquid. Perhaps the frequency of the tremor matches exactly the resonant frequency of milk,
so milk is the only liquid visibly affected. In this case you want to be able to send the gurgle command to
all milks, but not to any other liquids in the cafe. You could model this in your program by adding a
gurgle()
method to the Milk
class:
// In Source Packet in file inherit/ex4/Milk.java class Milk extends Liquid { void swirl(boolean clockwise) { System.out.println("One Milk object is swirling."); } static void gurgle() { System.out.println("All Milk objects are gurgling."); } }
Armed with the implementations of Liquid
and Milk
shown
above, you are ready to have some fun. Assume you wish to start by simulating an earth tremor that
gurgles milk, but leaves all other liquids alone. Given your pleasant experience of polymorphism with
instance methods, you might try to accomplish the milk gurgling by code similar to the following:
// In Source Packet in file inherit/ex4/Example4a.java class Example4a { public static void main(String[] args) { Liquid liq = new Milk(); liq.swirl(true); liq.gurgle(); } }Unfortunately, the above code will generate the following output:
One Milk object is swirling. All Liquid objects are gurgling.
The output generated by the above code demonstrates the statically linked nature of class method
invocations. In this code you have a reference of type Liquid
, but an object of class
Milk
. When you invoked swirl()
on the object, polymorphism
came through for you, because Milk
's implementation of
swirl()
was executed. When you invoked gurgle()
, however,
polymorphism abandoned you, and Liquid
's implementation of
gurgle()
was executed. The implementation of gurgle()
to
invoke was determined statically at compile-time based on the reference's type
(Liquid
), not dynamically at run-time based on the object's class
(Milk
).
One way to solve the problem is to make sure you invoke gurgle()
on a
Milk
object being referred to by a reference of type Milk
, as in:
// In Source Packet in file inherit/ex4/Example4b.java class Example4b { public static void main(String[] args) { Milk m = new Milk(); m.swirl(true); m.gurgle(); } }This code generates the desired effect. One
Milk
object is swirled, and all
Milk
objects are gurgled. The output generated by the above code is:
One Milk object is swirling. All Milk objects are gurgling.Yet even though you have gotten the results you want, you still haven't written code that clearly indicates the class-wide nature of
gurgle()
. Most of the time, the best way to invoke a class
method is to use the class name, not a reference to an object of the class. For example, you could rewrite
the above code:
// In Source Packet in file inherit/ex4/Example4c.java class Example4c { public static void main(String[] args) { Milk m = new Milk(); m.swirl(true); Milk.gurgle(); } }The line
Milk.gurgle()
more clearly indicates that a class method is
being invoked, and that polymorphism is not involved.
Redefining an instance method in a subclass is called "overriding" the instance method, however,
redefining a class method is not called "overriding." Instead, it is called hiding the class
method. The term "override" is associated with polymorphism, which doesn't apply to class methods.
Therefore, you can't override a class method, you can only hide it. For example, the
gurgle()
method defined in Milk
above hides the
gurgle()
implementation defined in Liquid
.
Because an invocation of a statically bound class method on an object looks similar to the invocation
of a dynamically bound instance method, you must be careful to always keep in mind the difference.
Invoking class methods using the class name, as in Milk.gurgle()
, instead of an
object reference, as in m.gurgle()
, is a good rule of thumb to help clarify your
code.
this
ReferencePoint out using this
for this.attr = attr
, and mention
this is a kind of hiding. Talk about hiding in general, and make another request for private data.
super
ReferenceHere talk about this, super, and ((SuperClass) var).memberName. Yes. This is where I can mention
the static binding of fields, because I show it with the (()) example. Whereas you could invoke a virtual
method in a superclass in C++ by using the scope resolution operator, in Java you can't access any
instance method that the current class overrides in any superclass other than your direct superclass. You
can do it with a field, whether its an instance or class variable, and with a class method simply by casting
the this
reference to a superclass type.
Any field you declare private can't be hidden. If you favor private data in your class designs, field hiding should be rare. You may occasionally encounter hiding with constant fields, which are often declared public. You may also encounter field hiding when you use libraries that declare public fields.
One other justification for keeping data private is that fields are accessed not based on the class of the object, but on the type of the reference. If a subclass hides a public instance variable of its superclass, then
[YIKES, DON'T I NEED TO TALK ABOUT STUFF FROM PAGE 61 OF JPL BOOK? FIELDS ARE ACCESSED NOT BASED ON THE CLASS OF OBJECT, BUT THE TYPE OF THE REFERENCE.]
The techniques discussed so far yield families of types in which all classes in the family have the
precisely same interface. A subclass can differentiate itself from all the other classes in the family by
overriding methods inherited from its direct superclass. In this very object-oriented scheme, a class that
belongs to a family of types expresses its uniqueness not by the interface it presents to the world, but by
the implementation underneath the interface. All the classes in a family might have a
swirl()
method, for example, but each individual class might swirl in its own
unique way. This manner of modeling families of types is very expressive and allows you to take
advantage of polymorphism, but can sometimes restrict your ability to model the specific nature of a
subclass. Sometimes you may want a subclass to accept messages that its superclass does not accept. To do
this you must extend the inherited interface. You must add to the subclass new methods that did not exist
in the superclass.
Java allows you to define methods that enable a subclass to receive messages that would normally be accepted only by the subclass, and not by a more general superclass. This muddies the object-oriented metaphor a bit, because even though you can still substitute a subclass wherever a superclass is required, the new methods you added to the subclass aren't accessible when you are treating the subclass as if it is a superclass. For this reason, you will usually want to first attempt to design families of types in which subclasses contain only methods that override methods inherited from superclasses. Sometimes, however, you will feel the need to add new methods to subclasses. In those situations you must just live with the inability to invoke one of the new methods when you have a reference to an object of the base type. You will only be able to invoke the new methods when you have an explicit reference to the subclass type in which the new methods are defined.
As an example, consider again the family of liquids. Up to this point you have been introduced to four
members of the liquid family, Liquid
, the base class, and subclass siblings
Coffee
, Milk
, and Tea
. Each of the
subclasses overrides the default implementation of swirl()
, defined in base class
Liquid
. So far every class in the family has the same interface, which is composed of
just two methods: swirl()
and gurgle()
. Now suppose you
want to be able to invoke a method on an object of class Tea
that causes the object to
inspect itself, and from the configuration of tea leaves floating in itself, describe the future of the person
who is drinking the tea.
The ability to predict a person's future from the tea leaves left after they drink a cup of tea is not a
general property of liquids. It is a property only of tea. If you added a
readFuture()
method to base class Liquid
, that would imply
that one can see the future by peering into any liquid. But this is not true of coffee. It is not true of milk.
(It is probably not true of tea either, but for the sake of this illustration, assume it is.) Therefore, the best
way to model this in your design is to add a readFuture()
method to the
Tea
class only:
// In Source Packet in file inherit/ex5/Liquid.java class Liquid { void swirl(boolean clockwise) { System.out.println("Liquid Swirling"); } } // In Source Packet in file inherit/ex5/Tea.java class Tea extends Liquid { void swirl(boolean clockwise) { System.out.println("Tea Swirling"); } void readFuture() { System.out.println("Reading the future..."); } } // In Source Packet in file inherit/ex5/Coffee.java class Coffee extends Liquid { void swirl(boolean clockwise) { System.out.println("Coffee Swirling"); } } // In Source Packet in file inherit/ex5/Milk.java class Milk extends Liquid { void swirl(boolean clockwise) { System.out.println("Milk Swirling"); } }
Given the design represented by the code above, you will not be able to call
readFuture()
if you have a reference to an object of type
Liquid
, even if the actual object being referenced is type Tea
. As
demonstrated below, only if you have a reference of type Tea
can you invoke the
readFuture()
method:
// In Source Packet in file inherit/ex5/Example5a.java class Example5a { public static void main(String[] args) { // Create a Tea reference and a Tea object Tea tea = new Tea(); // Create a Liquid reference and, in the spirit of // polymorphism, assign to it the same Tea object Liquid liq = tea; // Ask the tea object to read the future of its drinker tea.readFuture(); // Attempt to ask the same tea object to read the future // again, but this time via the reference to Liquid. liq.readFuture(); // THIS WON'T COMPILE. } }
The example above demonstrates the consequence of adding new methods to subclasses. The new methods can be called only when the type of the reference is the subclass. In this situation, however, it is a reasonable way to model the fortune telling behavior of tea, and the consequences of the design are acceptable.
instanceof
The fortune-telling behavior of Tea
illustrates a situation in which you might want
to use instanceof
. If you have a reference to a Liquid
and you
want to swirl the liquid clockwise, you can use polymorphism because all liquids swirl:
// In Source Packet in file inherit/ex5/Example5b.java class Example5b { public static void doSomethingWithALiquid(Liquid liq) { liq.swirl(true); } public static void main(String[] args) { // Create a Tea reference and a Tea object Tea tea = new Tea(); doSomethingWithALiquid(tea); } }If you also want, if the liquid actually is tea, to read the drinker's future, you can't use polymorphism, because not all liquids can predict the future:
// In Source Packet in file inherit/ex5/Example5c.java class Example5c { public static void doSomethingWithALiquid(Liquid liq) { liq.swirl(true); liq.readFuture(); // THIS WON'T COMPILE } public static void main(String[] args) { // Create a Tea reference and a Tea object Tea tea = new Tea(); doSomethingWithALiquid(tea); } }In this case, you must use
instanceof
to determine whether the object really is tea,
and if so, downcast the reference to type Tea
, and invoke
readFuture()
on that:
// In Source Packet in file inherit/ex5/Example5d.java class Example5d { public static void doSomethingWithALiquid(Liquid liq) { liq.swirl(true); if (liq instanceof Tea) { Tea tea = (Tea) liq; tea.readFuture(); } } public static void main(String[] args) { // Create a Tea reference and a Tea object Tea tea = new Tea(); doSomethingWithALiquid(tea); } }The process of converting the
Liquid
reference into a Tea
reference is called "downcasting" because you are casting the reference "down" the inheritance hierarchy,
from Liquid
to Tea
. This illustrates the kind of situation in
which you should use instanceof
. You have a base type reference, and if the object
referred to by the base type reference really is a certain subclass, you want to invoke a method that only
exists in that subclass.
Incidentally, Java ensures type-safety at run-time. If, for example, your program attempts at run-time
to downcast to Tea
a Liquid
reference that actually refers to a
Milk
object, the Java Virtual Machine will throw a
ClassCastException
. Each time a cast is performed, the actual class of the object
is checked to make sure the cast is valid.
The three special cases, mentioned above, in which Java performs static binding on instance methods
are:
o private methods
o methods invoked with the super
keyword
o instance initialization methods
When you invoke a private method from another method, both methods must be defined in the same class. Although a method of the same signature as a private method can be declared in a subclass, a private method can't be overridden by a subclass. When you invoke a private method, the Java compiler knows precisely which class contains the method to invoke, because it must by definition be in the same class. Static binding is used so that the private method is invoked independent of the actual class of the object at run-time.
The super
keyword, which will be described in detail later in this chapter, allows
you to access a superclass's methods and fields from a subclass, even if they are overridden or hidden in
the subclass. In the case of instance methods, static binding must be used. If a method is overridden in a
subclass, dynamic binding would cause the subclass's version of the method to be invoked rather than the
superclass's version. As with invocation of a private method, the compiler knows precisely which class
contains the method to invoke when it is invoked with the super
keyword. Static
binding allows a superclass's version of an instance method to be invoked independent of the actual class
of the object at run-time.
The Java compiler creates one instance initialization method for each constructor in the
source for a class. This special kind of instance method is invoked only when an object is created. Like
private methods and methods invoked with super
, instance initialization methods are
invoked using static binding. The details of this special kind of method will be described in Chapter 13.
For more information on static binding of instance methods, see the explanation of the invokespecial instruction in Chapter 25.
As illustrated in the previous chapter, one of the most important benefits of class extension is that you
can take advantage of polymorphism. In an inheritance hierarchy, if class
CoffeeCup
extends class Cup
, you can treat a
CoffeeCup
object as if it were a Cup
object. Sometimes,
however, it is difficult to get the polymorphism you want from the singly-inherited hierarchies you can
build with class extension. To help you get more polymorphism than you can easily get with single-
inheritance, Java supports a restricted form of multiple inheritance through a construct called the
"interface." This chapter will discuss the motivation and the mechanics of the Java interface.
To reap the benefits of polymorphism through class extension, you must build a family of
classes. In Java terminology, both classes and interfaces are "types." When you declare an
interface, as when you declare a class, you establish a new type. In the remainder of this book, "type" will
be used to refer to either classes or interfaces. Here, a "family of classes" is simply a family of types in
which all the types are classes (none are interfaces). Thus, a family of classes is a group of related classes
with a single base class from which all other classes in the family descend. Since every class in Java
descends from Object
, all Java classes are members of the
Object
family; however, you can still look at individual areas of an inheritance
hierarchy as individual "families of classes." For example, class Cup
and all its
subclasses, as shown in Figure 4-1, form the Cup
family.
[bv: I believe I covered this already in a previous chapter.]
Figure 4-1. The Cup
family
Given a family of classes, polymorphism allows you to treat a subclass object as if it were a superclass
object. For example, imagine you wanted to create a single method that could wash any kind of cup in
your virtual cafe. You could declare a public method named wash()
in the base class
of the Cup
family:
// In Source Packet in file interface/ex1/Cup.java class Cup { public void wash() { System.out.println("Washing a Cup."); // ... } //... } // In Source Packet in file interface/ex1/CoffeeCup.java class CoffeeCup extends Cup { public void wash() { System.out.println("Washing a CoffeeCup."); // ... } //... } // In Source Packet in file interface/ex1/CoffeeMug.java class CoffeeMug extends CoffeeCup { public void wash() { System.out.println("Washing a CoffeeMug."); // ... } //... } // In Source Packet in file interface/ex1/EspressoCup.java class EspressoCup extends CoffeeCup { public void wash() { System.out.println("Washing an EspressoCup."); // ... } //... }
Given this family of types, you could define a method that takes a Cup
reference as
follows:
// In Source Packet in file interface/ex1/VirtualCafe.java class VirtualCafe { public static void prepareACup(Cup cup) { //... cup.wash(); //... } //... }
Using polymorphism, you could pass to the method a reference to any object that is-a
Cup
:
// In Source Packet in file interface/ex1/Example1.java class Example1 { public static void main(String[] args) { Cup c = new Cup(); CoffeeCup cc = new CoffeeCup(); CoffeeMug cm = new CoffeeMug(); EspressoCup ec = new EspressoCup(); VirtualCafe.prepareACup(c); VirtualCafe.prepareACup(cc); VirtualCafe.prepareACup(cm); VirtualCafe.prepareACup(ec); } }
Here you have all the benefits of polymorphism. The prepareACup()
method
can invoke wash()
on many different objects, but it doesn't need to use
instanceof
. As a consequence, the code is easier to read and change. If later, you
wanted to add class TeaCup
to your program and wash TeaCup
objects with prepareACup()
, you only need to make TeaCup
a
subclass of Cup
. You don't need to change the prepareACup()
method itself.
This all works fine, but what if you wanted to wash a greater variety of objects with a single method?
What if you wanted to have a method that can wash any kind of object for which washing makes sense--
any "washable" object? For example, besides washing cups, you might also want to wash a window, wash
your car, or wash a dog. Since these objects don't seem to fit into the same family, you might end up using
instanceof
instead of polymorphism. For example, consider these classes:
// In Source Packet in file interface/ex2/Window.java class Window { public void wash() { System.out.println("Washing a Window."); // ... } //... } // In Source Packet in file interface/ex2/Cup.java class Cup { public void wash() { System.out.println("Washing a Cup."); // ... } //... } // In Source Packet in file interface/ex2/CoffeeCup.java class CoffeeCup extends Cup { public void wash() { System.out.println("Washing a CoffeeCup."); // ... } //... } // In Source Packet in file interface/ex2/CoffeeMug.java class CoffeeMug extends CoffeeCup { public void wash() { System.out.println("Washing a CoffeeMug."); // ... } //... } // In Source Packet in file interface/ex2/EspressoCup.java class EspressoCup extends CoffeeCup { public void wash() { System.out.println("Washing an EspressoCup."); // ... } //... } // In Source Packet in file interface/ex2/Car.java class Car { public void wash() { System.out.println("Washing a Car."); // ... } //... } // In Source Packet in file interface/ex2/Dog.java class Dog { public void wash() { System.out.println("Washing a Dog."); // ... } //... }
Here, instead of having one family of classes for washable objects, you have four separate families:
cups, dogs, cars, and windows. Each washable object in each family declares wash()
,
but because there is no common base class that declares wash()
, you can't use
polymorphism. To create a method that can wash any of these kinds of objects, your method would have to
use instanceof
:
// In Source Packet in file interface/ex2/Cleaner.java class Cleaner { // (This doesn't use polymorphism) public static void cleanAnObject(Object obj) { // Perform any necessary processing of the // object before washing... // Wash the object if (obj instanceof Cup) { // (Here you are using polymorphism, but just // within the Cup family.) ((Cup) obj).wash(); } else if (obj instanceof Dog) { ((Dog) obj).wash(); } else if (obj instanceof Window) { ((Window) obj).wash(); } else if (obj instanceof Car) { ((Car) obj).wash(); } // Else the object doesn't get washed // Perform other processing on the object to // complete the cleaning process... } }
This cleanAnObject()
method will work, but it doesn't participate in the
benefits of polymorphism. Most significantly, this code is less flexible than if it were able to take
advantage of polymorphism. With the above code, you'd have to add another
instanceof
check if you want to add another kind of washable object, say
Bicycle
, to your program.
To improve this situation, you might decide to give cups, cars, windows, and dogs a common base
class that declares the wash()
method. This would allow you to get the full benefit of
polymorphism in the cleanAnObject()
method. Here, the four families--cups,
cars, windows, and dogs--are combined into the WashableObject
family:
// In Source Packet in file interface/ex3/WashableObject.java abstract class WashableObject { public abstract void wash(); } // In Source Packet in file interface/ex3/Window.java class Window extends WashableObject { public void wash() { System.out.println("Washing a Window."); // ... } //... } // In Source Packet in file interface/ex3/Cup.java class Cup extends WashableObject { public void wash() { System.out.println("Washing a Cup."); // ... } //... } // In Source Packet in file interface/ex3/CoffeeCup.java class CoffeeCup extends Cup { public void wash() { System.out.println("Washing a CoffeeCup."); // ... } //... } // In Source Packet in file interface/ex3/CoffeeMug.java class CoffeeMug extends CoffeeCup { public void wash() { System.out.println("Washing a CoffeeMug."); // ... } //... } // In Source Packet in file interface/ex3/EspressoCup.java class EspressoCup extends CoffeeCup { public void wash() { System.out.println("Washing an EspressoCup."); // ... } //... } // In Source Packet in file interface/ex3/Car.java class Car extends WashableObject { public void wash() { System.out.println("Washing a Car."); //... } //... } // In Source Packet in file interface/ex3/Dog.java class Dog extends WashableObject { public void wash() { System.out.println("Washing a Dog."); //... } //... }
Given this WashableObject
family, which is shown graphically in Figure 4-2,
you can create a single method that uses polymorphism to wash any kind of washable object:
// In Source Packet in file interface/ex3/Cleaner.java class Cleaner { public static void cleanAnObject(WashableObject wo) { //... wo.wash(); //... } }
Figure 4-2. The WashableObject
family
As this example demonstrates, it is possible to fit cups, windows, cars, and dogs all into the same
family of classes; however, the resulting family, WashableObject
, is not very
intuitive and not very flexible.
As an example of this family's inflexibility,
imagine that later you decide you want Dog
to descend from
Animal
. You would have to make Animal
descend from
WashableObject
. But what if, as is shown in Figure 4-4, you declared
Cat
and Fish
as subclasses of Animal
too? Is
a Cat
washable? Potentially, but you'd best let the Cat
take care of
that itself. And how do you wash a Fish
? Although each of these washings are
possible to imagine, they may not be a behavior you intend a Cat
or
Fish
to exhibit. You are making Cat
s and
Fish
endure washing, when all you really want to do is keep the
Dog
clean. Given that you want Dog
s to both to descend from
Animal
and be washable, however, you need to either make every
Animal
a WashableObject
or every
WashableObject
an Animal
. Here is the code for the
Animal
family:
// In Source Packet in file interface/ex5/WashableObject.java abstract class WashableObject { public abstract void wash(); } // In Source Packet in file interface/ex5/Animal.java class Animal extends WashableObject { public void wash() { System.out.println("Washing an Animal."); //... } //... } // In Source Packet in file interface/ex5/Dog.java class Dog extends Animal { public void wash() { System.out.println("Washing a Dog."); //... } //... } // In Source Packet in file interface/ex5/Cat.java class Cat extends Animal { public void wash() { System.out.println("Washing a Cat."); //... } //... } // In Source Packet in file interface/ex5/Fish.java class Fish extends Animal { public void wash() { System.out.println("Washing a Fish."); //... } //... }
Figure 4-4. Nervous cats and puzzled fish
The problem here is that you are using class extension not to model specialization of objects in
the problem domain, but simply to get at polymorphism. In your problem domain, is it true that a
Cup
is-a WashableObject
? What exactly is a washable object?
What does one look like? Washable is an adjective, not a noun. It describes a behavior exhibited by
objects, not an object itself. To get the benefits of polymorphism, you insert
WashableObject
into the inheritance hierarchy, but it doesn't fit very well.
Although class extension only allows single inheritance (each class can have at most one direct
superclass), Java offers a special variation of multiple inheritance through the "interface." An interface is
like an abstract class that has only public
abstract
methods
and public
static
final
fields. An
interface (the Java construct) represents a pure interface (the object-oriented concept); it has no
implementation.
Interfaces in Java allow you to get the benefits of polymorphism without requiring you to build a
singly-inherited family of classes. Although a class can extend only one other class, it can "implement"
multiple interfaces. Interfaces allow you to use families of classes to model what objects are (such as cups
or animals) rather than what you plan to do with them (such as wash them). You can design a family of
classes for cups, another for animals (including dogs), one for vehicles (including cars), one for parts of a
building (including windows). Then each washable class can implement the
Washable
interface.
Here is a potential declaration of the interface:
// In Source Packet in file interface/ex6/Washable.java interface Washable { int IMPERVIOUS = 0; int RESISTENT = 1; int FRAGILE = 2; int EXPLOSIVE = 3; /** * returns true if the object needs to be washed */ boolean needsWashing(); /** * washes the object */ void wash(); }
The methods declared in this interface are not explicitly declared public
and
abstract
, because they are public
and
abstract
by default. Likewise, the constants
in Washable
are
not declared public
, static
, and final
,
because they are so by default.
A class Cup
could implement the Washable
interface as follows:
// In Source Packet in file interface/ex6/Cup.java class Cup extends Object implements Washable { public int getLevelOfFragility() { return Washable.FRAGILE; } public boolean needsWashing() { // No implementation yet... // hard-code a return value so it will compile return true; } public void wash() { System.out.println("Washing a Cup."); //... } //... } // In Source Packet in file interface/ex6/CoffeeCup.java class CoffeeCup extends Cup { public void wash() { System.out.println("Washing a CoffeeCup."); //... } //... }
Class Cup
declares that it implements interface
Washable
, so it must implement each method contained in that
interface. If
it doesn't, it must declare itself as abstract. (If class Cup
didn't implement the
methods contained in the interfaces and didn't declare itself abstract, it
wouldn't compile.) In this case, it
implements all methods declared in Washable
.
Class CoffeeCup
, which extends class Cup
, can either inherit or
override Cup
's implementation of the methods defined in
Washable
and Breakable
. Figure 4-5 shows an inheritance
hierarchy for this version of class Cup
and CoffeeCup
.
[bv: Explain the UML diagram for interfaces.]
Note that interfaces do not descend from class Object
.
Figure 4-5. An inheritance hierarchy that includes interfaces
Although interfaces (like abstract classes) cannot be instantiated by themselves, you can create a variable to hold a reference to an interface type:
// In Source Packet in file interface/ex6/Example6a.java class Example6a { public static void main(String[] args) { // OK to declare a variable as an interface type Washable wa; // Can't instantiate an interface by itself. wa = new Washable(); // THIS WON'T COMPILE } }Given an object variable of an interface type (such as
Washable
wa
), you can assign a reference to an object of a class that implements the interface
(such as class CoffeeCup
):
// In Source Packet in file interface/ex6/Example6b.java class Example6b { public static void main(String[] args) { Washable wa = new CoffeeCup(); wa.wash(); } }
Thus, you can upcast a CoffeeCup
reference not only to a
Cup
or an Object
reference, but also to a
Washable
reference as well. On an interface
reference such as wa
above, you can invoke any method declared in the interface, as
wash()
was in this example.
Given the Washable
interface, you could
overcome the difficulties encountered earlier in writing
cleanAnObject()
.
Here is how you could declare the classes and the interface:
// In Source Packet in file interface/ex7/Washable.java interface Washable { void wash(); } // In Source Packet in file interface/ex7/Window.java class Window implements Washable { public void wash() { System.out.println("Washing a Window."); //... } //... } // In Source Packet in file interface/ex7/Cup.java class Cup implements Washable { public void wash() { System.out.println("Washing a Cup."); // ... } //... } // In Source Packet in file interface/ex7/CoffeeCup.java class CoffeeCup extends Cup { public void wash() { System.out.println("Washing a CoffeeCup."); // ... } //... } // In Source Packet in file interface/ex7/CoffeeMug.java class CoffeeMug extends CoffeeCup { public void wash() { System.out.println("Washing a CoffeeMug."); // ... } //... } // In Source Packet in file interface/ex7/EspressoCup.java class EspressoCup extends CoffeeCup { public void wash() { System.out.println("Washing an EspressoCup."); // ... } //... } // In Source Packet in file interface/ex7/Car.java class Car implements Washable { public void wash() { System.out.println("Washing a Car."); //... } //... } // In Source Packet in file interface/ex7/Animal.java class Animal { //... } // In Source Packet in file interface/ex7/Dog.java class Dog extends Animal implements Washable { public void wash() { System.out.println("Washing a Dog."); //... } //... } // In Source Packet in file interface/ex7/Cat.java class Cat extends Animal { //... } // In Source Packet in file interface/ex7/Fish.java class Fish extends Animal { //... }
The inheritance hierarchy for these classes is shown in Figure 4-6. Given these definitions for cups,
animals, windows, and cars, you could once again get the benefits of
polymorphism when writing method cleanAnObject()
:
// In Source Packet in file interface/ex7/Cleaner.java class Cleaner { public static void cleanAnObject(Washable washMe) { //... washMe.wash(); //... } }
Figure 4-6. The interface solution
Interfaces allow you to get the benefits of polymorphism without requiring that you fit everything into
one singly-inherited family of classes. In the examples above, the Washable
interface defines a
standard way to do washing, and any class can implement it.
Because of interfaces, you can use class extension to model what objects are.
You can use interface implementation to get polymorphism based purely on what an object
does. Any desired polymorphism that class extension doesn't
produce, you can get with interface implementation.
[bv: have one example of this.]
Similar to classes, you can build up inheritance hierarchies of interfaces by using the
extends
keyword, as in:
// In Source Packet in file interface/ex8/Washable.java interface Washable { void wash(); } // In Source Packet in file interface/ex8/Soakable.java interface Soakable extends Washable { void soak(); }
In this example, interface Soakable
extends interface
Washable
. Consequently, Soakable
inherits all the members of
Washable
. A class that implements Soakable
must provide
bodies for all the methods declared in or inherited by Soakable
,
wash()
and soak()
, or be declared abstract. Note that only
interfaces can "extend" other interfaces. Classes can't extend interfaces, they can only implement
interfaces.
Soakable
inherits member wash()
from its
superinterface.
Similar to "subclass" and "superclass," interfaces in an inheritance hierarchy
can be called "subinterface" and "superinterface." To refer to either classes or interfaces, you can say
"subtype" and "supertype." As with subclass and superclass, you can use "direct" to indicate that a type or
interface is a direct descendant or ancestor of another type or interface, as in "direct superinterface" or
"direct subtype."
To extend the previous example further, here are a few more interfaces:
// In Source Packet in file interface/ex8/Scrubable.java interface Scrubable extends Washable { void scrub(); } // In Source Packet in file interface/ex8/BubbleBathable.java interface BubbleBathable extends Soakable, Scrubable { void takeABubbleBath(); }
In this example, Washable
, Soakable
, and
Scrubable
are all superinterfaces of BubbleBathable
. (Note
that BubbleBathable
extends two direct superinterfaces. Just as classes can
implement multiple interfaces, interfaces can extend multiple interfaces.) Classes that implement
BubbleBathable
must therefore provide bodies for methods declared in
Washable
, Soakable
, Scrubable
, and
BubbleBathable
, or be declared abstract. Figure 4-7 shows the inheritance
hierarchy for this family of interfaces.
Figure 4-7. Interfaces can extend other interfaces
instanceof
with InterfacesGiven a reference to an object, you can find out if a particular interface is a superinterface of that
object's class by using instanceof
. For example, the
washIfPossible()
method, shown below, uses instanceof
to determine whether an object is a subtype of the Washable
interface:
// In Source Packet in file interface/ex9/Example9.java class Example9 { public static void washIfPossible(Object o) { if (o instanceof Washable) { // Washable is a superinterface of the // object's class ((Washable) o).wash(); } else { System.out.println("Can't wash this."); } } public static void main(String[] args) { washIfPossible(new Cup()); washIfPossible(new CoffeeCup()); washIfPossible(new CoffeeMug()); washIfPossible(new EspressoCup()); washIfPossible(new Car()); washIfPossible(new Animal()); washIfPossible(new Dog()); washIfPossible(new Cat()); washIfPossible(new Fish()); washIfPossible(new Window()); } }
Alternatively, you can obtain a list of all the interfaces an object's class implements using the
java.lang.Class
object. This will be described in the chapter on Reflections,
Chapter 19. These mechanisms allow you to query an object to find out what methods you can invoke on
it, or "what it can do for you."
Multiple inheritance brings with it the potential for name conflicts. If, for example, the
Soakable
and Scrubable
interfaces both declare a method
named dryOff()
, classes that implement both Soakable
and
Scrubable
would inherit dryOff()
twice. If
dryOff()
has different signatures in both interfaces, then the class would inherit two
overloaded names, and would have to define implementations for both, or else declare itself as abstract:
// In Source Packet in file interface/ex10/Washable.java interface Washable { void wash(); } // In Source Packet in file interface/ex10/Soakable.java interface Soakable extends Washable { void soak(); void dryOff(); } // In Source Packet in file interface/ex10/Scrubable.java interface Scrubable extends Washable { void scrub(); void dryOff(boolean withTowel); } // In Source Packet in file interface/ex10/Cup.java class Cup implements Soakable, Scrubable { // implement Soakable's dryOff() public void dryOff() { // ... } // implement Scrubable's dryOff() public void dryOff(boolean withTowel) { //... } public void wash() { //... } public void soak() { //... } public void scrub() { //... } //... }
On the other hand, if the dryOff()
methods in Soakable
and Scrubable
have the same signature and return value, then
Cup
need only implement one dryOff()
method:
// In Source Packet in file interface/ex11/Washable.java interface Washable { void wash(); } // In Source Packet in file interface/ex11/Soakable.java interface Soakable extends Washable { void soak(); void dryOff(); } // In Source Packet in file interface/ex11/Scrubable.java interface Scrubable extends Washable { void scrub(); void dryOff(); } // In Source Packet in file interface/ex11/Cup.java class Cup implements Soakable, Scrubable { // Implement both Soakable's and Scrubable's // dryOff() method with one method body public void dryOff() { // ... } public void wash() { //... } public void soak() { //... } public void scrub() { //... } //... }If the
dryOff()
methods declared in Soakable
and
Scrubable
have the same signature but different return types, they couldn't be
implemented by the same class.
// In Source Packet in file interface/ex12/Washable.java interface Washable { void wash(); } // In Source Packet in file interface/ex12/Soakable.java interface Soakable extends Washable { void soak(); void dryOff(); } // In Source Packet in file interface/ex12/Scrubable.java interface Scrubable extends Washable { void scrub(); boolean dryOff(); } // In Source Packet in file interface/ex12/Cup.java // THIS WON'T COMPILE, BECAUSE NO CLASS CAN // IMPLEMENT BOTH Soakable AND Scrubable class Cup implements Soakable, Scrubable { //... }
If both Soakable
and Scrubable
declared two constants
with the same name, that in itself would never prevent a class from implementing both interfaces. The
like-named constants can be of different values and even different types. To refer to one of the constants
from with the class, however, you must use the qualified name of the field (the name of the
interface, a dot, and the name of the field):
// In Source Packet in file interface/ex13/Washable.java interface Washable { void wash(); } // In Source Packet in file interface/ex13/Soakable.java interface Soakable extends Washable { int BUBBLE_TOLERANCE = 4; void soak(); void dryOff(); } // In Source Packet in file interface/ex13/Scrubable.java interface Scrubable extends Washable { double BUBBLE_TOLERANCE = 0.001; void scrub(); void dryOff(); } // In Source Packet in file interface/ex13/Cup.java class Cup implements Soakable, Scrubable { // Here, can't just say BUBBLE_TOLERANCE. Must // use the qualified name. public void dryOff() { int tol = Soakable.BUBBLE_TOLERANCE; double doubleBubble = Scrubable.BUBBLE_TOLERANCE; // ... } public void wash() { //... } public void soak() { //... } public void scrub() { //... } //... }
The Java Language Specification's recommended naming conventions for interfaces are either a noun
or noun phrase, if you are using the interface to represent an abstract base class, or an adjective, if you are
using the interface to represent a behavior. (Here, both Washable
and
Breakable
are being used to represent a behavior, so their names are adjectives.
Using an interface to represent an abstract base class will be discussed later in this chapter.) The
capitalization of interface names follows that of classes: the first letter of each word should be upper case,
the rest lower case.
An interface can have several possible implementations, each appropriate for different classes of
objects or different situations. The implementations can vary in the algorithm or data structures used,
yielding, for example, some methods that are faster but use a lot of memory and others that are slower but
use memory more conservatively. Sometimes a method declared in an interface simply has a slightly
different meaning for the various classes that implement the interface. For example, you might wash an
object differently depending upon what the object is. Some ways you could the wash an object are: with
soap, water, and a sponge; with sudsy water and a squeegee; with glass cleaner and a paper towel; or with
a machine. The appropriate way for a class to implement the wash()
method of the
Washable
interface depends upon that class's unique nature or circumstances.
If you are writing a class that has superinterfaces, you must implement all methods defined in the superinterfaces, or declare the class abstract. There are three approaches you can take to implement those methods:
If a class has a unique manner of implementing an interface, it can take the first approach and
implement it directly. For example, if there is a way to wash cups that is unique to cups, class
Cup
could declare a wash()
method that washes in that unique
way. Subclasses of Cup would then have the option of inheriting Cup's implementation (the second
approach) or overriding it:
// In Source Packet in file interface/ex14/Washable.java interface Washable { void wash(); } // In Source Packet in file interface/ex14/Cup.java class Cup implements Washable { // Approach one, implement wash() directly public void wash() { // Sponge off with soap and water. // Rinse thoroughly. } //... } // In Source Packet in file interface/ex14/CoffeeCup.java class CoffeeCup extends Cup { // Approach two, don't explicitly declare a // wash() method. Inherit Cup's implementation // of wash(). //... }
Sometimes a single implementation of an interface may make sense for different objects that aren't in
the same family of classes. As an example, imagine you wanted to be able to add properties to some of
your classes, where a property is a value string indexed by a key string. If a particular property's key
string were "color"
, for example, its value string could be
"blue"
. You want to be able to add properties, remove properties, and lookup a value
given a key. Because this behavior is something you'd like to be able to apply to any kind of object, an
interface is called for:
// In Source Packet in file interface/ex15/Propertied.java interface Propertied { void setProperty(String key, String val); void removeProperty(String key); String getProperty(String key); }
You may wish to add properties to both cups and cars, for example, and use the same mechanisms for
managing the data. Instead of repeating the same implementation of
setProperty()
, removeProperty()
, and
getProperty()
in both the Cup
and Car
classes, you could create a PropertyManager
class that implements the
Propertied
interface:
// In Source Packet in file interface/ex15/PropertyManager.java class PropertyManager implements Propertied { private java.util.Hashtable props = new java.util.Hashtable(); public void setProperty(String key, String val) { props.put(key, val); } public void removeProperty(String key) { props.remove(key); } public String getProperty(String key){ // Returns null if property not found return (String) props.get(key); } }
The PropertyManager
class represents one way to implement the
Propertied
interface. This implementation uses the
Hashtable
class from the java.util
library. (The name
java.util.Hashtable
is Hashtable
's fully qualified
name. The details of fully qualified names are described in the next chapter.) Note that this class
uses composition. Class PropertyManager
has-a Hashtable
.
Cup
and Car
objects could each contain a
PropertyManager
object and then forward calls to their
PropertyManager
. (This is the third approach from the list above.):
// In Source Packet in file interface/ex15/Cup.java class Cup implements Propertied { private PropertyManager propMgr = new PropertyManager(); public void setProperty(String key, String val) { propMgr.setProperty(key, val); } public void removeProperty(String key) { propMgr.removeProperty(key); } public String getProperty(String key){ // Returns null if property not found return (String) propMgr.getProperty(key); } //... } // In Source Packet in file interface/ex15/Car.java class Car implements Propertied { private PropertyManager propMgr = new PropertyManager(); public void setProperty(String key, String val) { propMgr.setProperty(key, val); } public void removeProperty(String key) { propMgr.removeProperty(key); } public String getProperty(String key){ // Returns null if property not found return (String) propMgr.getProperty(key); } //... }
Any other classes that you later decide could use a Hashtable
for implementing
the Propertied
interface could contain a PropertyManager
object and forward calls to it. If you later encounter classes for which it doesn't make sense to use a
Hashtable
, you could write another class, perhaps
LinkedListProperyManager
, that implements
Propertied
in a different way. Those classes for which a
Hashtable
doesn't make sense could contain a
LinkedListProperyManager
object and forward calls to it.
As mentioned before, the Java Language Specification suggests two conventions for naming
interfaces. If the interface represents a behavior, its name should be an adjective. The interfaces given so
far as examples in this chapter, Washable
, Breakable
, and
Propertied
fall into this category. They represent pure behavior, and their names
are adjectives. The other suggested naming convention is for interfaces that serve as abstract base classes.
In this case, interfaces should be given names that are nouns or noun phrases, just like classes.
As described in the previous chapter, you can declare classes abstract. If you have a class that is conceptual only--not one that represents actual objects, but one that represents a category of types--you should declare that class abstract. An abstract class cannot be instantiated. Instead of serving as a blueprint for instantiating objects, an abstract class serves as a base class in a family of types.
For example, you could decide that in your virtual cafe, you have coffee cups and tea cups. In your
inheritance hierarchy, you could define an abstract class Cup
that serves as a base class
for both CoffeeCup
and TeaCup
. The abstract
Cup
class could define abstract methods that both CoffeeCup
and
TeaCup
must implement:
// In Source Packet in file interface/ex16/Cup.java abstract class Cup { public abstract void add(int amount); public abstract int removeOneSip(int sipSize); public abstract int spillEntireContents(); } // In Source Packet in file interface/ex16/CoffeeCup.java class CoffeeCup extends Cup { public void add(int amount) { //... } public int removeOneSip(int sipSize) { //... return 0; } public int spillEntireContents() { //... return 0; } //... } // In Source Packet in file interface/ex16/TeaCup.java class TeaCup extends Cup { public void add(int amount) { //... } public int removeOneSip(int sipSize) { //... return 0; } public int spillEntireContents() { //... return 0; } //... }
Given this inheritance hierarchy, shown graphically in Figure 4-8, you could not instantiate a
Cup
object, but you could use a Cup
reference to send messages to
a CoffeeCup
or TeaCup
object. Given a Cup
reference that refers to a CoffeeCup
or TeaCup
, you could
invoke add()
, releaseOneSip()
, or
spillEntireContents()
on it. The implementation of those methods that
actually gets invoked at run-time will depend upon the actual class of the object referred to by the
Cup
reference.
Figure 4-8. Cup
as an abstract base class
Since this class Cup
contains only public abstract methods, it could alternatively
be declared as an interface:
// In Source Packet in file interface/ex17/Cup.java interface Cup { void add(int amount); int removeOneSip(int sipSize); int spillEntireContents(); } // In Source Packet in file interface/ex17/CoffeeCup.java class CoffeeCup implements Cup { public void add(int amount) { //... } public int removeOneSip(int sipSize) { //... return 0; } public int spillEntireContents() { //... return 0; } //... } // In Source Packet in file interface/ex17/TeaCup.java class TeaCup implements Cup { public void add(int amount) { //... } public int removeOneSip(int sipSize) { //... return 0; } public int spillEntireContents() { //... return 0; } //... }
Here you are using an interface to represent an abstract base class. As suggested by the Java Language
Specification, the name of the class, Cup
, is a noun. The inheritance hierarchy for this
is shown in Figure 4-9.
Figure 4-9. Cup
as an interface
In general, if you have an abstract base class that declares only public abstract methods and public static final fields, you may as well make it an interface. Because an abstract class is restricted to single inheritance, but an interface can be multiply inherited, an interface is more flexible than an abstract class. If you want to have any default implementation of methods, or non-public members in the base class, however, it must be an abstract class.
Sponsored Links
|