So far, we know how to create a Java class and to create objects, which are instances of a class. But an object by itself isn’t very interesting—no more interesting than, say, a table knife. You can marvel at a table knife’s perfection, but you can’t really do anything with it until you have some other pieces of cutlery and food to use the cutlery on. The same is true of objects and classes in Java: they’re interesting by themselves, but what’s really important comes from relationships that you establish among them.
That’s what we’ll cover in this chapter. In particular, we’ll be looking at several kinds of relationships:
- Inheritance relationships
How a class inherits methods and variables from its parent class
- Interfaces
How to declare that a class supports certain behavior and define a type to refer to that behavior
- Packaging
How to organize objects into logical groups
- Inner classes
A generalization of classes that lets you nest a class definition inside of another class definition
Classes in Java exist in a class
hierarchy. A class in Java can be declared as a
subclass of another class using the
extends
keyword. A subclass
inherits
variables and
methods from
its superclass and uses them as if they were
declared within the subclass itself:
class Animal { float weight; ... void eat( ) { ... } ... } class Mammal extends Animal { int heartRate; // inherits weight ... void breathe( ) { ... } // inherits eat( ) }
In this example, an object of type Mammal
has both
the instance variable weight
and the method
eat( )
. They are inherited from
Animal
.
A class can extend only one other class. To use the proper terminology, Java allows single inheritance of class implementation. Later in this chapter we’ll talk about interfaces, which take the place of multiple inheritance as it’s primarily used in C++.
A subclass can be further subclassed. Normally, subclassing specializes or refines a class by adding variables and methods:
class Cat extends Mammal { boolean longHair; // inherits weight and heartRate ... void purr( ) { ... } // inherits eat() and breathe( ) }
The Cat
class is a type of
Mammal
that is ultimately a type of
Animal
. Cat
objects inherit all
the characteristics of Mammal
objects and, in
turn, Animal
objects. Cat
also
provides additional behavior in the form of the purr( )
method and the longHair
variable. We
can denote the class relationship in a diagram, as shown in Figure 6.1.
A subclass inherits all members of its
superclass not designated as private
. As
we’ll discuss shortly, other levels of visibility affect what
inherited members of the class can be seen from outside of the class
and its subclasses, but at a minimum, a subclass always has the same
set of visible members as its parent. For this reason, the type of a
subclass can be considered a subtype of its parent, and instances of
the subtype can be used anywhere instances of the supertype are
allowed. Consider the following example:
Cat simon = new Cat( ); Animal creature = simon;
The Cat
simon
in this example
can be assigned to the Animal
type variable
creature
because Cat
is a
subtype of Animal
.
In the section on methods in Chapter 5, we saw that a local variable of the same name as an instance variable shadows (hides) the instance variable. Similarly, an instance variable in a subclass can shadow an instance variable of the same name in its parent class, as shown in Figure 6.2.
In Figure 6.2, the variable
weight
is declared in three places: as a local
variable in the method foodConsumption( )
of the
class Mammal
, as an instance variable of the class
Mammal
, and as an instance variable of the class
Animal
. The actual variable selected depends on
the scope in which we are working.
In the previous example, all variables were of the same type. About the only reason for declaring a variable with the same type in a subclass is to provide an alternate initializer.
A more important use of shadowed variables involves changing their
types. We could, for example, shadow an int
variable with a double
variable in a subclass that
needs decimal values instead of integer values. We do this without
changing the existing code because, as its name suggests, when we
shadow variables, we don’t replace them but instead mask them.
Both variables still exist; methods of the superclass see the
original variable, and methods of the subclass see the new version.
The determination of what variables the various methods see occurs at
compile time.
Here’s a simple example:
class IntegerCalculator { int sum; ... } class DecimalCalculator extends IntegerCalculator { double sum; ... }
In this example, we shadow the instance variable
sum
to change its type from int
to double
.[19] Methods
defined in the class IntegerCalculator
see the
integer variable sum
, while methods defined in
DecimalCalculator
see the floating-point variable
sum
. However, both variables actually exist for a
given instance of DecimalCalculator
, and they can
have independent values. In fact, any methods that
DecimalCalculator
inherits from
IntegerCalculator
actually see the integer
variable sum
.
Since both variables exist in DecimalCalculator
,
we need to reference the variable inherited from
IntegerCalculator
. We do that using the
super
reference:
int s = super.sum;
Inside of DecimalCalculator
, the
super
keyword used in this manner refers to the
sum
variable defined in the superclass.
We’ll explain the use of super
more fully in
a bit.
Another important point about shadowed variables has to do with how
they work when we refer to an object by way of a less derived type.
For example, we can refer to a DecimalCalculator
object as an IntegerCalculator
. If we do so and
then access the variable sum
, we get the integer
variable, not the decimal one:
DecimalCalculator dc = new DecimalCalculator( ); IntegerCalculator ic = dc; int s = ic.sum; // accesses IntegerCalculator sum
After this detailed explanation, you may still be wondering what shadowed variables are good for. Well, to be honest, the usefulness of shadowed variables is limited, but it’s important to understand the concepts before we talk about doing the same thing with methods. We’ll see a different and more dynamic type of behavior with method shadowing, or more correctly, method overriding.
In Chapter 5, we saw we could declare overloaded methods (i.e., methods with the same name but a different number or type of arguments) within a class. Overloaded method selection works in the way we described on all methods available to a class, including inherited ones. This means that a subclass can define some overloaded methods that augment the overloaded methods provided by a superclass.
But a subclass does more than that; it can define a method that has exactly the same method signature (arguments and return type) as a method in its superclass. In that case, the method in the subclass overrides the method in the superclass and effectively replaces its implementation, as shown in Figure 6.3. Overriding methods to change the behavior of objects is called sub-type polymorphism . It’s the kind that most people think of when they talk about the power of object-oriented languages.
In Figure 6.3, Mammal
overrides the reproduce( )
method of
Animal
, perhaps to specialize the method for the
peculiar behavior of mammals’ giving live birth.[20] The
Cat
object’s sleeping behavior is overridden
to be different from that of a general Animal
,
perhaps to accommodate cat naps. The Cat
class
also adds the more unique behaviors of purring and hunting mice.
From what you’ve seen so far, overridden methods probably look
like they shadow methods in superclasses, just as variables do. But
overridden methods are actually more powerful than that. An
overridden method in Java acts like a
virtual
method in C++. When there are multiple
implementations of a method in the inheritance hierarchy of an
object, the one in the “most derived” class (the lowest
one in the hierarchy) always overrides the others, even if we refer
to the object by way of a less derived type. For example, if we have
a Cat
instance assigned to a variable of the more
general type Animal
and we call its
sleep( )
method, we get the sleep( )
method implemented in the Cat
class,
not the one in Animal
:
Cat simon = new Cat( ); Animal creature = simon; ... creature.sleep(); // accesses Cat sleep( );
In other respects, the variable creature
looks
like an Animal
. For example, access to a shadowed
variable would find the implementation in the
Animal
class, not the Cat
class. However, because methods are virtual, the appropriate method
in the Cat
class can be located, even though we
are dealing with an Animal
object. This means we
can deal with specialized objects as if they were more general types
of objects and still take advantage of their specialized
implementations of behavior.
A common programming error in Java is to miss and accidentally overload a method when trying to override it. Any difference in the number or type of arguments produces two overloaded methods instead of a single, overridden method. Make it a habit to look twice when overriding methods.
In a previous section, we mentioned that overloaded methods are selected by the compiler at compile time. Overridden methods, on the other hand, are selected dynamically at runtime. Even if we create an instance of a subclass, our code has never seen before (perhaps a new object type loaded from the network), any overriding methods that it contains will be located and invoked at runtime to replace those that existed when we last compiled our code.
In contrast, if we load a new class that implements an additional, more specific overloaded method, our code will continue to use the implementation it discovered at compile time. Another effect of this is that casting (i.e., explicitly telling the compiler to treat an object as one of its assignable types) affects the selection of overloaded methods, but not overridden methods.
static
methods do
not belong to any object instance; they are accessed directly through
a class name, so they are not dynamically selected at runtime like
instance methods. That is why static
methods are
called “static”—they are always bound at compile
time.
A static
method in a superclass can be shadowed by
another static
method in a subclass, as long as
the original method was not declared final
.
However, you can’t override a static
method
with a nonstatic
method. In other words, you
can’t change a static
method in a superclass
into an instance method in a subclass.
When Java has to search dynamically for overridden methods in subclasses, there’s a small performance penalty. In languages like C++, the default is for methods to act like shadowed variables, so you have to declare explicitly the methods you want to be dynamic (or, as C++ terms them, virtual).
In Java, instance methods are, by default, dynamic. But you can use
the final
modifier to declare that an instance
method can’t be overridden, so it won’t be subject to
dynamic binding and its performance won’t suffer.
We have seen
final
used with variables to effectively make them
constants. When applied to a method, final
means
that its implementation is constant; no overriding allowed.
final
can also be applied to an entire class,
which means the class can’t be subclassed.
Newer runtime systems like Sun’s HotSpot should, however, eliminate the need for this kind of specificity. A profiling runtime should be able to determine which methods are not being overridden and “optimistically inline” them.
When javac
,
the Java compiler, is run with the -O
switch, it
performs certain optimizations. It can inline
final
methods to improve performance (while
slightly increasing the size of the resulting class file).
private
methods, which are effectively
final
, can also be inlined, and
final
classes may also benefit from more powerful
optimizations.
Another kind of optimization allows you to include
debugging code in your Java
source without penalty. Java doesn’t have a preprocessor to
explicitly control what source is included, but you can get some of
the same effects by making a block of code conditional on a constant
(i.e., static
and final
)
variable. The Java compiler is smart enough to remove this code when
it determines that it won’t be called. For example:
static final boolean DEBUG = false; ... final void debug (String message) { if (DEBUG) { System.err.println(message); // do other stuff ... } }
If we compile this code using the -O
switch, the
compiler can recognize that the condition on the
DEBUG
variable is always false, and the body of
the debug( )
method will be optimized away. But
that’s not all—since debug( )
itself
is also final
, it can be inlined, and an empty
inlined method generates no code at all. So when we compile with
DEBUG
set to false
, calls to
the debug( )
method generate no residual code at
all.
Note
The -O
compiler switch is something that may eventually go away in favor of smarter runtime systems, like Sun’s HotSpot, which can inline arbitrary chunks of code dynamically. In some recent versions of Java, the -O
switch is documented not to work at all! We document it here mainly for completeness.
By now you should have a good, intuitive idea as to how methods are selected from the pool of potentially overloaded and overridden method names of a class. If, however, you are dying for a dry definition, we’ll provide one now. If you are satisfied with your understanding, you may wish to skip this little exercise in logic.
In a previous section, we offered an inductive rule for overloaded method resolution. It said that a method is considered more specific than another if its arguments are assignable to the arguments of the second method. We can now expand this rule to include the resolution of overridden methods by adding the following condition: to be more specific than another method, the type of the class containing the method must also be assignable to the type of the class holding the second method.
What does that mean? Well, the only classes whose types are assignable are classes in the same inheritance hierarchy. So, what we’re talking about now is the set of all methods of the same name in a class or any of its parent or child classes. Since subclass types are assignable to superclass types, but not vice versa, the resolution is pushed, in the way that we expect, down the chain, toward the subclasses. This effectively adds a second dimension to the search, in which resolution is pushed down the inheritance tree towards more refined classes and, simultaneously, toward the most specific overloaded method within a given class.
When we talked about exception handling in
Chapter 4, we didn’t mention an important
restriction that applies when you override a method. When you
override a method, the new method (the overriding method) must adhere
to the throws
clause of the method it overrides.
In other words, if an overridden method declares that it can throw an
exception, the overriding method must also specify that it can throw
the same kind of exception, or a subtype of that exception. By
allowing the exception to be a subtype of the one specified by the
parent, the overriding method can refine the type of exception
thrown, to go along with its new behavior. For example:
class MeatInedibleException extends InedibleException { ... } class Animal { void eat( Food f ) throws InedibleException { ... } } class Herbivore extends Animal { void eat( Food f ) throws InedibleException { if ( f instanceof Meat ) throw new MeatInedibleException( ); ... } }
In this code, Animal
specifies that it can throw
an InedibleException
from its eat( )
method. Herbivore
is a subclass of
Animal
, so its eat( )
method
must also be able to throw an InedibleException
.
However, Herbivore
’s eat( )
method actually throws a more specific exception:
MeatInedibleException
. It can do this because
MeatInedibleException
is a subtype of
InedibleException
(remember that exceptions are
classes, too). Our calling code’s catch
clause can therefore be more specific:
Animal creature = ... try { creature.eat( food ); } catch ( MeatInedibleException ) { // creature can't eat this food because it's meat } catch ( InedibleException ) { // creature can't eat this food }
However, if we don’t care why the food is inedible, we’re
free to catch InedibleException
alone, because a
MeatInedibleException
is also an
InedibleException
.
The
special references
this
and super
allow you to
refer to the members of the current object instance or to members of
the superclass, respectively. We have seen this
used elsewhere to pass a reference to the current object and to refer
to shadowed instance variables. The reference
super
does the same for the parents of a class.
You can use it to refer to members of a superclass that have been
shadowed or overridden. A common arrangement is for an overridding
method in a subclass to do some preliminary work and then defer to
the overridden method of the superclass to finish the job:
class Animal { void eat( Food f ) throws InedibleException { // consume food } } class Herbivore extends Animal { void eat( Food f ) throws MeatInedibleException { // check if edible ... super.eat( f ); } }
In this example, our Herbivore
class overrides the
Animal
eat( )
method to first
do some checking on the food object. After doing its job, it uses
super.eat( )
to call the (otherwise overridden)
implementation of eat( )
in its superclass.
super
prompts a search for the method or variable
to begin in the scope of the immediate superclass rather than the
current class. The inherited method or variable found may reside in
the immediate superclass, or in a more distant one. The usage of the
super
reference when applied to overridden methods
of a superclass is special; it tells the method resolution system to
stop the dynamic method search at the superclass, instead of at the
most derived class (as it otherwise does). Without
super
, there would be no way to access overridden
methods.
As
in C++, a
cast
explicitly
tells the compiler to change the apparent type of an object
reference. Unlike in C++, casts in Java are checked both at compile
time and at runtime to make sure they are legal. Attempting to cast
an object to an incompatible type at runtime results in a
ClassCastException
.
Only casts between objects in the same inheritance hierarchy (and as
we’ll see later, to appropriate interfaces) are legal in Java
and pass the scrutiny of the compiler and the runtime system.
Casts in Java affect only the treatment of references; they never change the form of the actual object. This is an important rule to keep in mind. You never change the object pointed to by a reference by casting it; you change only the compiler’s (or runtime system’s) notion of it.
A cast can be used to
narrow
the type of a reference—to make it
more specific. Often, we’ll do this when we have to retrieve an
object from a more general type of collection or when it has been
previously used as a less derived type. (The prototypical example is
using an object in a Vector
or
Hashtable
, as you’ll see in Chapter 9.) Continuing with our Cat
example:
Animal creature = ... Cat simon = ... creature = simon; // OK // simon = creature; // Compile time error, incompatible type simon = (Cat)creature; // OK
We can’t reassign the reference in creature
to the variable simon
even though we know it holds
an instance of a Cat
(Simon). We have to perform
the indicated cast. This is also called
downcasting
the reference.
Note that an implicit cast was performed when we went
the other way to widen the reference
simon
to type Animal
during the
first assignment. In this case, an explicit cast would have been
legal, but superfluous.
If casting seems complicated, here’s a simple way to think
about it. Basically, you can’t lie about what an object is. If
you have a Cat
object, you can cast it to a less
derived type (i.e., a type above it in the class hierarchy) such as
Animal
or even Object
, since
all Java classes are a subclass of Object
. If you
have an Object
you know is a
Cat
, you can downcast the
Object
to be an Animal
or a
Cat
. However, if you aren’t sure if the
Object
is a Cat
or a
Dog
at runtime, you should check it with
instanceof
before you perform the cast. If you
get the cast wrong, the runtime system throws a
ClassCastException
.
As we mentioned earlier, casting can affect the selection of
compile-time items such as variables and overloaded methods, but not
the selection of overridden methods. Figure 6.4 shows the difference. As shown in the top
half of the diagram, casting the reference simon
to type Animal
(widening it) affects the selection
of the shadowed variable weight
within it.
However, as the lower half of the diagram indicates, the cast
doesn’t affect the selection of the
overridden method sleep( )
.
When
we talked
earlier about constructors, we discussed
how the special statement this( )
invokes an
overloaded constructor upon entry to another constructor. Similarly,
the statement
super( )
explicitly invokes the constructor of a superclass. Of
course, we also talked about how Java makes a chain of constructor
calls that includes the superclass’s constructor, so why use
super( )
explicitly? When Java makes an implicit
call to the superclass constructor, it calls the default constructor.
So, if we want to invoke a superclass constructor that takes
arguments, we have to do so explicitly using super( )
.
If we are going to call a superclass constructor with super( )
, it must be the first statement of our constructor, just
as this( )
must be the first call we make in an
overloaded constructor. Here’s a simple example:
class Person { Person ( String name ) { // setup based on name ... } ... } class Doctor extends Person { Doctor ( String name, String specialty ) { super( name ); // setup based on specialty ... } ... }
In this example, we use super( )
to take advantage
of the implementation of the superclass constructor and avoid
duplicating the code to set up the object based on its name. In fact,
because the class Person
doesn’t define a
default (no arguments) constructor, we have no choice but to call
super( )
explicitly. Otherwise, the compiler would
complain that it couldn’t find an appropriate default
constructor to call. In other words, if you subclass a class whose
constructors all take arguments, you have to invoke one of the
superclass’s constructors explicitly from your subclass
constructor.
Instance variables of the class are initialized upon return from the
superclass constructor, whether that’s due to an explicit call
to super( )
or an implicit call to the default
superclass constructor.
We can now give the full story of how constructors are chained together and when instance variable initialization occurs. The rule has three parts and is applied repeatedly for each successive constructor invoked.
If the first statement of a constructor is an ordinary statement—i.e., not a call to
this()
orsuper( )
—Java inserts an implicit call tosuper( )
to invoke the default constructor of the superclass. Upon returning from that call, Java initializes the instance variables of the current class and proceeds to execute the statements of the current constructor.If the first statement of a constructor is a call to a superclass constructor via
super( )
, Java invokes the selected superclass constructor. Upon its return, Java initializes the current class’s instance variables and proceeds with the statements of the current constructor.If the first statement of a constructor is a call to an overloaded constructor via
this( )
, Java invokes the selected constructor and upon its return simply proceeds with the statements of the current constructor. The call to the superclass’s constructor has happened within the overloaded constructor, either explicitly or implicitly, so the initialization of instance variables has already occurred.
A
method in Java can be
declared with the
abstract
modifier to indicate that it’s just
a prototype. An abstract method has no body; it’s simply a
signature declaration followed by a semicolon. You can’t
directly use a class that contains an abstract method; you must
instead create a subclass that implements the abstract method’s
body.
abstract void vaporMethod( String name );
In Java, a class that contains one or more abstract methods must be
explicitly declared as an abstract class, also using the
abstract
modifier:
abstract class vaporClass { ... abstract void vaporMethod( String name ); ... }
An abstract class can contain other, nonabstract methods and ordinary
variable declarations; however, it can’t be instantiated. To be
used, it must be subclassed and its abstract methods must be
overridden with methods that implement a body. Not all abstract
methods have to be implemented in a single subclass, but a subclass
that doesn’t override all its superclass’s abstract
methods with actual, concrete implementations must also be declared
abstract
.
Abstract classes provide a framework for
classes that are to be “filled in” by the implementor.
The java.io.InputStream
class, for example, has a
single abstract method called read( )
. Various
subclasses of InputStream
implement read( )
in their own ways to read from their own sources. The
rest of the InputStream
class, however, provides
extended functionality built on the simple read( )
method. A subclass of InputStream
inherits these
nonabstract methods that provide functionality based on the simple
read( )
method that the
subclass implements.
Get Learning Java now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.