The Artima Developer Community
Sponsored Link

Objects and Java by Bill Venners
Chapter 6:
Composition and Inheritance

Advertisement

Modeling the relationships between types is a fundamental part of the process of object-oriented design. This chapter shows you how to model relationships using composition and inheritance. It describes many facets of inheritance in Java, including abstract classes and final classes. [bv:need better intro]

Composition

As you progress in an object-oriented design, you will likely encounter objects in the problem domain that contain other objects. In this situation you will be drawn to modeling a similar arrangement in the design of your solution. In an object-oriented design of a Java program, the way in which you model objects that contain other objects is with composition, the act of composing a class out of references to other objects. With composition, references to the constituent objects become fields of the containing object.

For example, it might be useful if the coffee cup object of your program could contain coffee. Coffee itself could be a distinct class, which your program could instantiate. You would award coffee with a type if it exhibits behavior that is important to your solution. Perhaps it will swirl one way or another when stirred, keep track of a temperature that changes over time, or keep track of the proportions of coffee and any additives such as cream and sugar.

To use composition in Java, you use instance variables of one object to hold references to other objects. For the CoffeeCup example, you could create a field for coffee within the definition of class CoffeeCup, as shown below: [bv: implement the methods]

// In Source Packet in file inherit/ex1/CoffeeCup.java
class CoffeeCup {

    private Coffee innerCoffee;

    public void addCoffee(Coffee newCoffee) {
        // no implementation yet
    }

    public Coffee releaseOneSip(int sipSize) {
        // no implementation yet
        // (need a return so it will compile)
        return null;
    }

    public Coffee spillEntireContents() {
        // no implementation yet
        // (need a return so it will compile)
        return null;
    }
}

// In Source Packet in file inherit/ex1/Coffee.java
public class Coffee {

    private int mlCoffee;

    public void add(int amount) {
        // No implementation yet
    }

    public int remove(int amount) {
        // No implementation yet
        // (return 0 so it will compile)
        return 0;
    }

    public int removeAll() {
        // No implementation yet
        // (return 0 so it will compile)
        return 0;
    }
}

In the example above, the CoffeeCup class contains a reference to one other object, an object of type Coffee. Class Coffee is defined is a separate source file.

The relationship modeled by composition is often referred to as the "has-a" relationship. In this case a CoffeeCup has Coffee. As you can see from this example, the has-a relationship doesn't mean that the containing object must have a constituent object at all times, but that the containing object may have a constituent object at some time. Therefore the CoffeeCup may at some time contain Coffee, but it need not contain Coffee all the time. (When a CoffeeCup object doesn't contain Coffee, its innerCoffee field is null.) In addition, note that the object contained can change throughout the course of the containing object's life.

[bv: need to add UML diagram for composition, and explain the difference between composition and agregation and why I draw my diagrams like I do.]

Inheritance

As you partition your problem domain into types you will likely want to model relationships in which one type is a more specific or specialized version of another. For example you may have identified in your problem domain two types, Cup and CoffeeCup, and you want to be able to express in your solution that a CoffeeCup is a more specific kind of Cup (or a special kind of Cup). In an object-oriented design, you model this kind of relationship between types with inheritance.

Building Inheritance Hierarchies

The relationship modeled by inheritance is often referred to as the "is-a" relationship. In the case of Cup and CoffeeCup, a "CoffeeCup is-a Cup." Inheritance allows you to build hierarchies of classes, such as the one shown in Figure 5-1. The upside-down tree structure shown in Figure 5-1 is an example of an inheritance hierarchy displayed in UML form. Note that the classes become increasingly more specific as you traverse down the tree. A CoffeeCup is a more specific kind of Cup. A CoffeeMug is a more specific kind of CoffeeCup. Note also that the is-a relationship holds even for classes that are connected in the tree through other classes. For instance, a CoffeeMug is not only more specific version of a CoffeeCup, it is also a more specific version of a Cup. Therefore, the is-a relationship exists between CoffeeMug and Cup: a CoffeeMug is-a Cup.


Figure 5-1. The is-a relationship of inheritance

[bv: mention this is a UML diagram]

When programming in Java, you express the inheritance relationship with the extends keyword:

