Bruce Eckel’s Thinking in Java | Contents | Prev | Next |
you learn about polymorphism, it can seem that everything ought to be inherited
because polymorphism is such a clever tool. This can burden your designs; in
fact if you choose inheritance first when you’re using an existing class
to make a new class things can become needlessly complicated.
better approach is to choose composition
first, when it’s not obvious which one you should use. Composition does
not force a design into an inheritance hierarchy. But composition is also more
flexible since it’s possible to dynamically choose a type (and thus
behavior) when using composition, whereas inheritance requires an exact type to
be known at compile time. The following example illustrates this:
//: Transmogrify.java // Dynamically changing the behavior of // an object via composition. interface Actor { void act(); } class HappyActor implements Actor { public void act() { System.out.println("HappyActor"); } } class SadActor implements Actor { public void act() { System.out.println("SadActor"); } } class Stage { Actor a = new HappyActor(); void change() { a = new SadActor(); } void go() { a.act(); } } public class Transmogrify { public static void main(String[] args) { Stage s = new Stage(); s.go(); // Prints "HappyActor" s.change(); s.go(); // Prints "SadActor" } } ///:~
Stage
object contains a handle to an
Actor,
which is initialized to a
HappyActor
object. This means
go( )
produces a particular behavior. But since a handle can be re-bound to a
different object at run time, a handle for a
SadActor
object can be substituted in
a
and then the behavior produced by
go( )
changes. Thus you gain dynamic flexibility at run time. In contrast, you
can’t decide to inherit differently at run time; that must be completely
determined at compile time.
general guideline is “Use inheritance to express differences in behavior,
and member variables to express variations in state.” In the above
example, both are used: two different classes are inherited to express the
difference in the
act( )
method, and
Stage
uses composition to allow its state to be changed. In this case, that change in
state happens to produce a change in behavior.
Pure
inheritance vs. extension
studying inheritance, it would seem that the cleanest way to create an
inheritance hierarchy is to take the “pure” approach. That is, only
methods that have been established in the base class or
interface
are to be overridden in the derived class, as seen in this diagram:
can be termed a pure “is-a”
relationship because the interface of a class establishes what it is.
Inheritance guarantees that any derived class will have the interface of the
base class and nothing less. If you follow the above diagram, derived classes
will also have
no
more
than the base class interface.
can be thought of as pure
substitution
,
because derived class objects can be perfectly substituted for the base class,
and you never need to know any extra information about the subclasses when
you’re using them:
is, the base class can receive any message you can send to the derived class
because the two have exactly the same interface. All you need to do is upcast
from the derived class and never look back to see what exact type of object
you’re dealing with. Everything is handled through polymorphism.
you see it this way, it seems like a pure “is-a” relationship is
the only sensible way to do things, and any other design
indicates muddled thinking and is by definition broken. This too is a trap. As
soon as you start thinking this way, you’ll turn around and discover that
extending the interface (which, unfortunately, the keyword extends
seems to promote) is the perfect solution to a particular problem. This could
be termed an “is-like-a”
relationship because the derived class is
like
the base class – it has the same fundamental interface – but it has
other features that require additional methods to implement:
this is also a useful and sensible approach (depending on the situation) it has
a drawback. The extended part of the interface in the derived class is not
available from the base class, so once you upcast you can’t call the new
methods:
you’re not upcasting in this case, it won’t bother you, but often
you’ll get into a situation in which you need to rediscover the exact
type of the object so you can access the extended methods of that type. The
following sections show how this is done.
Downcasting
and run-time
type
identification
you lose the specific type information via an
upcast
(moving up the inheritance hierarchy), it makes sense that to retrieve the type
information – that is, to move back down the inheritance hierarchy
– you use a downcast.
However, you know an upcast is always safe; the base class cannot have a bigger
interface than the derived class, therefore every message you send through the
base class interface is guaranteed to be accepted. But with a downcast, you
don’t really know that a shape (for example) is actually a circle. It
could instead be a triangle or square or some other type.
solve this problem there must be some way to guarantee that a downcast is
correct, so you won’t accidentally cast to the wrong type and then send a
message that the object can’t accept. This would be quite unsafe.
some languages (like C++) you must perform a special operation in order to get
a type-safe downcast, but in Java
every
cast
is checked! So even though it looks like you’re just performing an
ordinary parenthesized cast, at run time this cast is checked to ensure that it
is in fact the type you think it is. If it isn’t, you get a
ClassCastException.
This act of checking types at run time is called run-time
type identification
(RTTI).
The following example demonstrates the behavior of RTTI:
//: RTTI.java // Downcasting & Run-Time Type // Identification (RTTI) import java.util.*; class Useful { public void f() {} public void g() {} } class MoreUseful extends Useful { public void f() {} public void g() {} public void u() {} public void v() {} public void w() {} } public class RTTI { public static void main(String[] args) { Useful[] x = { new Useful(), new MoreUseful() }; x[0].f(); x[1].g(); // Compile-time: method not found in Useful: //! x[1].u(); ((MoreUseful)x[1]).u(); // Downcast/RTTI ((MoreUseful)x[0]).u(); // Exception thrown } } ///:~
in the diagram,
MoreUseful
extends the interface of
Useful.
But since it’s inherited, it can also be upcast to a
Useful.
You can see this happening in the initialization of the array
x
in
main( ).
Since both objects in the array are of class
Useful,
you can send the
f( )
and
g( )
methods to both, and if you try to call
u( )
(which exists only in
MoreUseful)
you’ll get a compile-time error message.
you want to access the extended interface of a
MoreUseful
object, you can try to downcast. If it’s the correct type, it will be
successful. Otherwise, you’ll get a ClassCastException.
You don’t need to write any special code for this exception, since it
indicates a programmer error that could happen anywhere in a program.