Inheritance
syntax
Inheritance
is such an integral part of Java (and OOP languages in general) that it was
introduced in Chapter 1 and has been used occasionally in chapters before this
one because certain situations required it. In addition, you’re always
doing inheritance when you create a class, because if you don’t say
otherwise you inherit from Java’s standard root class
Object. The
syntax for composition is obvious, but to perform inheritance there’s a
distinctly different form. When you inherit, you say “This new class is
like that old class.” You state this in code by giving the name of the
class as usual, but before the opening brace of the class body, put the keyword
extends
followed by the name of the base
class
.
When you do this, you automatically get all the data members and methods in the
base class. Here’s an example:
//: Detergent.java
// Inheritance syntax & properties
class Cleanser {
private String s = new String("Cleanser");
public void append(String a) { s += a; }
public void dilute() { append(" dilute()"); }
public void apply() { append(" apply()"); }
public void scrub() { append(" scrub()"); }
public void print() { System.out.println(s); }
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute(); x.apply(); x.scrub();
x.print();
}
}
public class Detergent extends Cleanser {
// Change a method:
public void scrub() {
append(" Detergent.scrub()");
super.scrub(); // Call base-class version
}
// Add methods to the interface:
public void foam() { append(" foam()"); }
// Test the new class:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Testing base class:");
Cleanser.main(args);
}
} ///:~
This
demonstrates a number of features. First, in the
Cleanser
append( )
method,
Strings
are concatenated to
s
using the
+=
operator, which is one of the operators (along with ‘
+’)
that the Java designers “overloaded” to work with Strings. Second,
both
Cleanser
and
Detergent
contain a main( )
method. You can create a
main( )
for each one of your classes, and it’s often recommended to code this way
so that your test code is wrapped in with the class. Even if you have a lot of
classes in a program only the
main( )
for the
public
class invoked on the command line will be called. (And you can have only one
public
class per file.) So in this case, when you say
java
Detergent
,
Detergent.main( )
will be called. But you can also say
java
Cleanser
to
invoke
Cleanser.main( ),
even though
Cleanser
is not a
public
class. This technique of putting a
main( )
in each class allows easy unit
testing for each class. And you don’t need to remove the
main( )
when you’re finished testing; you can leave it in for later testing.
Here,
you can see that
Detergent.main( )
calls
Cleanser.main( )
explicitly.
It’s
important that all of the methods in
Cleanser
are
public.
Remember that if you leave off any access specifier the member defaults to
“friendly,” which allows access only to package members. Thus,
within this package, anyone could use those methods if there were no access
specifier.
Detergent
would have no trouble, for example. However, if a class from some other package
were to inherit
Cleanser
it could access only
public
members.
So to plan for inheritance, as a general rule make all fields
private
and
all methods
public.
(protected
members
also allow access by derived classes; you’ll learn about this later.) Of
course, in particular cases you must make adjustments, but this is a useful
guideline.
Note
that
Cleanser
has a set of methods in its interface:
append( ),
dilute( ),
apply( ),
scrub( )
and
print( ).
Because
Detergent
is
derived
from
Cleanser
(via the extends
keyword) it automatically gets all these methods in its interface, even though
you don’t see them all explicitly defined in
Detergent.
You can think of inheritance, then, as
reusing
the interface.
(The implementation comes along for free, but that part isn’t the primary
point.)
As
seen in
scrub( ),
it’s possible to take a method that’s been defined in the base
class and modify it. In this case, you might want to call the method from the
base class inside the new version. But inside
scrub( )
you cannot simply call
scrub( ),
since that would produce a recursive call, which isn’t what you want. To
solve this problem Java has the keyword
super
that refers to the “superclass”
that the current class has been inherited from. Thus the expression
super.scrub( )
calls the base-class version of the method
scrub( ). When
inheriting you’re not restricted to using the methods of the base class.
You can also add new methods to the derived class exactly the way you put any
method in a class: just define it. The
extends
keyword
suggests that you are going to add new methods to the base-class interface, and
the method
foam( )
is an example of this.
In
Detergent.main( )
you
can see that for a
Detergent
object you can call all the methods that are available in
Cleanser
as well as in
Detergent
(i.e.
foam( )).
Initializing
the base class
Since
there are now two classes involved – the base class and the derived
class – instead of just one, it can be a bit confusing to try to imagine
the resulting object produced by a derived class. From the outside, it looks
like the new class has the same interface as the base class and maybe some
additional methods and fields. But inheritance doesn’t just copy the
interface of the base class. When you create an object of the derived class, it
contains within it a
subobject
of the base class. This subobject
is the same as if you had created an object of the base class by itself.
It’s just that, from the outside, the subobject of the base class is
wrapped within the derived-class object.
Of
course, it’s essential that the base-class subobject be initialized
correctly and there’s only one way to guarantee that: perform the
initialization in the constructor, by calling the base-class constructor, which
has all the appropriate knowledge and privileges to perform the base-class
initialization. Java automatically inserts calls to the base-class constructor
in the derived-class constructor. The following example shows this working with
three levels of inheritance:
//: Cartoon.java
// Constructor calls during inheritance
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
} ///:~
The
output for this program shows the automatic calls:
Art constructor
Drawing constructor
Cartoon constructor
You
can see that the construction happens from the base “outward,” so
the base class is initialized before the derived-class constructors can access
it.
Even
if you don’t create a constructor for
Cartoon( ),
the compiler will synthesize
a default constructor for you that calls the base class constructor.
Constructors
with arguments
The
above example has default constructors;
that is, they don’t have any arguments. It’s easy for the compiler
to call these because there’s no question about what arguments to pass.
If your class doesn’t have default arguments or if you want to call a
base-class constructor that has an argument you must explicitly write the calls
to the base-class constructor using the super
keyword and the appropriate argument list:
//: Chess.java
// Inheritance, constructors and arguments
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
} ///:~
If
you don’t call the base-class constructor in
BoardGame( ),
the compiler will complain that it can’t find a constructor of the form
Game( ).
In addition, the call to the base-class constructor
must
be the first thing you do in the derived-class constructor. (The compiler will
remind you if you get it wrong.)
Catching
base constructor exceptions
As
just noted, the compiler forces you to place the base-class constructor call
first in the body of the derived-class constructor. This means nothing else can
appear before it. As you’ll see in Chapter 9, this also prevents a
derived-class constructor from catching any exceptions that come from a base
class. This can be inconvenient at times.