class Cup {
}
class CoffeeCup extends Cup {
}
class CoffeeMug extends CoffeeCup {
}

In Java terminology, a more general class in an inheritance hierarchy is called a superclass. A more specific class is a subclass. In Figure 5-1, Cup is a superclass of both CoffeeCup and CoffeeMug. Going in the opposite direction, both CoffeeMug and CoffeeCup are subclasses of Cup. When two classes are right next to each other in the inheritance hierarchy, their relationship is said to be direct. For example Cup is a direct superclass of CoffeeCup, and CoffeeMug is a direct subclass of CoffeeCup.

The act of declaring a direct subclass is referred to in Java circles as class extension. For example, a Java guru might be overheard saying, "Class CoffeeCup extends class Cup." Owing to the flexibility of the English language, Java in-the-knows may also employ the term "subclass" as a verb, as in "Class CoffeeCup subclasses class Cup." One other way to say the same thing is, "Class CoffeeCup descends from class Cup."

An inheritance hierarchy, such as the one shown in Figure 5-1, defines a family of types. The most general class in a family of types--the one at the root of the inheritance hierarchy--is called the base class. In Figure 5-1, the base class is Cup. Because every class defines a new type, you can use the word "type" in many places you can use "class." For example, a base class is a base type, a subclass is a subtype, and a direct superclass is a direct supertype.

In Java, every class descends from one common base class: Object. The declaration of class Cup above could have been written:

class Cup extends Object { // "extends Object" is optional
}

This declaration of Cup has the same effect as the earlier one that excluded the "extends Object" clause. If a class is declared with no extends clause, it by default extends the Object class. (The only exception to this rule is class Object itself, which has no superclass.) The inheritance hierarchy of Figure 5-1 could also have shown the Object class hovering above the Cup class, in its rightful place as the most super of all superclasses. In this case, class Object remained invisible, because the purpose of the figure was to focus on one particular family of types, the Cup family.

In Java, a class can have only one direct superclass. In object-oriented parlance, this is referred to as single inheritance . It contrasts with multiple inheritance , in which a class can have multiple direct superclasses. Although Java only supports single inheritance of classes through class extension, it supports a special variant of multiple inheritance through "interface implementation." Java interfaces, and how a class implements them, will be discussed in Chapter 7.

Inheriting interface and implementation

Modeling an is-a relationship is called inheritance because the subclass inherits the interface and, by default, the implementation of the superclass. Inheritance of interface guarantees that a subclass can accept all the same messages as its superclass. A subclass object can, in fact, be used anywhere a superclass object is called for. For example, a CoffeeCup as defined in Figure 5-1 can be used anywhere a Cup is needed. This substitutability of a subclass (a more specific type) for a superclass (a more general type) works because the subclass accepts all the same messages as the superclass. In a Java program, this means you can invoke on a subclass object any method you can invoke on the superclass object.

This is only half of the inheritance story, however, because by default, a subclass also inherits the entire implementation of the superclass. This means that not only does a subclass accept the same messages as its direct superclass, but by default it behaves identically to its direct superclass when it receives one of those messages. Yet unlike inheritance of interface, which is certain, inheritance of implementation is optional. For each method inherited from a superclass, a subclass may choose to adopt the inherited implementation, or to override it. To override a method, the subclass merely implements its own version of the method.

Overiding methods is a primary way a subclass specializes its behavior with respect to its superclass. A subclass has one other way to specialize besides overriding the implementation of methods that exist in its direct superclass. It can also extend the superclass's interface by adding new methods. This possibility will be discussed in detail later in the next chapter.

Suppose there is a method in class Cup with the following signature:

public void addLiquid(Liquid liq) {
}
The addLiquid() method could be invoked on any Cup object. Because CoffeeCup descends from Cup, the addLiquid() method could also be invoked on any CoffeeCup object.

If you do not explicitly define in class CoffeeCup a method with an identical signature and return type as the addLiquid() method shown above, your CoffeeCup class will inherit the same implementation (the same body of code) used by superclass Cup. If, however, you do define in CoffeeCup an addLiquid() method with the same signature and return type, that implementation overrides the implementation that would otherwise have been inherited by default from Cup.

