Designing with inheritance

Bruce Eckel’s Thinking in Java Contents | Prev | Next

Once
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.

//: 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"
  }
} ///:~ 

A
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.

Pure
inheritance vs. extension

When
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:

That
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.

While
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:

Downcasting
and run-time

type
identification

To
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.

//: 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
  }
} ///:~ 

As
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.

If
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.

More by Author

Must Read