The
most important aspect of inheritance is not that it provides methods for the
new class. It’s the relationship expressed between the new class and the
base class. This relationship
can be summarized by saying “The new class
is
a type of
the existing class.”
This
description is not just a fanciful way of explaining inheritance –
it’s supported directly by the language. As an example, consider a base
class called
Instrument
that represents musical instruments and a derived class called
Wind.
Because inheritance means that all of the methods in the base class are also
available in the derived class, any message you can send to the base class can
also be sent to the derived class. If the
Instrument
class has a
play( )
method, so will
Wind
instruments. This means we can accurately say that a
Wind
object is also a type of
Instrument.
The following example shows how the compiler supports this notion:
//: Wind.java
// Inheritance & upcasting
import java.util.*;
class Instrument {
public void play() {}
static void tune(Instrument i) {
// ...
i.play();
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
} ///:~
What’s
interesting in this example is the
tune( )
method, which accepts an
Instrument
handle. However, in
Wind.main( )
the
tune( )
method is called by giving it a
Wind
handle. Given that Java is particular about type checking, it seems strange
that a method that accepts one type will readily accept another type, until you
realize that a
Wind
object is also an
Instrument
object, and there’s no method that
tune( )
could call for an
Instrument
that isn’t also in
Wind.
Inside
tune( ),
the code works for
Instrument
and anything derived from
Instrument,
and the act of converting a
Wind
handle into an
Instrument
handle is called
upcasting.
Why
“upcasting”?
The
reason for the term is historical and is based on the way class inheritance
diagrams have
traditionally been drawn with the root at the top of the page, growing
downward. (Of course, you can draw your diagrams any way you find helpful.) The
inheritance diagram for
Wind.java
is then:
Casting
from derived to base moves
up
on the inheritance diagram, so it’s commonly referred to as upcasting.
Upcasting is always safe because you’re going from a more specific type
to a more general type. That is, the derived class is a superset of the base
class. It might contain more methods than the base class, but it must contain
at
least
the methods in the base class. The only thing that can occur to the class
interface during the upcast is that it can lose methods, not gain them. This is
why the compiler allows upcasting without any explicit casts or other special
notation.
You
can also perform the reverse of upcasting, called downcasting,
but this involves a dilemma that is the subject of Chapter 11.
Composition
vs. inheritance revisited
In
object-oriented programming, the most likely way that you’ll create and
use code is by simply packaging data and methods together into a class, and
using objects of that class. Occasionally, you’ll use existing classes to
build new classes with composition. Even less frequently than that you’ll
use inheritance. So although inheritance gets a lot of emphasis while learning
OOP, it doesn’t mean that you should use it everywhere you possibly can.
On the contrary, you should use it sparingly, only when it’s clear that
inheritance is useful. One
of the clearest ways to determine whether you should use composition or
inheritance is to ask whether you’ll ever need to upcast from your new
class to the base class. If you must upcast, then inheritance is necessary, but
if you don’t need to upcast, then you should look closely at whether you
need inheritance. The next chapter (polymorphism) provides one of the most
compelling reasons for upcasting, but if you remember to ask “Do I need
to upcast?”, you’ll have a good tool for deciding between
composition and inheritance.