Recall that we identified three key elements of OO programming:
We now discuss the third of these, inheritance. Inheritance is a mechanism that supports extensibility and reuse of code, both across programs and within programs.
Suppose someone gives us an implementation of the Puzzle
interface
that we saw in the previous lecture. In the APuzzle
implementation, the puzzle is
represented by a 2D array of integers:
class APuzzle implements Puzzle { private int[][] tiles; public int tile(int r, int c) { return tiles[r][c]; } public void move(Direction d) { ... } ... }
Now suppose that we want to build a better puzzle implementation by
reusing this code. For example, we might want the puzzle to
log all of the moves that have been made. We could proceed by
copying the code of APuzzle
to create a new class LogPuzzle
, which
we could then add new fields and methods to. But this strategy doesn't work
very well when the original supplier of APuzzle
fixes some bugs and
gives us a new version APuzzle'
. Now we have to either apply the same bug fixes to
LogPuzzle
on the code that we copied from APuzzle
to get a new LogPuzzle',
or start afresh from APuzzle'
and build a new LogPuzzle'
from it.
Either way, it will be a lot of work and difficult to automate.
Figure 1: The problem of merging upgrades
At least for some kinds of changes that we might make between
APuzzle
and LogPuzzle
, inheritance offers a solution to this
problem. We can think of inheritance as a language mechanism for
copying a class and making certain kinds of changes to that class, without
actually physically copying the code.
To add the new functionality to APuzzle
, we build a new class
LogPuzzle
that inherits all the functionality from APuzzle
.
This is done by declaring that LogPuzzle
extends APuzzle
:
class LogPuzzle extends APuzzle { private int numMoves; public int numMoves() { return numMoves; } public void move(Direction d) { numMoves++; super.move(d); } }
Here APuzzle
is the superclass and LogPuzzle
is the subclass.
In general, classes inheriting from other classes form an inheritance
hierarchy or class hierarchy, in which superclasses are above their
subclasses.
The LogPuzzle
class is just like the APuzzle
class, except:
numMoves
, and a
new field, numMoves
, that APuzzle
does not have.
move
method that overrides the original
version from APuzzle
. It overrides the original because it has the
same name and signature as the original method. (Recall that the signature
consists of the name of the method, the types of its arguments, and its return type.)
LogPuzzle
is a subtype of the type APuzzle
. That
is, LogPuzzle
<: APuzzle
.
Because of the subtyping relationship between the two classes, we can write the following code:
APuzzle p = new LogPuzzle(); p.move(up); // which move?
Now here is an interesting question. There are move
methods in
both LogPuzzle
and APuzzle
, and they do different
things. Which one is executed in the call p.move(up)
? The
static type of p
is APuzzle
, but at the time
of the call, p
refers to an object of dynamic type
LogPuzzle
.
In Java, instance (non-static) methods such as move
are dispatched according to the
dynamic type of the receiver object (the class from which it was originally created via new
).
So it is the LogPuzzle
version that is run. This is known as dynamic dispatch.
As a slightly more complicated example, suppose the
superclass APuzzle
has a method scramble
that calls move
,
but LogPuzzle
does not have a scramble
method:
class APuzzle { public void scramble() { ... move(randomDir) ... } } APuzzle p = new LogPuzzle(); p.scramble(); // is p.numMoves() equal to 0?
When p.scramble()
is called, the scramble
method of
APuzzle
is invoked. But when that method calls move
,
which version of move
is dispatched?
It is the move
method of LogPuzzle
. The
move
method is still an instance method, and as above, the method
dispatch mechanism starts at the dynamic class of the receiver object and
searches upward until finding an implementation. In this case it finds one
immediately in the LogPuzzle
class, and that is the one that is
dispatched.
There are two different ways to understand how inheritance works when an instance method is invoked. One way is to say that the system searches upward through the inheritance hierarchy starting at the dynamic type of the object, looking for the first implementation of the method. But it is (almost) equivalent to say that the methods from the superclasses are copied down to their subclasses, except when overridden.
We say "almost" because there are some cases where code cannot be naively copied down verbatim. For example, a method in the superclass can refer to a instance variable in the superclass that is shadowed by a instance variable of the same name in a subclass. If the method in the superclass refers to this instance variable, then it still refers to the same instance variable even after it is copied down to the subclass. The identity of the instance variable being named is fixed at the time the superclass is compiled.
For example, consider these class definitions:
class A { int x = 3; void print() { System.out.println(x); } } class B extends A { int x = 4; void print() { System.out.println(x); } }
If you write A a = new B(); a.print()
, then 4
will be
printed (dynamic dispatch!). However, if you got rid of the print
method in B
, say
class A { int x = 3; void print() { System.out.println(x); } } class B extends A { int x = 4; }
and then said B b = new B(); b.print()
, then 3
would be printed. When the inherited method refers to x
, it
still refers to the original, shadowed instance variable rather than the
newly declared one.
Static methods complicate the story slightly, because they cannot be overridden by subclasses. Unlike instance methods, the choice of what version to call is based purely on static information available at compile time, before the program is run. The version that is invoked is the version of the class to which the method belongs, and does not change when the code making the call is inherited by a subclass. Consider the following code:
class A { static int f() { ... } void g() { f(); } } class B extends A { static int f() { ... } } A x = new B(); x.g();
Classes A
and B
both have static methods f()
.
When the method g()
is called, the call to f()
inside
g()
invokes the A
version of f
rather than the B
version. Because f
is a static
method, the call to f()
is exactly the same as if it were written
A.f()
. It does not change in meaning when it is inherited
by B
.
The special syntax super.f()
is also a static call that always
invokes the parent class's version of f
.
Constructors are special static methods that are called with new
to create and initialize new instances of the class. If the programmer does not specify
a constructor, there is an implicit one with no arguments that just creates the object
but doesn't do anything else. If the programmer specifies an explicit constructor,
the implicit constructor with no arguments is no longer available.
In that case, the constructors of a subclass must call a
superclass constructor explicitly, as in the following example:
class APuzzle { private int[][] tiles; public APuzzle(int size) { tiles = new int[size][size]; } } class LogPuzzle extends APuzzle { private int numMoves; public LogPuzzle() { super(4); numMoves = 0; } }
Here, the constructor LogPuzzle
always creates puzzles of
size 4×4, which is accomplished by calling the superclass constructor
with super(4)
. The call to the superclass constructor is static.
What if we want the LogPuzzle
code to access the tiles
field
directly? As defined, we cannot, because tiles
is declared private
.
However, if we give tiles
the protected
visibility in
APuzzle
, it becomes visible to subclasses:
class APuzzle { protected int[][] tiles; ... }
Protected fields and methods form a second interface to a class. Public methods and fields are the public interface, which is exposed to client code. Protected methods and fields are the specialization interface, which is available to subclasses but not to ordinary clients. One of the challenges of good object-oriented design is to design both of these interfaces effectively, without confusing their roles. Designing a good specialization interface is especially important for object-oriented libraries where the classes provided by the library are intended to be extended through inheritance.
Suppose that scramble()
had been defined to call a protected method
internalMove
instead of the public move
method:
class APuzzle { public void scramble() { ... internalMove(n); ... } protected internalMove(int d) { ... } } class LogPuzzle extends APuzzle { private int numMoves = 0; protected internalMove(int d) { numMoves++; super.internalMove(d); } }
This example shows that the specialization interface of APuzzle
allows the LogPuzzle
class to change the behavior of existing
public methods without overriding them directly. Protected methods
are hooks for future extensibility of OO code. The specialization
interface defines how code can be extended.
An abstract class is a class that provides some state and behavior that
can be inherited by other classes, but that cannot be instantiated with new
.
Thus an abstract class cannot be the dynamic class
of any object. An abstract class, indicated by the keyword abstract
in the class declaration, may similarly declare methods that are marked
abstract
. Such methods do not need to come with an implementation,
but any non-abstract subclass must implement them.
Abstract classes are useful as a way to factor out and centralize common functionality needed by a group of related classes. Using inheritance in this way is much better than copying the code and state into all the classes, because the code occurs only in one place.
One useful pattern is to use such methods as holes in the implementation to be
filled in by subclasses. In this case, the protected
visibility
is appropriate because the methods are not intended to be used directly by
clients.