By
itself, the concept of an object is a convenient tool. It allows you to package
data and functionality together by
concept,
so you can represent an appropriate problem-space idea rather than being forced
to use the idioms of the underlying machine. These concepts are expressed in
the primary idea of the programming language as a data type (using the
class
keyword).
It
seems a pity, however, to go to all the trouble to create a data type and then
be forced to create a brand new one that might have similar functionality.
It’s nicer if we can take the existing data type, clone it and make
additions and modifications to the clone. This is effectively what you get with
inheritance,
with the exception that if the original class (called the
base
or
super
or
parent
class) is changed, the modified “clone” (called the
derived
or
inherited
or
sub
or
childclass)
also reflects the appropriate changes. Inheritance is implemented in Java with
the
extends
keyword. You make a new class and you say that it
extends
an existing class.
When
you inherit you create a new type, and the new type contains not only all the
members of the existing type (although the
private
ones are hidden away and inaccessible), but more importantly it duplicates the
interface of the base class. That is, all the messages you can send to objects
of the base class you can also send to objects of the derived class. Since we
know the type of a class by the messages we can send to it, this means that the
derived class
is
the same type as the base class
.
This type equivalence via inheritance is one of the fundamental gateways in
understanding the meaning of object-oriented programming.
Since
both the base class and derived class have the same interface, there must be
some implementation to go along with that interface. That is, there must be a
method to execute when an object receives a particular message. If you simply
inherit a class and don’t do anything else, the methods from the
base-class interface come right along into the derived class. That means
objects of the derived class have not only the same type, they also have the
same behavior, which doesn’t seem particularly interesting.
You
have two ways to differentiate your new derived class from the original base
class it inherits from. The first is quite straightforward: you simply add
brand new functions to the derived class. These new functions are not part of
the base class interface. This means that the base class simply didn’t do
as much as you wanted it to, so you add more functions. This simple and
primitive use for inheritance is, at times, the perfect solution to your
problem. However, you should look closely for the possibility that your base
class might need these additional functions.
Overriding
base-class functionality
Although
the
extends
keyword implies that you are going to add new functions to the interface,
that’s not necessarily true. The second way to differentiate your new
class is to
change
the behavior of an existing base-class function. This is referred to as
overriding
that function.
To
override a function, you simply create a new definition for the function in the
derived class. You’re saying “I’m using the same interface
function here, but I want it to do something different for my new type.”
Is-a
vs. is-like-a relationships
There’s
a certain debate that can occur about inheritance: Should inheritance override
only
base-class functions? This means that the derived type is
exactly
the same type as the base class since it has exactly the same interface. As a
result, you can exactly substitute an object of the derived class for an object
of the base class. This can be thought of as
pure
substitution
.
In a sense, this is the ideal way to treat inheritance. We often refer to the
relationship between the base class and derived classes in this case as an
is-a
relationship, because you can say “a circle
is
a
shape.” A test for inheritance is whether you can state the is-a
relationship about the classes and have it make sense.
There
are times when you must add new interface elements to a derived type, thus
extending the interface and creating a new type. The new type can still be
substituted for the base type, but the substitution isn’t perfect in a
sense because your new functions are not accessible from the base type. This
can be described as an
is-like-a
relationship; the new type has the interface of the old type but it also
contains other functions, so you can’t really say it’s exactly the
same. For example, consider an air conditioner. Suppose your house is wired
with all the controls for cooling; that is, it has an interface that allows you
to control cooling. Imagine that the air conditioner breaks down and you
replace it with a heat pump, which can both heat and cool. The heat pump
is-like-an
air conditioner, but it can do more. Because your house is wired only to
control cooling, it is restricted to communication with the cooling part of the
new object. The interface of the new object has been extended, and the existing
system doesn’t know about anything except the original interface.
When
you see the substitution principle it’s easy to feel like that’s
the only way to do things, and in fact it is nice if your design works out that
way. But you’ll find that there are times when it’s equally clear
that you must add new functions to the interface of a derived class. With
inspection both cases should be reasonably obvious.