Designing with inheritance | CodeGuru

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 […]

Written By
CodeGuru Staff
CodeGuru Staff
Mar 1, 2001
5 minute read
CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More

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.

A


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

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.

A


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

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:

This


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.

This


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:

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.

When


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:

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:

If


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

Since


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.

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.

In


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

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.

There’s


more to RTTI than a simple cast. For example, there’s a way to see what


type you’re dealing with


before

you try to downcast it. All of Chapter 11 is devoted to the study of different


aspects of Java run-time type identification.


Contents

|

Prev

|

Next
CodeGuru Logo

CodeGuru covers topics related to Microsoft-related software development, mobile development, database management, and web application programming. In addition to tutorials and how-tos that teach programmers how to code in Microsoft-related languages and frameworks like C# and .Net, we also publish articles on software development tools, the latest in developer news, and advice for project managers. Cloud services such as Microsoft Azure and database options including SQL Server and MSSQL are also frequently covered.

Property of TechnologyAdvice. © 2026 TechnologyAdvice. All Rights Reserved

Advertiser Disclosure: Some of the products that appear on this site are from companies from which TechnologyAdvice receives compensation. This compensation may impact how and where products appear on this site including, for example, the order in which they appear. TechnologyAdvice does not include all companies or all types of products available in the marketplace.