When you override a method, you can make the access permission more public, but you cannot make it less public. So far, you have only been introduced to two access levels, public and private. There are, however, two other access levels that sit in-between public and private, which form the two ends of the access-level spectrum. (All four access levels will be discussed together in Chapter 8.). In the case of the addLiquid() method, because class Cup declares it with public access, class CoffeeCup must declare it public also. If CoffeeCup attempted to override addLiquid() with any other access level, class CoffeeCup wouldn't compile.

For an illustration of the difference between inheriting and overriding the implementation of a method, see Figure 5-2. The left side of this figure shows an example of inheriting an implementation, whereas the right side shows an example of overriding the implementation.

The method in question is the familiar addLiquid() method. In the superclass, Cup, a comment indicates that the code of the method, which is not shown in the figure, will cause the liquid to swirl clockwise as it is added to the cup. Liquid added to an instance of the CoffeeCup class defined on the left will also swirl clockwise, because that CoffeeCup inherits Cup's implementation of addLiquid(), which swirls clockwise. By contrast, liquid added to an instance of the CoffeeCup class defined on the right will swirl counterclockwise, because this CoffeeCup class overrides Cup's implementation with one of its own. A more advanced CoffeeCup could override addLiquid() with an implementation that first checks to see whether the coffee cup is in the northern or southern hemisphere of the planet, and based on that information, decide which way to swirl.


Figure 5-2. Inheriting vs. overriding the implementation of a method

In addition to the bodies of public methods, the implementation of a class includes any private methods and any fields defined in the class. Using the official Java meaning of the term "inherit," a subclass does not inherit private members of its superclass. It only inherits accessible members. Well- designed classes most often refuse other classes direct access to their non-constant fields, and this policy generally extends to subclasses as well. If a superclass has private fields, those fields will be part of the object data in its subclasses, but they will not be "inherited" by the subclass. Methods defined in the subclasses will not be able to directly access them. Subclasses, just like any other class, will have to access the superclass's private fields indirectly, through the superclass's methods.

Hiding Fields

