|
|
|
Sponsored Link •
|
|
Advertisement
|
Initialization and inheritance
When an object is initialized, all the instance variables defined in
the object's class must be set to proper initial values. While this is
necessary, often it is not enough to yield a fully initialized class.
An object incorporates not only the fields explicitly declared in its
class, but also those declared in its superclasses. To fully
initialize an object, therefore, all instance variables declared in its
class and in all its superclasses must be initialized.
Instance data of objects
Every object, except class Object itself, has at least one
superclass. When an object is created, the Java virtual machine
allocates enough space for all the object's instance variables, which
include all fields defined in the object's class and in all its
superclasses. For example, consider the following classes:
// Declared in file Object.java (not In source packet)
package java.lang;
public class Object {
// Has no fields
// Has several methods, not shown...
}
// In source packet in file init/ex14/Liquid.java
class Liquid {
// Has two fields:
private int mlVolume;
private float temperature; // in Celsius
// Has several methods, not shown...
}
// In source packet in file init/ex14/Coffee.java
class Coffee extends Liquid {
// Has two fields:
private boolean swirling;
private boolean clockwise;
// Has several methods, not shown...
}
Figure 1. Class Coffee's superclasses and fields
|
You can see the inheritance hierarchy for class Coffee, as
defined above, in Figure 1. This figure, as well as the code above,
shows Object as having no instance variables. But it is
possible that Object could have instance variables. The
actual internal make-up of class Object is a detail
specific to each Java platform implementation. It is extremely likely,
however, that Object will have no fields in any given Java
platform implementation. Because Object is the superclass
of all other objects, any fields declared in Object must
be allocated for every object used by every Java program.
In Figure 2, you can see the data that must be allocated on the heap
for a Coffee object. The part of the heap that is occupied
by the instance data for the Coffee object is shown in the
cyan color. Keep in mind that the actual manner of representing objects
on the heap is an implementation detail of each particular Java virtual
machine. This figure represents just one of many possible schemes for
storing objects on the heap inside the JVM.
Figure 2. Instance data for a Coffee object
|
Figure 2 shows that the instance data for a Coffee object
includes each instance variable defined in class Coffee
and each of Coffee's superclasses. Both of
Liquid's fields, mlVolume and
temperature, are part of the Coffee object's
data, as well as Coffee's fields: swirling
and clockwise. This is true even though
Coffee doesn't actually inherit the
mlVolume and temperature fields from class
Liquid.
A note on the word "inherit"
In Java jargon, the word "inherit" has a restricted meaning.
A subclass inherits only accessible members of its superclasses -- and
only if the subclass doesn't override or hide those accessible members.
A class's members are the fields and methods actually declared in the
class, plus any fields and methods it inherits from superclasses. In
this case, because Liquid's mlVolume and
temperature fields are private, they are not accessible to
class Coffee. Coffee does not inherit those
fields. As a result, the methods declared in class Coffee
can't directly access those fields. Despite this, those fields are
still part of the instance data of a Coffee object.
Pointers to class data
Figure 2 also shows, as part of the instance data of the
Coffee object, a mysterious 4-byte quantity labeled
"native pointer to class information." Every Java virtual
machine must have the capability to determine information about its
class, given only a reference to an object. This is needed for many
reasons, including type-safe casting and the instanceof
operator.
Figure 2 illustrates one way in which a Java virtual machine implementation could associate class information with the instance data for an object. In this figure, a native pointer to a data structure containing class information is stored along with the instance variables for an object. The details in which the various ways a JVM could connect an object's data with its class information are beyond the scope of this article. The important thing to understand here is that class information will in some way be associated with the instance data of objects, and that the instance data includes fields for an object's class and all its superclasses.
Initializing fields in superclasses
Each class contains code to initialize the fields explicitly declared
in that class. Unlike methods, constructors are never inherited. If you
don't explicitly declare a constructor in a class, that class will not
inherit a constructor from its direct superclass. Instead, the compiler
will generate a default constructor for that class. This is because a
superclass constructor can't initialize fields in the subclass. A
subclass must have its own constructor to initialize its own instance
variables. In the class file, this translates to: every class has at
least one <init> method responsible for initializing
the class variables explicitly declared in that class.
For every object, you can trace a path of classes on an inheritance
hierarchy between the object's class and class Object. For
the Coffee object described above and shown in Figures 1
and 2, the path is: Coffee, Liquid,
Object. To fully initialize an object, the Java virtual
machine must invoke (at least) one instance initialization method from
each class along the object's inheritance path. In the case of
Coffee, this means that at least one instance
initialization method must be invoked for each of the classes
Coffee, Liquid, and Object.
During initialization, an <init> method may use one
field in calculating another field's initial value. While this is
perfectly reasonable, it brings up the possibility that a field could
be used before it has been initialized to its proper (not default)
initial value. As mentioned earlier in this article, Java includes
mechanisms that help prevent an instance variable from being used
before it has been properly initialized. One mechanism is the rule,
enforced by the Java compiler, forbidding initializers from directly
using instance variables declared textually after the variable being
initialized. Another mechanism is the order in which the fields from
each class along an object's inheritance path are initialized: the
"order of initialization."
Order of initialization
In Java, the fields of an object are initialized starting with the
fields declared in the base class and ending with the fields declared
in the object's class. For a CoffeeCup object with the
inheritance path shown in Figure 1, the order of initialization of
fields would be:
Object's fields (this will be quick, because there are
none)
Liquid's fields (mlVolume and
temperature)
Coffee's fields (swirling and
clockwise)
This base-class-first order aims to prevent fields from being used before they are initialized to their proper (not default) values. In a constructor or initializer, you can safely use a superclass's field directly, or call a method that uses a superclass's field. By the time the code in your constructor or initializer is executed, you can be certain that the fields declared in any superclasses have already been properly initialized.
For example, you could safely use the temperature variable
declared in class Liquid when you are initializing the
swirling variable declared in class Coffee.
(Perhaps if the temperature is above the boiling point for coffee, you
set swirling to false.) If temperature were
not private, class Coffee would inherit the field, and you
could use it directly in an initializer or constructor of class
Coffee. In this case, temperature is private,
so you'll have to use the temperature field indirectly,
through a method:
// In source packet in file init/ex15/Liquid.java
class Liquid {
private int mlVolume;
private float temperature; // in Celsius
public Liquid() {
mlVolume = 300;
temperature = (float) (Math.random() * 100.0);
}
public float getTemperature() {
return temperature;
}
// Has several other methods, not shown...
}
// In source packet in file init/ex15/Coffee.java
class Coffee extends Liquid {
private static final float BOILING_POINT = 100.0f; // Celsius
private boolean swirling;
private boolean clockwise;
public Coffee(boolean swirling, boolean clockwise) {
if (getTemperature() >= BOILING_POINT) {
// Leave swirling at default value: false
return;
}
this.swirling = swirling;
if (swirling) {
this.clockwise = clockwise;
} // else, leave clockwise at default value: false
}
// Has several methods, not shown,
// but doesn't override getTemperature()...
}
In the example, the constructor for Coffee invokes
getTemperature() and uses the return value in the
calculation of the proper initial value of swirling and
clockwise. getTemperature() returns the
value of the temperature variable; thus, the constructor
for Coffee uses a field declared in Liquid.
This works because, by the time the code inside Coffee's
constructor is executed, the instance variables declared in
Liquid are guaranteed to have already been initialized to
their proper starting values.
|
Sponsored Links
|