If you define a field in a subclass that has the same name as an accessible field in its superclass, the subclass's field hides the superclass's version. (The type of the variables need not match, just the names.) For example, if a superclass declares a public field, subclasses will either inherit or hide it. (You can't override a field.) If a subclass hides a field, the superclass's version is still part of the subclass's object data; however, the subclass doesn't "inherit" the superclass's version of the field, because methods in the subclass can't access the superclass's version of the field by its simple name. They can only access the subclass's version of the field by its simple name. You can access the superclass's version by qualifying the simple name with the super keyword, as in super.fieldName. (More on super in the next section.)

Java permits you to declare a field in a subclass with the same name as a field in a superclass so you can add fields to a class without worrying about breaking compatibility with already existing subclasses. For example, you may publish a library of classes that your customers can use in their programs. If your customers subclass the classes in your library, you will likely have no idea what new fields they have declared in their subclasses. In making enhancements to your library, you may inadvertently add a field that has the same name as a field in one of your customer's subclasses. If Java didn't permit field hiding, the next time you released your library, your customer's program might not run properly, because the like- named field in the subclass would clash with the new field in the superclass from your library. Java's willingness to tolerate hidden fields makes subclasses more accepting of changes in their superclasses.

[bv: See Behind the Scenes in this chapter for a description of object images on a JVM heap?]

Abstract Classes and Methods

As you perform an object-oriented design, you may come across classes of objects that you would never want to instantiate. Those classes will nevertheless occupy a place in your hierarchies. An example of such a class might be the Liquid class from the previous discussions. Class Liquid served as a base class for the family of types that included subclasses Coffee, Milk, and Tea. While you can picture a customer walking into a caf� and ordering a coffee, a milk, or a tea, you might find it unlikely that a customer would come in and order a "liquid." You might also find it difficult to imagine how you would serve a "liquid." What would it look like? How would it taste? How would it swirl or gurgle?

Java provides a way to declare a class as conceptual only, not one that represents actual objects, but one that represents a category of types. Such classes are called abstract classes. To mark a class as abstract in Java, you merely declare it with the abstract keyword. The abstract keyword indicates the class should not be instantiated. Neither the Java compiler nor the Java Virtual Machine will allow an abstract class to be instantiated. The syntax is straightforward:

// In Source Packet in file inherit/ex6/Liquid.java
abstract class Liquid {

    void swirl(boolean clockwise) {
        System.out.println("One Liquid object is swirling.");
    }

    static void gurgle() {
        System.out.println("All Liquid objects are gurgling.");
    }
}

The above code makes Liquid a place holder in the family tree, unable to be an object in its own right.

Note that the Liquid class shown above still intends to implement a default behavior for swirling and gurgling. This is perfectly fine, however, classes are often made abstract when it doesn't make sense to implement all of the methods of the class's interface. The abstract keyword can be used on methods as well as classes, to indicate the method is part of the interface of the class, but does not have any implementation in that class. Any class with one or more abstract methods is itself abstract and must be declared as such. In the Liquid class, you may decide that there is no such thing as a default swirling behavior that all liquids share. If so, you can declare the swirl() method abstract and forgo an implementation, as shown below:

// In Source Packet in file inherit/ex7/Liquid.java
abstract class Liquid {

    abstract void swirl(boolean clockwise);

    static void gurgle() {
        System.out.println("All Liquid objects are gurgling.");
    }
}

In the above declaration of Liquid, the swirl() method is part of Liquid's interface, but doesn't have an implementation. Any subclasses that descend from the Liquid class shown above will have to either implement swirl() or declare themselves abstract. For example, if you decided there were so many varieties of coffee that there is no sensible default implementation for Coffee, you could neglect to implement swirl() in Coffee. In that case, however, you would need to declare Coffee abstract. If you didn't, you would get a compiler error when you attempted to compile the Coffee class. You would have to subclass Coffee (for example: Latte, Espresso, CafeAuLait) and implement swirl() in the subclasses, if you wanted the Coffee type to ever see any action.

Most often you will place abstract classes at the upper regions of your inheritance hierarchy, and non- abstract classes at the bottom. Nevertheless, Java does allow you to declare an abstract subclass of a non- abstract superclass. For example, you can declare a method inherited from a non-abstract superclass as abstract in the subclass, thereby rendering the method abstract at that point in the inheritance hierarchy. This design implies that the default implementation of the method is not applicable to that section of the hierarchy. As long as you implement the method again further down the hierarchy, this design would yield an abstract class sandwiched in the inheritance hierarchy between a non-abstract superclass and non- abstract subclasses.

Final Classes and Methods

Most Java programmers have two hats on their shelf, both of which they wear at different times. Sometimes they wear their "designer" hat, and build libraries of classes for others to use. Other times they wear their "client" hat, and make use of a library of classes created by someone else. Some Java programmers even wear both hats at the same time, completely oblivious to the rules of fashion.

When you put on your "designer" hat and work to build a library of classes that will be distributed to people you don't know and don't necessarily trust, you will likely encounter situations in which you want to prevent a client from declaring a subclass of one of the classes in your library. Or you might want to allow a client to declare a subclass, but you want to prevent them from overriding specific methods of the superclass. The reason you'll feel the need for this kind of control is that a client could take advantage of polymorphism to effectively change the behavior of the classes in your library. For example, a swirl() method of a hot beverage object could be redefined to swirl right out of the cup and dampen or possibly even scald a customer. Fortunately, Java gives you the final keyword to prevent just such nightmarish scenarios as that.

If you declare a method final, no subclass will be allowed to override that method. If you declare an entire class final, no other class will be allowed to extend it. In other words, a class declared final cannot be subclassed. In an inheritance diagram, a final class is the end of the line. No other classes will appear below it. Subclasses can appear below a non-final class that contains a final method, but every subclass will inherit the final implementation of the method.

Because marking a class or method final is so restrictive to clients of the class, you should use it with caution. Only if you are certain you want to absolutely prevent clients from declaring a subclass or overriding a method should you use the final keyword on a class or method.

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 4-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 4-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:

  1. Object's fields (this will be quick, because there are none)
  2. Liquid's fields (mlVolume and temperature)
  3. 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.

Design Corner

Composition versus Inheritance

Behind the Scenes

[bv: want to have description of object image on the JVM heap here?]

[bv: want to mention that Object can be redefined?]

The structure of <init>
How does Java ensure the correct ordering of initialization? By the manner in which the Java compiler generates the instance initialization method. Into each <init> method, the compiler can place three kinds of code:

  1. An invocation of another constructor
  2. Initializers in textual order
  3. The constructor body

The order in which the compiler places these components into the <init> method determines the order of initialization of an object's fields.

(Almost) every constructor's first act
For every class except Object, the first thing each <init> method will do is invoke another constructor. If you included a this() invocation as the first statement in a constructor, the corresponding <init> method will start by calling another <init> method of the same class. For example, for the following class:

// In source packet in file init/ex4/CoffeeCup.java
class CoffeeCup {

    private int innerCoffee;

    public CoffeeCup() {
        this(237); // Calls other constructor
        // Could have done more construction here
    }

    public CoffeeCup(int amount) {
        innerCoffee = amount;
    }
    // ...
}

The <init> method for the no-arg constructor would first invoke the <init> method for the constructor, which takes an int parameter, passing it 237.

Automatic invocation of super()
For any class except class java.lang.Object, if you write a constructor that does not begin with a this() invocation, the <init> method for that constructor will begin with an invocation of a superclass constructor. You can explicitly invoke a superclass constructor using the super() statement. If you don't, the compiler will automatically generate an invocation of the superclass's no-arg constructor. (This is true for default constructors as well. With the exception of class Object, the <init> method for any default constructor will do only one thing: invoke the <init> method for the superclass's no-arg constructor.) For example, given this CoffeeCup constructor from the example above:

public CoffeeCup(int amount) {
    innerCoffee = amount;
}

The corresponding <init> method would begin with an invocation of the <init> method for Liquid's (the direct superclass's) no-arg constructor.

Alternatively, you could have included an explicit super() statement at the top of the Coffee constructor, as in:

public CoffeeCup(int amount) {
    super();
    innerCoffee = amount;
}

This version has the same effect as the previous version. If you want to invoke the superclass's no-arg constructor, you needn't provide an explicit super() invocation. The compiler will generate a no-arg super() invocation for you.

Invoking super() with arguments
If, on the other hand, you want to invoke a superclass constructor that takes parameters, you must provide an explicit super() invocation. Here's an example:

// In source packet in file init/ex16/Liquid.java
class Liquid {

    private int mlVolume;
    private float temperature; // in Celsius

    public Liquid(int mlVolume, float temperature) {
        this.mlVolume = mlVolume;
        this.temperature = temperature;
    }

    public float getTemperature() {
        return temperature;
    }
    // Has several other methods, not shown,
    // but doesn't include another constructor...
}

// In source packet in file init/ex16/Coffee.java
public class Coffee extends Liquid {

    private static final float BOILING_POINT = 100.0f; // Celsius
    private boolean swirling;
    private boolean clockwise;

    public Coffee(int mlVolume, float temperature,
        boolean swirling, boolean clockwise) {

        super(mlVolume, temperature);
        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 this example, Coffee's constructor explicitly invokes Liquid's constructor with a super() statement. Because class Liquid explicitly declares a constructor, the Java compiler won't generate a default constructor. Moreover, because Liquid doesn't explicitly declare a no-arg constructor, class Liquid won't have a no-arg constructor at all. For this reason, had Coffee's constructor not started with an explicit super() invocation, class Coffee would not have compiled. (Given this declaration of class Liquid, a simple new Liquid() statement would not compile either. You must invoke the constructor that is available to you, as in: new Liquid(25, 50.0).) If a subclass's direct superclass does not offer a no-arg constructor, every constructor in that subclass must begin with either an explicit super() or this()invocation.

Only one constructor invocation allowed
Note that you can't have both this() and super() in the same constructor. You can only have one or the other (or neither, if the direct superclass includes a no-arg constructor). If a constructor includes a this() or super() invocation, it must be the first statement in the constructor.

Catching exceptions not allowed
One other rule enforced on constructors is that you can't catch any exceptions thrown by the constructor invoked with this() or super(). To do so, you would have to begin your constructor with a try statement:

// In source packet in file init/ex17/Coffee.java
// THIS WON'T COMPILE, BECAUSE THE super() INVOCATION
// DOESN'T COME FIRST IN THE CONSTRUCTOR
class Coffee extends Liquid {
    //...
    public Coffee(int mlVolume, float temperature,
        boolean swirling, boolean clockwise) {

        try {
            super(mlVolume, temperature);
        }
        catch (Throwable e) {
            //...
        }
        //...
    }
    //...
}

The point to understand here is that if any instance initialization method completes abruptly by throwing an exception, initialization of the object fails. This in turn means that object creation fails, because in Java programs, objects must be properly initialized before they are used.

The proper way to signal that an error occurred during object initialization is by throwing an exception. If an <init> method throws an exception, it is likely that at least some of the fields that <init> method normally takes responsibility for did not get properly initialized. If you were able to catch an exception thrown by an <init> method you invoked with this() or super(), you could ignore the exception and complete normally. This could result in an improperly or incompletely initialized object being returned by new. This is why catching exceptions thrown by <init> methods invoked via this() or super() is not allowed.

Inheritance and initialization order
From the many rules that surround the invocation of instance initialization methods via this() or super(), there arises a clear and certain order for instance variable initialization. Although <init> methods are called in an order starting from the object's class and proceeding up the inheritance path to class Object, instance variables are initialized in the reverse order. Instance variables are initialized in an order starting from class Object and proceeding down the inheritance path to the object's class. The reason the order of instance variable initialization is reverse to that of <init> method invocation is that the first thing each <init> method (except Object's) does is call another <init> method. So the superclass <init> method is invoked and completes before any initialization code of the current class's <init> method begins execution.

As an example of this ordering, consider again the inheritance hierarchy for class Coffee as shown in Figure 1 and the following implementation of those classes:

// In source packet in file init/ex18/Liquid.java
class Liquid {

    private int mlVolume;
    private float temperature; // in Celsius

    Liquid(int mlVolume, float temperature) {
        this.mlVolume = mlVolume;
        this.temperature = temperature;
    }
    //...
}

// In source packet in file init/ex18/Coffee.java
class Coffee extends Liquid {

    private boolean swirling;
    private boolean clockwise;

    public Coffee(int mlVolume, float temperature,
        boolean swirling, boolean clockwise) {

        super(mlVolume, temperature);
        this.swirling = swirling;
        this.clockwise = clockwise;
    }
    //...
}

When you instantiate a new Coffee object with the new operator, the Java virtual machine first will allocate (at least) enough space on the heap to hold all the instance variables declared in Coffee and its superclasses. Second, the virtual machine will initialize all the instance variables to their default initial values. Third, the virtual machine will invoke the <init> method in the Coffee class.

The first thing Coffee's <init> method will do is invoke the <init> method in its direct superclass, Liquid. The first thing Liquid's <init> method will do is invoke the no-arg <init> method in its direct superclass, Object. Object's <init> method most likely will do nothing but return, because it has no instance variables to initialize. (Once again, what Object's <init> method actually does is an implementation detail of each particular Java runtime environment.) When Object's <init> method returns, Liquids <init> method will initialize mlVolume and temperature to their proper starting values and return. When Liquids <init> method returns, Coffee's <init> method will initialize swirling and clockwise to their proper starting values and return. Upon normal completion of Coffee's <init> method (in other words, so long as it doesn't complete abruptly by throwing an exception), the JVM will return the reference to the new Coffee object as the result of the new operator.

this() won't change the order of initialization
Note that if an <init> method begins not by invoking a superclass's <init> method (a super() invocation), but instead by invoking another <init> method from the same class (a this() invocation), the order of instance variable initialization remains the same. You can have several this() invocations in a row if you wish. In other words, you could have an <init> method that invokes another with this(), and that <init> method invokes yet another with this(), and so on. But in the end, there will always be an <init> method with a super() invocation -- either an explicit super() invocation or a compiler-generated one. Since this() and super() are both always the first action a constructor takes, the instance variables will always be initialized in order from the base class on down.

In addition to the code for constructor invocations and constructor bodies, the Java compiler also places code for any initializers in the <init> method. If a class includes initializers, the code for them will be placed after the superclass method invocation but before the code for the constructor body, in every <init> method that begins with an explicit or implicit super() invocation. Code for initializers are not included as part of <init> methods that begin with a this() invocation. Because initializer code appears only in <init> methods that begin with a super() invocation, and not in those that begin with a this() invocation, the initializers for a class are guaranteed to be run only once for each new class creation. Because initializers appear after the super() invocation and before the code from the constructor's body, you can always be certain that initializers will have been run by the time any constructor code for that class is executed.

Calling subclassed methods from constructors
The strict ordering of instance variable initialization enforced by the Java compiler is, in part, an effort to ensure that during the initialization process, instance variables are never used before they have been initialized to their proper initial values. As illustrated earlier in this article, however, the rules of ordering are not bulletproof. There are ways you can use an instance variable during initialization before it has been initialized to its proper value, while it still has its default value. In the case of instance variable initializers, you can invoke a method that uses a variable declared textually after the variable being initialized. Another way to use an instance variable before it has been properly initialized is to invoke a method from a superclass initializer or constructor that uses instance variables in a subclass.

Unlike C++, which treats the invocation of virtual functions from constructors specially, Java methods invoked from <init> methods behave the same as if they were invoked from any method. If <init> in a superclass invokes a method that has been overridden in a subclass, the subclass's implementation of that method will run. If the subclass's method implementation uses instance variables explicitly declared in the subclass, those variables will still have their default initial values.

You should be careful when you invoke methods from initializers or constructors, because you can end up using instance variables before they've been properly initialized -- while they still have their default initial values. It is fine to use variables while they still have their default initial values, so long as it is the result you are aiming for. If you invoke non-private methods from initializers and constructors, remember that later some other programmer could come along, extend your class, and override those methods, thereby thwarting your grand initialization scheme.

Example Programs

Could perhaps show how class vars can be used to keep track of all the instances of the class and then how a gurgleAllObjects() class method can send gurgle() methods to all objects.

On the CD-ROM

The CD-ROM contains several examples from this chapter, all of which are in subdirectories of the inherit directory. The files for example one are in the ex1 subdirectory, the files for example two are in ex2, and so on.

Example one is simply the CoffeeCup and Coffee classes, shown above, that illustrate composition. In this version of CoffeeCup, the innerCoffee instance variable is a reference to an object of type Coffee. The files are in the inherit/ex1 directory.

Example two is the polymorphism example. All of the code for this example is shown above as part of the text of this chapter. The files are in the inherit/ex2 directory. In this example, the addLiquid() method of class CoffeeCup use polymorphism to call the appropriate swirl() method an object that either is or descends from class Liquid. If you execute the Java application, Example2, it will print out the output:

Liquid Swirling
Coffee Swirling
Milk Swirling

Example three is an example of poor design that doesn't take advantage of polymorphism. Only the UglyCoffeeCup class from this example is shown in the text of this chapter. (The rest aren't shown because this example doesn't provide a positive role model.) All the files exist in the inherit/ex3 directory of the CD-ROM, however, so you can run the application Example3. When you run Example3, you get the same output as Example2 gives you. The example works, it just doesn't take advantage of polymorphism. Try to avoid this style of program design.

Example four illustrates the difference between static and dynamic binding. The code for all the files in this example, which are shown above as part of the text of this chapter. The files are in the inherit/ex4 directory. You can see the different between static and dynamic binding by running Example4a, which doesn't yield the desired behavior of gurgling all milk objects. Example4b shows one way to gurgle milk, however, the preferred way to gurgle milk is shown in Example4c. When you run Example4c, it will print out:

One Milk object is swirling.
All Milk objects are gurgling.

Example five illustrates adding behavior to one member of a family of types, and using instanceof to access that behavior. All of the code in this example is shown above in the text of this chapter. The files are in the inherit/ex5 directory of the CD- ROM. In this example, two of the source files, Example5a.java and Example5c.java, don't compile. These files illustrate that you can't access a method defined in a subclass, Tea, if you have a reference to a superclass, Liquid. You can, however, run Example5b and Example5d. When you run Example5d, which illustrates the proper way to access the readFuture() method defined in subclass Tea, the application will print out:

Tea Swirling
Reading the future...

Examples six and seven are simply the two Liquid classes, shown above, that illustrate abstract classes and methods. In example six, which is in the inherit/ex6 directory, class Liquid is declared abstract even though it doesn't contain any abstract methods. In example seven, which is in the inherit/ex7 directory, both the swirl() method and the Liquid class itself are declared abstract.


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use