7 Object-Oriented Programming

7.1 Single Implementation Inheritance

Inheritance is one of the fundamental mechanisms for code reuse in OOP. It allows new classes to be derived from an existing class. The new class (also called subclass, subtype, derived class, child class) can inherit members from the old class (also called superclass, supertype, base class, parent class). The subclass can add new behavior and properties and, under certain circumstances, modify its inherited behavior.

In Java, implementation inheritance is achieved by extending classes (i.e., adding new fields and methods) and modifying inherited members (see Section 7.2, p. 288). Inheritance of members is closely tied to their declared accessibility. If a superclass member is accessible by its simple name in the subclass (without the use of any extra syntax like super), that member is considered inherited. This means that private, overridden, and hidden members of the superclass are not inherited (see Section 7.2, p. 288). Inheritance should not be confused with the existence of such members in the state of a subclass object (see Example 7.1).

The superclass is specified using the extends clause in the header of the subclass declaration. The subclass only specifies the additional new and modified members in its class body. The rest of its declaration is made up of its inherited members. If no extends clause is specified in the header of a class declaration, the class implicitly inherits from the java.lang.Object class (see Section 10.2, p. 424). This implicit inheritance is assumed in the declaration of the Light class at (1) in Example 7.1. Also in Example 7.1, the subclass TubeLight at (2) explicitly uses the extends clause and only specifies additional members to what it already inherits from the superclass Light (which, in turn, inherits from the Object class). Members of the superclass Light that are accessible by their simple names in the subclass TubeLight, are inherited by the subclass.

Private members of the superclass are not inherited by the subclass and can only be indirectly accessed. The private field indicator of the superclass Light is not inherited, but exists in the subclass object and is indirectly accessible.

Using appropriate accessibility modifiers, the superclass can limit which members can be accessed directly and, thereby, inherited by its subclasses (see Section 4.9, p. 138). As shown in Example 7.1, the subclass can use the inherited members as if they were declared in its own class body. This is not the case for members that are declared private in the superclass. Members that have package accessibility in the superclass are also not inherited by subclasses in other packages, as these members are accessible by their simple names only in subclasses within the same package as the superclass.

Since constructors (see Section 7.5, p. 302) and initializer blocks (see Section 9.7, p. 406) are not members of a class, they are not inherited by a subclass.

Extending generic classes is discussed in Section 14.2, p. 668.

Example 7.1 Extending Classes: Inheritance and Accessibility

class Light {                           // (1)
  // Instance fields:
            int     noOfWatts;          // wattage
  private   boolean indicator;          // on or off
  protected String  location;           // placement

  // Static field:
  private static int counter;           // no. of Light objects created

  // Constructor:
  Light() {
    noOfWatts = 50;
    indicator = true;
    location  = "X";
    counter++;
  }

  // Instance methods:
  public  void    switchOn()  { indicator = true; }
  public  void    switchOff() { indicator = false; }
  public  boolean isOn()      { return indicator; }
  private void    printLocation() {
    System.out.println("Location: " + location);
  }

  // Static methods:
  public static void writeCount() {
    System.out.println("Number of lights: " + counter);
  }
  //...
}
//____________________________________________________
class TubeLight extends Light {         // (2) Subclass uses the extends clause.
  // Instance fields:
  private int tubeLength = 54;
  private int colorNo    = 10;

  // Instance methods:
  public int getTubeLength() { return tubeLength; }

  public void printInfo() {
    System.out.println("Tube length: "  + getTubeLength());

    System.out.println("Color number: " + colorNo);
    System.out.println("Wattage: "      + noOfWatts);     // Inherited.
//  System.out.println("Indicator: "    + indicator);     // Not Inherited.
    System.out.println("Indicator: "    + isOn());        // Inherited.
    System.out.println("Location: "     + location);      // Inherited.
//  printLocation();                                      // Not Inherited.
//  System.out.println("Counter: "   + counter);          // Not Inherited.
    writeCount();                                         // Inherited.
  }
  // ...
}
//____________________________________________________
public class Utility {                  // (3)
  public static void main(String[] args) {
    new TubeLight().printInfo();
  }
}

Output from the program:

Tube length: 54
Color number: 10
Wattage: 50
Indicator: true
Location: X
Number of lights: 1

Inheritance Hierarchy

In Java, a class can only extend one other class; i.e., it can only have one immediate superclass. This kind of inheritance is sometimes called single or linear implementation inheritance. The name is appropriate, as the subclass inherits the implementations of its superclass members. The inheritance relationship can be depicted as an inheritance hierarchy(also called class hierarchy). Classes higher up in the hierarchy are more generalized, as they abstract the class behavior. Classes lower down in the hierarchy are more specialized, as they customize the inherited behavior by additional properties and behavior. Figure 7.1 illustrates the inheritance relationship between the class Light, which represents the more general abstraction, and its more specialized subclasses. The java.lang.Object class is always at the top of any Java inheritance hierarchy, as all classes, with the exception of the Object class itself, inherit (either directly or indirectly) from this class.

Figure 7.1 Inheritance Hierarchy

Inheritance Hierarchy

Relationships: is-a and has-a

Inheritance defines the relationship is-a (also called the superclass–subclass relationship) between a superclass and its subclasses. This means that an object of a subclass is-a superclass object, and can be used wherever an object of the superclass can be used. This is often employed as a litmus test for choosing inheritance in object-oriented design. It has particular consequences on how objects can be used.

An object of the TubeLight class is-an object of the superclass Light. Referring to Figure 7.1, an object of the TubeLight class can be used wherever an object of the superclass Light can be used. The inheritance relationship is transitive: if class B extends class A, then a class C, which extends class B, will also inherit from class A via class B. An object of the SpotLightBulb class is-an object of the class Light. The is-a relationship does not hold between peer classes: an object of the LightBulb class is not an object of the class TubeLight and vice versa.

Whereas inheritance defines the relationship is-a between a superclass and its subclasses, aggregation defines the relationship has-a (also called the whole–part relationship) between an instance of a class and its constituents (also called parts). Aggregation comprises the usage of objects. An instance of class Light has (or uses) the following parts: a field to store its wattage (noOfWatts), a field to store whether it is on or off (indicator), and a String object to store its location (denoted by the field reference location). In Java, a composite object cannot contain other objects. It can only store reference values of its constituent objects in its fields. This relationship defines an aggregation hierarchy (also called object hierarchy) that embodies the has-a relationship. Constituent objects can be shared between objects, and their lifetimes can be dependent or independent of the lifetime of the composite object. Inheritance and aggregation are compared in Section 7.13, p. 342.

The Supertype-Subtype Relationship

A class defines a reference type. Therefore the inheritance hierarchy can be regarded as a type hierarchy, embodying the supertype-subtype relationship between reference types. In the context of Java, the supertype-subtype relationship implies that the reference value of a subtype object can be assigned to a supertype reference, because a subtype object can be substituted for a supertype object. This assignment involves a widening reference conversion (see Section 5.1, p. 161), as references are assigned up the inheritance hierarchy. Using the reference types in Example 7.1, the following code assigns the reference value of an object of the subtype TubeLight to the reference light of the supertype Light:

Light light = new TubeLight();               // (1) widening reference conversion

We can now use the reference light to invoke those methods on the subtype object that are inherited from the supertype Light:

light.switchOn();                            // (2)

Note that the compiler only knows about the declared type of the reference light, which is Light, and ensures that only methods from this type can be called using the reference light. However, at runtime, the reference light will refer to an object of the subtype TubeLight when the call to the method switchOn() is executed. It is the type of the object that the reference is referring to at runtime that determines which method is executed. The subtype object inherits the switchOn() method from its supertype Light, and it is this method that is executed. The type of the object that the reference refers to at runtime is often called the dynamic type of the reference.

One might be tempted to invoke methods exclusive to the TubeLight subtype via the supertype reference light:

light.getTubeLength();                       // (3) Not OK.

However, this will not work, as the compiler does not know what object the reference light is denoting. It only knows the declared type of the reference. As the declaration of the class Light does not have a method called getTubeLength(), this method call at (3) results in a compile-time error. As we shall see later in this chapter, eliciting subtype-specific behavior using a supertype reference requires a narrowing reference conversion with an explicit cast (Section 7.11, p. 327).

The rest of this chapter will elaborate on various aspects of OOP, and understanding them is founded in understanding the consequences of the subtype-supertype relationship.

7.2 Overriding Methods

Instance Method Overriding

Under certain circumstances, a subclass may override instance methods that it would otherwise inherit from a superclass. Overriding such a method allows the subclass to provide its own implementation of the method. When the method is invoked on an object of the subclass, it is the method implementation in the subclass that is executed. The overridden method in the superclass is not inherited by the subclass, and the new method in the subclass must abide by the following rules of method overriding:

The new method definition must have the same method signature, i.e., the method name, and the types and the number of parameters, including their order, are the same as in the overridden method.

Whether parameters in the overriding method should be final is at the discretion of the subclass (see Section 3.9, p. 95). A method’s signature does not comprise the final modifier of parameters, only their types and order.

• The return type of the overriding method can be a subtype of the return type of the overridden method (called covariant return).

• The new method definition cannot narrow the accessibility of the method, but it can widen it (see Section 4.9, p. 138).

• The new method definition can only throw all or none, or a subset of the checked exceptions (including their subclasses) that are specified in the throws clause of the overridden method in the superclass (see Section 6.9, p. 259).

These requirements also apply to interfaces, where a subinterface can override abstract method declarations from its superinterfaces (see Section 7.6, p. 309). The implications of generics on overriding methods is discussed in Section 14.12, p. 718.

In Example 7.2, the new definition of the getBill() method at (6) in the subclass TubeLight has the same signature and the same return type as the method at (2) in the superclass Light. The new definition specifies a subset of the exceptions (ZeroHoursException) thrown by the overridden method (the exception class Invalid HoursException is a superclass of NegativeHoursException and ZeroHoursException). The new definition also widens the accessibility (public) from what it was in the overridden definition (protected). The overriding method also declares the parameter to be final, but this has no bearing in overriding the method.

The astute reader will have noticed the @Override annotation preceding the method definition at (6). The compiler will now report an error if the method definition does not override an inherited method. The annotation helps to ensure that the method definition overrides, and not overloads another method silently (see Section 14.12, p. 718).

Invocation of the method getBill() on an object of subclass TubeLight using references of the subclass and the superclass at (14) and (15), results in the new definition at (6) being executed, since both references are aliases of the TubeLight object created at (11).

tubeLight.getBill(5);                     // (14) Invokes method at (6).
light1.getBill(5);                        // (15) Invokes method at (6).

Not surprisingly, the invocation of the method getBill() on an object of superclass Light using a reference of the superclass at (16), results in the overridden definition at (2) being executed:

light2.getBill(5);                        // (16) Invokes method at (2).

Covariant return in Overriding Methods

In Example 7.2, the definition of the method makeInstance() at (8) overrides the method definition at (3). Note that the method signatures are the same, but the return type at (8) is a subtype of the return type at (3). The method at (8) returns an object of the subtype TubeLight, whereas the method at (3) returns an object of the supertype Light. This is an example of covariant return.

Depending on whether we call the method makeInstance() on an object of the subtype TubeLight or that of the supertype Light, the respective method definition will be executed. The code at (17) and (18) illustrates what object is returned by the method, depending on which method definition is executed.

Note that covariant return only applies to reference types, not to primitive types. For example, changing the return type of the getBill() method at (6) to float, will result in a compile-time error. There is no subtype relationship between primitive types.

Example 7.2 Overriding, Overloading, and Hiding

//Exceptions
class InvalidHoursException extends Exception {}
class NegativeHoursException extends InvalidHoursException {}
class ZeroHoursException extends InvalidHoursException {}
class Light {

  protected String billType = "Small bill";       // (1) Instance field

  protected double getBill(int noOfHours)
                   throws InvalidHoursException { // (2) Instance method
    if (noOfHours < 0)
      throw new NegativeHoursException();
    double smallAmount = 10.0, smallBill = smallAmount * noOfHours;
    System.out.println(billType + ": " + smallBill);
    return smallBill;
  }

  public Light makeInstance() {                   // (3) Instance method
    return new Light();
  }

  public static void printBillType() {            // (4) Static method
    System.out.println("Small bill");
  }
}
//____________________________________________________
class TubeLight extends Light {

  public static String billType = "Large bill";  // (5) Hiding field at (1).

  @Override
  public double getBill(final int noOfHours)

         throws ZeroHoursException {   // (6) Overriding instance method at (2).
    if (noOfHours == 0)
      throw new ZeroHoursException();
    double largeAmount = 100.0, largeBill = largeAmount * noOfHours;
    System.out.println(billType + ": " + largeBill);
    return largeBill;
  }

  public double getBill() {            // (7) Overloading method at (6).
    System.out.println("No bill");
    return 0.0;
  }

  @Override
  public TubeLight makeInstance() {    // (8) Overriding instance method at (3).
    return new TubeLight();
  }

  public static void printBillType() { // (9) Hiding static method at (4).
    System.out.println(billType);
  }
}
//____________________________________________________
public class Client {
  public static void main(String[] args) throws InvalidHoursException { // (10)

    TubeLight tubeLight = new TubeLight();    // (11)
    Light     light1    = tubeLight;          // (12) Aliases.
    Light     light2    = new Light();        // (13)

    System.out.println("Invoke overridden instance method:");
    tubeLight.getBill(5);                     // (14) Invokes method at (6).
    light1.getBill(5);                        // (15) Invokes method at (6).
    light2.getBill(5);                        // (16) Invokes method at (2).

    System.out.println(
           "Invoke overridden instance method with covariant return:");
    System.out.println(
           light2.makeInstance().getClass()); // (17) Invokes method at (3).
    System.out.println(
        tubeLight.makeInstance().getClass()); // (18) Invokes method at (8).

    System.out.println("Access hidden field:");
    System.out.println(tubeLight.billType);   // (19) Accesses field at (5).
    System.out.println(light1.billType);      // (20) Accesses field at (1).
    System.out.println(light2.billType);      // (21) Accesses field at (1).

    System.out.println("Invoke hidden static method:");
    tubeLight.printBillType();                // (22) Invokes method at (9).
    light1.printBillType();                   // (23) Invokes method at (4).
    light2.printBillType();                   // (24) Invokes method at (4).

    System.out.println("Invoke overloaded method:");
    tubeLight.getBill();                      // (25) Invokes method at (7).
  }
}

Output from the program:

Invoke overridden instance method:
Large bill: 500.0
Large bill: 500.0
Small bill: 50.0
Invoke overridden instance method with covariant return:
class Light
class TubeLight
Access hidden field:
Large bill
Small bill
Small bill
Invoke hidden static method:
Large bill
Small bill
Small bill
Invoke overloaded method:
No bill

Here are a few more facts to note about overriding. A subclass must use the keyword super in order to invoke an overridden method in the superclass (see p. 295).

An instance method in a subclass cannot override a static method in the superclass. The compiler will flag this as an error. A static method is class-specific and not part of any object, while overriding methods are invoked on behalf of objects of the subclass. However, a static method in a subclass can hide a static method in the superclass (see below).

A final method cannot be overridden, because the modifier final prevents method overriding. An attempt to override a final method will result in a compile-time error. An abstract method, on the other hand, requires the non-abstract subclasses to override the method, in order to provide an implementation.

The accessibility modifier private for a method means that the method is not accessible outside the class in which it is defined; therefore, a subclass cannot override it. However, a subclass can give its own definition of such a method, which may have the same signature as the method in its superclass.

Overriding vs. Overloading

Method overriding should not be confused with method overloading (see Section 3.3, p. 47).

Method overriding always requires the same method signature (name and parameter types) and the same or covariant return types. Overloading occurs when the method names are the same, but the parameter lists differ. Therefore, to overload methods, the parameters must differ either in type, order, or number. As the return type is not a part of the method signature, just having different return types is not enough to overload methods.

Only non-final instance methods in the superclass that are directly accessible from the subclass can be overridden. Both instance and static methods can be overloaded in the class they are defined in or in a subclass of their class.

Invoking an overridden method in the superclass from a subclass requires a special syntax (e.g., the keyword super). This is not necessary for invoking an overloaded method in the superclass from a subclass. If the right kinds of arguments are passed in the method call occurring in the subclass, the overloaded method in the superclass will be invoked. In Example 7.2, the method getBill() at (2) in class Light is overridden in class TubeLight at (6) and overloaded at (7). When invoked at (25), the definition at (7) is executed.

For overloaded methods, which method implementation will be executed at runtime is determined at compile time (see Section 7.10, p. 324), but for overridden methods, the method implementation to be executed is determined at runtime (see Section 7.12, p. 340). Table 7.1 provides a comparison between overriding and overloading.

Table 7.1 Overriding vs. Overloading

Table 7.1 Overriding vs. Overloading

7.3 Hiding Members

Field Hiding

A subclass cannot override fields of the superclass, but it can hide them. The subclass can define fields with the same name as in the superclass. If this is the case, the fields in the superclass cannot be accessed in the subclass by their simple names; therefore, they are not inherited by the subclass. Code in the subclass can use the keyword super to access such members, including hidden fields. A client can use a reference of the superclass to access members that are hidden in the subclass, as explained below. Of course, if the hidden field is static, it can also be accessed by the superclass name.

The following distinction between invoking instance methods on an object and accessing fields of an object must be noted. When an instance method is invoked on an object using a reference, it is the class of the current object denoted by the reference, not the type of reference, that determines which method implementation will be executed. In Example 7.2 at (14), (15), and (16), this is evident from invoking the overridden method getBill(): the method from the class corresponding to the current object is executed, regardless of the reference type. When a field of an object is accessed using a reference, it is the type of the reference, not the class of the current object denoted by the reference, that determines which field will actually be accessed. In Example 7.2 at (19), (20), and (21), this is evident from accessing the hidden field billType: the field accessed is declared in the class corresponding to the reference type, regardless of the object denoted by the reference.

In contrast to method overriding, where an instance method cannot override a static method, there are no such restrictions on the hiding of fields. The field billType is static in the subclass, but not in the superclass. The type of the fields need not be the same either, it is only the field name that matters in the hiding of fields.

Static Method Hiding

A static method cannot override an inherited instance method, but it can hide a static method if the exact requirements for overriding instance methods are fulfilled (see Section 7.2, p. 288). A hidden superclass static method is not inherited. The compiler will flag an error if the signatures are the same, but the other requirements regarding return type, throws clause, and accessibility are not met. If the signatures are different, the method name is overloaded, not hidden.

A call to a static or final method is bound to a method implementation at compile time (private methods are implicitly final). Example 7.2 illustrates invocation of static methods. Analogous to accessing fields, the method invoked in (22), (23), and (24) is determined by the class of the reference. In (22) the class type is TubeLight, therefore, the static method printBillType() at (9) in this class is invoked. In (23) and (24), the class type is Light and the hidden static method printBillType() at (4) in that class is invoked. This is borne out by the output from the program.

A hidden static method can always be invoked by using the superclass name in the subclass declaration. Additionally, the keyword super can be used in non-static code in the subclass declaration to invoke hidden static methods.

7.4 The Object Reference super

The this reference is available in non-static code and refers to the current object. When an instance method is invoked, the this reference denotes the object on which the method is called (see Section 3.3, p. 45). The keyword super can also be used in non-static code (e.g., in the body of an instance method), but only in a subclass, to access fields and invoke methods from the superclass (see Table 4.1, p.130). The keyword super provides a reference to the current object as an instance of its superclass. In method invocations with super, the method from the superclass is invoked regardless of the actual type of the object or whether the current class overrides the method. It is typically used to invoke methods that are overridden and to access members that are hidden in the subclass. Unlike the this keyword, the super keyword cannot be used as an ordinary reference. For example, it cannot be assigned to other references or cast to other reference types.

In Example 7.3, the declaration of the method demonstrate() at (9) in the class NeonLight makes use of the super keyword to access members higher up in its inheritance hierarchy. This is the case when the banner() method is invoked at (10). This method is defined at (4) in the class Light and not in the immediate superclass TubeLight of the subclass NeonLight. The overridden method getBill() and its overloaded version at (6) and (8) in the class TubeLight are invoked, using super at (11) and (12), respectively.

The class NeonLight is a subclass of the class TubeLight, which is a subclass of the class Light, which has a field named billType and a method named getBill defined at (1) and (2), respectively. One might be tempted to use the syntax super.super.getBill(20) in the subclass NeonLight to invoke this method, but this is not a valid construct. One might also be tempted to cast the this reference to the class Light and try again as shown at (13). The output shows that the method getBill() at (6) in the class TubeLight was executed, not the one from the class Light. The reason is that a cast only changes the type of the reference (in this case to Light), not the class of the object (which is still NeonLight). Method invocation is determined by the class of the current object, resulting in the inherited method getBill() in the class TubeLight being executed. There is no way to invoke the method getBill() in the class Light from the subclass NeonLight.

At (14) the keyword super is used to access the field billType at (5) in the class TubeLight. At (15) the field billType from the class Light is accessed successfully by casting the this reference, because it is the type of the reference that determines which field is accessed. From non-static code in a subclass, it is possible to directly access fields in a class higher up the inheritance hierarchy by casting the this reference. However, it is futile to cast the this reference to invoke instance methods in a class higher up the inheritance hierarchy, as illustrated above in the case of the overridden method getBill().

Finally, the calls to the static methods at (16) and (17) using the super and this references, exhibit runtime behavior analogous to accessing fields, as discussed earlier.

Example 7.3 Using the super Keyword

//Exceptions
class InvalidHoursException extends Exception {}
class NegativeHoursException extends InvalidHoursException {}
class ZeroHoursException extends InvalidHoursException {}

class Light {

  protected String billType  = "Small bill";       // (1)

  protected double getBill(int noOfHours)
  throws InvalidHoursException {                   // (2)
    if (noOfHours < 0)
      throw new NegativeHoursException();
    double smallAmount = 10.0, smallBill = smallAmount * noOfHours;
    System.out.println(billType + ": " + smallBill);
    return smallBill;
  }

  public static void printBillType() {             // (3)
    System.out.println("Small bill");
  }

  public void banner() {                           // (4)
    System.out.println("Let there be light!");
  }
}
//____________________________________________________
class TubeLight extends Light {

  public static String billType = "Large bill"; // (5) Hiding static field at (1).

  @Override
  public double getBill(final int noOfHours)
         throws ZeroHoursException {     // (6) Overriding instance method at (2).
    if (noOfHours == 0)
      throw new ZeroHoursException();
    double largeAmount = 100.0, largeBill = largeAmount * noOfHours;
    System.out.println(billType + ": " + largeBill);
    return largeBill;
  }

  public static void printBillType() {   // (7) Hiding static method at (3).
    System.out.println(billType);
  }

  public double getBill() {              // (8) Overloading method at (6).
    System.out.println("No bill");
    return 0.0;
  }
}
//____________________________________________________
class NeonLight extends TubeLight {
  // ...
  public void demonstrate() throws InvalidHoursException {     // (9)
    super.banner();                              // (10) Invokes method at (4)
    super.getBill(20);                           // (11) Invokes method at (6)
    super.getBill();                             // (12) Invokes method at (8)
    ((Light) this).getBill(20);                  // (13) Invokes method at (6)
    System.out.println(super.billType);          // (14) Accesses field at (5)
    System.out.println(((Light) this).billType); // (15) Accesses field at (1)
    super.printBillType();                       // (16) Invokes method at (7)
    ((Light) this).printBillType();              // (17) Invokes method at (3)
  }
}
//____________________________________________________
public class Client {
  public static void main(String[] args)
  throws InvalidHoursException {
    NeonLight neonRef = new NeonLight();
    neonRef.demonstrate();
  }
}

Output from the program:

Let there be light!
No bill
Large bill: 2000.0
Large bill: 2000.0
Large bill
Small bill
Large bill
Small bill

Review Questions

Review Questions

7.1 Which statements are true?

Select the two correct answers.

(a) In Java, the extends clause is used to specify the inheritance relationship.

(b) The subclass of a non-abstract class can be declared abstract.

(c) All members of the superclass are inherited by the subclass.

(d) A final class can be abstract.

(e) A class in which all the members are declared private, cannot be declared public.

7.2 Which statements are true?

Select the two correct answers.

(a) A class can only be extended by one class.

(b) Every Java object has a public method named equals.

(c) Every Java object has a public method named length.

(d) A class can extend any number of classes.

(e) A non-final class can be extended by any number of classes.

7.3 Which statements are true?

Select the two correct answers.

(a) A subclass must define all the methods from the superclass.

(b) It is possible for a subclass to define a method with the same name and parameters as a method defined by the superclass.

(c) It is possible for a subclass to define a field with the same name as a field defined by the superclass.

(d) It is possible for two classes to be the superclass of each other.

7.4 Given the following classes and declarations, which statements are true?

// Classes
class Foo {
  private int i;
  public void f() { /* ... */ }
  public void g() { /* ... */ }
}

class Bar extends Foo {
  public int j;
  public void g() { /* ... */ }
}
// Declarations:
  Foo a = new Foo();
  Bar b = new Bar();

Select the three correct answers.

(a) The Bar class is a subclass of Foo.

(b) The statement b.f(); is legal.

(c) The statement a.j = 5; is legal.

(d) The statement a.g(); is legal.

(e) The statement b.i = 3; is legal.

7.5 Which statement is true?

Select the one correct answer.

(a) Private methods cannot be overridden in subclasses.

(b) A subclass can override any method in a superclass.

(c) An overriding method can declare that it throws checked exceptions that are not thrown by the method it is overriding.

(d) The parameter list of an overriding method can be a subset of the parameter list of the method that it is overriding.

(e) The overriding method must have the same return type as the overridden method.

7.6 Given classes A, B, and C, where B extends A, and C extends B, and where all classes implement the instance method void doIt(). How can the doIt() method in A be called from an instance method in C?

Select the one correct answer.

(a) doIt();

(b) super.doIt();

(c) super.super.doIt();

(d) this.super.doIt();

(e) A.this.doIt();

(f) ((A) this).doIt();

(g) It is not possible.

7.7 What would be the result of compiling and running the following program?

// Filename: MyClass.java
public class MyClass {
  public static void main(String[] args) {
    C c = new C();
    System.out.println(c.max(13, 29));
  }
}

class A {
  int max(int x, int y) { if (x>y) return x; else return y; }
}

class B extends A{
  int max(int x, int y) { return super.max(y, x) - 10; }
}

class C extends B {
  int max(int x, int y) { return super.max(x+10, y+10); }
}

Select the one correct answer.

(a) The code will fail to compile because the max() method in B passes the arguments in the call super.max(y, x) in the wrong order.

(b) The code will fail to compile because a call to a max() method is ambiguous.

(c) The code will compile and print 13, when run.

(d) The code will compile and print 23, when run.

(e) The code will compile and print 29, when run.

(f) The code will compile and print 39, when run.

7.8 Which is the simplest expression that can be inserted at (1), so that the program prints the value of the text field from the Message class?

// Filename: MyClass.java
class Message {
  // The message that should be printed:
  String text = "Hello, world!";
}

class MySuperclass {
  Message msg = new Message();
}

public class MyClass extends MySuperclass {
  public static void main(String[] args) {
    MyClass object = new MyClass();
    object.print();
  }

  public void print() {
    System.out.println( /* (1) INSERT THE SIMPLEST EXPRESSION HERE */ );
  }
}

Select the one correct answer.

(a) text

(b) Message.text

(c) msg.text

(d) object.msg.text

(e) super.msg.text

(f) object.super.msg.text

7.9 Which method declarations, when inserted at (7), will not result in a compile-time error?

class MySuperclass {
  public        Integer step1(int i)                                { return 1; }     // (1)
  protected     String  step2(String str1, String str2) { return str1; }  // (2)
  public        String  step2(String str1)                      { return str1; }  // (3)
  public static String  step2()                                    { return "Hi"; }  // (4)

  public MyClass      makeIt()      { return new MyClass(); }             // (5)
  public MySuperclass makeIt2() { return new MyClass(); }            // (6)
}

public class MyClass extends MySuperclass {
  // (7) INSERT METHOD DECLARATION HERE
}

Select the two correct answers.

(a) public int step1(int i) { return 1; }

(b) public String step2(String str2, String str1) { return str1; }

(c) private void step2() { }

(d) private static void step2() { }

(e) private static String step2(String str) { return str; }

(f) public MySuperclass makeIt() { return new MySuperclass(); }

(g) public MyClass makeIt2() { return new MyClass(); }

7.10 What would be the result of compiling and running the following program?

class Vehicle {
  static public String getModelName() { return "Volvo"; }
  public long getRegNo() { return 12345; }
}

class Car extends Vehicle {
  static public String getModelName() { return "Toyota"; }
  public long getRegNo() { return 54321; }
}

public class TakeARide {
  public static void main(String args[]) {
    Car c = new Car();
    Vehicle v = c;

    System.out.println("|" + v.getModelName() + "|" + c.getModelName() +
                       "|" + v.getRegNo()     + "|" + c.getRegNo() + "|");
  }
}

Select the one correct answer.

(a) The code will fail to compile.

(b) The code will compile and print |Toyota|Volvo|12345|54321|, when run.

(c) The code will compile and print |Volvo|Toyota|12345|54321|, when run.

(d) The code will compile and print |Toyota|Toyota|12345|12345|, when run.

(e) The code will compile and print |Volvo|Volvo|12345|54321|, when run.

(f) The code will compile and print |Toyota|Toyota|12345|12345|, when run.

(g) The code will compile and print |Volvo|Toyota|54321|54321|, when run.

7.11 What would be the result of compiling and running the following program?

final class Item {
  Integer size;
  Item(Integer size) { this.size = size; }
  public boolean equals(Item item2) {
    if (this == item2) return true;
    return this.size.equals(item2.size);
  }
}

public class SkepticRide {
  public static void main(String[] args) {
    Item itemA = new Item(10);
    Item itemB = new Item(10);
    Object itemC = itemA;
    System.out.println("|" + itemA.equals(itemB) +
                       "|" + itemC.equals(itemB) + "|");
  }
}

Select the one correct answer.

(a) The code will fail to compile.

(b) The code will compile and print |false|false|, when run.

(c) The code will compile and print |false|true|, when run.

(d) The code will compile and print |true|false|, when run.

(e) The code will compile and print |true|true|, when run.

7.5 Chaining Constructors Using this() and super()

Constructors are discussed in Section 3.4, p. 48. Other uses of the keywords this and super can be found in Section 7.2, p. 288, and Section 8.3, p. 360.

The this() Constructor Call

Constructors cannot be inherited or overridden. They can be overloaded, but only in the same class. Since a constructor always has the same name as the class, each parameter list must be different when defining more than one constructor for a class. In Example 7.4, the class Light has three overloaded constructors. In the nondefault constructor at (3), the this reference is used to access the fields shadowed by the parameters. In the main() method at (4), the appropriate constructor is invoked depending on the arguments in the constructor call, as illustrated by the program output.

Example 7.4 Constructor Overloading

class Light {

  // Fields:
  private int     noOfWatts;       // wattage
  private boolean indicator;      // on or off
  private String  location;         // placement

  // Constructors:
  Light() {                                               // (1) Explicit default constructor
    noOfWatts = 0;
    indicator = false;
    location  = "X";
    System.out.println("Returning from default constructor no. 1.");
  }
  Light(int watts, boolean onOffState) {                                   // (2) Non-default
    noOfWatts = watts;
    indicator = onOffState;
    location = "X";
    System.out.println("Returning from non-default constructor no. 2.");
  }
  Light(int noOfWatts, boolean indicator, String location) {  // (3) Non-default
    this.noOfWatts = noOfWatts;

    this.indicator = indicator;
    this.location = location;
    System.out.println("Returning from non-default constructor no. 3.");
  }
}
//____________________________________________________
public class DemoConstructorCall {
  public static void main(String[] args) {                    // (4)
    System.out.println("Creating Light object no. 1.");
    Light light1 = new Light();
    System.out.println("Creating Light object no. 2.");
    Light light2 = new Light(250, true);
    System.out.println("Creating Light object no. 3.");
    Light light3 = new Light(250, true, "attic");
  }
}

Output from the program:

Creating Light object no. 1.
Returning from default constructor no. 1.
Creating Light object no. 2.
Returning from non-default constructor no. 2.
Creating Light object no. 3.
Returning from non-default constructor no. 3.

Example 7.5 illustrates the use of the this() construct, which is used to implement local chaining of constructors in the class when an instance of the class is created. The first two constructors at (1) and (2) from Example 7.4 have been rewritten using the this() construct in Example 7.5 at (1) and (2), respectively. The this() construct can be regarded as being locally overloaded, since its parameters (and hence its signature) can vary, as shown in the body of the constructors at (1) and (2). The this() call invokes the local constructor with the corresponding parameter list. In the main() method at (4), the appropriate constructor is invoked depending on the arguments in the constructor call when each of the three Light objects are created. Calling the default constructor to create a Light object results in the second and third constructors being executed as well. This is confirmed by the output from the program. In this case, the output shows that the third constructor completed first, followed by the second, and finally the default constructor that was called first. Bearing in mind the definition of the constructors, the constructors are invoked in the reverse order; i.e., invocation of the default constructor immediately leads to invocation of the second constructor by the call this(0, false), and its invocation leads to the third constructor being called immediately by the call this(watt, ind, "X"), with the completion of the execution in the reverse order of their invocation. Similarly, calling the second constructor to create an instance of the Light class results in the third constructor being executed as well.

Java requires that any this() call must occur as the first statement in a constructor. The this() call can be followed by any other relevant code. This restriction is due to Java’s handling of constructor invocation in the superclass when an object of the subclass is created. This mechanism is explained in the next subsection.

Example 7.5 The this() Constructor Call

class Light {
  // Fields:
  private int     noOfWatts;
  private boolean indicator;
  private String  location;

  // Constructors:
  Light() {                                                   // (1) Explicit default constructor
    this(0, false);
    System.out.println("Returning from default constructor no. 1.");
  }
  Light(int watt, boolean ind) {                                               // (2) Non-default
    this(watt, ind, "X");
    System.out.println("Returning from non-default constructor no. 2.");
  }
  Light(int noOfWatts, boolean indicator, String location) { // (3) Non-default
    this.noOfWatts = noOfWatts;
    this.indicator = indicator;
    this.location  = location;
    System.out.println("Returning from non-default constructor no. 3.");
  }
}
//____________________________________________________
public class DemoThisCall {
  public static void main(String[] args) {                           // (4)
    System.out.println("Creating Light object no. 1.");
    Light light1 = new Light();                                            // (5)
    System.out.println("Creating Light object no. 2.");
    Light light2 = new Light(250, true);                             // (6)
    System.out.println("Creating Light object no. 3.");
    Light light3 = new Light(250, true, "attic");                // (7)
  }
}

Output from the program:

Creating Light object no. 1.
Returning from non-default constructor no. 3.
Returning from non-default constructor no. 2.
Returning from default constructor no. 1.
Creating Light object no. 2.
Returning from non-default constructor no. 3.
Returning from non-default constructor no. 2.
Creating Light object no. 3.
Returning from non-default constructor no. 3.

The super() Constructor Call

The super() construct is used in a subclass constructor to invoke a constructor in the immediate superclass. This allows the subclass to influence the initialization of its inherited state when an object of the subclass is created. A super() call in the constructor of a subclass will result in the execution of the relevant constructor from the superclass, based on the signature of the call. Since the superclass name is known in the subclass declaration, the compiler can determine the superclass constructor invoked from the signature of the parameter list.

A constructor in a subclass can access the class’s inherited members by their simple names. The keyword super can also be used in a subclass constructor to access inherited members via its superclass. One might be tempted to use the super keyword in a constructor to specify initial values of inherited fields. However, the super() construct provides a better solution to initialize the inherited state.

In Example 7.6, the non-default constructor at (3) of the class Light has a super() call (with no arguments) at (4). Although the constructor is not strictly necessary, as the compiler will insert one—as explained below—it is included for expositional purposes. The non-default constructor at (6) of the class TubeLight has a super() call (with three arguments) at (7). This super() call will match the non-default constructor at (3) of the superclass Light. This is evident from the program output.

Example 7.6 The super() Constructor Call

class Light {
  // Fields:
  private int     noOfWatts;
  private boolean indicator;
  private String  location;

  // Constructors:
  Light() {                                  // (1) Explicit default constructor
    this(0, false);
    System.out.println(
    "Returning from default constructor no. 1 in class Light");
  }
  Light(int watt, boolean ind) {                              // (2) Non-default
    this(watt, ind, "X");
    System.out.println(
    "Returning from non-default constructor no. 2 in class Light");
  }
  Light(int noOfWatts, boolean indicator, String location) {  // (3) Non-default
    super();                                                                               // (4)
    this.noOfWatts = noOfWatts;
    this.indicator = indicator;
    this.location  = location;
    System.out.println(
        "Returning from non-default constructor no. 3 in class Light");
  }

}
//____________________________________________________
class TubeLight extends Light {
  // Instance variables:
  private int tubeLength;
  private int colorNo;

  // Constructors:
  TubeLight(int tubeLength, int colorNo) {                    // (5) Non-default
    this(tubeLength, colorNo, 100, true, "Unknown");
    System.out.println(
           "Returning from non-default constructor no. 1 in class TubeLight");
  }
  TubeLight(int tubeLength, int colorNo, int noOfWatts,
                    boolean indicator, String location) {             // (6) Non-default
    super(noOfWatts, indicator, location);                        // (7)
    this.tubeLength = tubeLength;
    this.colorNo    = colorNo;
    System.out.println(
           "Returning from non-default constructor no. 2 in class TubeLight");
  }
}
//____________________________________________________
public class Chaining {
  public static void main(String[] args) {
    System.out.println("Creating a TubeLight object.");
    TubeLight tubeLightRef = new TubeLight(20, 5);            // (8)
  }
}

Output from the program:

Creating a TubeLight object.
Returning from non-default constructor no. 3 in class Light
Returning from non-default constructor no. 2 in class TubeLight
Returning from non-default constructor no. 1 in class TubeLight

The super() construct has the same restrictions as the this() construct: if used, the super() call must occur as the first statement in a constructor, and it can only be used in a constructor declaration. This implies that this() and super() calls cannot both occur in the same constructor. The this() construct is used to chain constructors in the same class. The constructor at the end of such a chain can invoke a superclass constructor using the super() construct. Just as the this() construct leads to chaining of constructors in the same class, the super() construct leads to chaining of subclass constructors to superclass constructors. This chaining behavior guarantees that all superclass constructors are called, starting with the constructor of the class being instantiated, all the way to the top of the inheritance hierarchy, which is always the Object class. Note that the body of the constructor is executed in the reverse order to the call order, as super() can only occur as the first statement in a constructor. This ensures that the constructor from the Object class is completed first, followed by the constructors in the other classes down to the class being instantiated in the inheritance hierarchy. This is called (subclass–superclass) constructor chaining. The output from Example 7.6 clearly illustrates this chain of events when an object of the class TubeLight is created.

If a constructor at the end of a this()-chain (which may not be a chain at all if no this() call is invoked) does not have an explicit call to super(), the call super() (without the parameters) is implicitly inserted by the compiler to invoke the default constructor of the superclass. In other words, if a constructor has neither a this() nor a super() call as its first statement, the compiler inserts a super() call to the default constructor in the superclass. The code

class A {
  public A() {}
  // ...
}
class B extends A {
  // no constructors
  // ...
}

is equivalent to

class A {
  public A() { super(); } // (1)
  // ...
}
class B extends A {
  public B() { super(); } // (2)
  // ... }

where the default constructors with calls to the default superclass constructor are inserted in the code.

If a superclass only defines non-default constructors (i.e., only constructors with parameters), its subclasses cannot rely on the implicit super() call being inserted. This will be flagged as a compile-time error. The subclasses must then explicitly call a superclass constructor, using the super() construct with the right arguments.

class NeonLight extends TubeLight {
  // Field
  String sign;

  NeonLight() {                                         // (1)
    super(10, 2, 100, true, "Roof-top");    // (2) Cannot be commented out.
    sign = "All will be revealed!";
  }
  // ...
}

The above declaration of the subclass NeonLight provides a constructor at (1). The call of the constructor at (2) in the superclass TubeLight cannot be omitted. If it is omitted, any insertion of a super() call (with no arguments) in this constructor will try to match a default constructor in the superclass TubeLight, which only provides non-default constructors. The class NeonLight will not compile unless an explicit valid super() call is inserted at (2).

If the superclass provides only non-default constructors (that is, does not have a default constructor), this has implications for its subclasses. A subclass that relies on its own implicit default constructor will fail to compile. This is because the implicit default constructor of the subclass will attempt to call the (non-existent) default constructor in the superclass. A constructor in a subclass must explicitly use the super() call, with the appropriate arguments, to invoke a non-default constructor in the superclass. This is because the constructor in the subclass cannot rely on an implicit super() call to the default constructor in the superclass.

Review Questions

Review Questions

7.12 Which constructors can be inserted at (1) in MySub without causing a compile-time error?

class MySuper {
  int number;
  MySuper(int i) { number = i; }
}

class MySub extends MySuper {
  int count;
  MySub(int count, int num) {
    super(num);
    this.count = count;
  }

  // (1) INSERT CONSTRUCTOR HERE
}

Select the one correct answer.

(a) MySub() {}

(b) MySub(int count) { this.count = count; }

(c) MySub(int count) { super(); this.count = count; }

(d) MySub(int count) { this.count = count; super(count); }

(e) MySub(int count) { this(count, count); }

(f) MySub(int count) { super(count); this(count, 0); }

7.13 Which statement is true?

Select the one correct answer.

(a) A super() or this() call must always be provided explicitly as the first statement in the body of a constructor.

(b) If both a subclass and its superclass do not have any declared constructors, the implicit default constructor of the subclass will call super() when run.

(c) If neither super() nor this() is declared as the first statement in the body of a constructor, this() will implicitly be inserted as the first statement.

(d) If super() is the first statement in the body of a constructor, this() can be declared as the second statement.

(e) Calling super() as the first statement in the body of a constructor of a subclass will always work, since all superclasses have a default constructor.

7.14 What will the following program print when run?

// Filename: MyClass.java
 public class MyClass {
   public static void main(String[] args) {
     B b = new B("Test");
  }
}

class A {
  A() { this("1", "2"); }

  A(String s, String t) { this(s + t); }

  A(String s) { System.out.println(s); }
}

class B extends A {
  B(String s) { System.out.println(s); }

  B(String s, String t) { this(t + s + "3"); }

  B() { super("4"); };
}

Select the one correct answer.

(a) It will just print Test.

(b) It will print Test followed by Test.

(c) It will print 123 followed by Test.

(d) It will print 12 followed by Test.

(e) It will print 4 followed by Test.

7.6 Interfaces

Extending classes using single implementation inheritance creates new class types. A superclass reference can refer to objects of its own type and its subclasses strictly according to the inheritance hierarchy. Because this relationship is linear, it rules out multiple implementation inheritance, i.e., a subclass inheriting from more than one superclass. Instead Java provides interfaces, which not only allow new named reference types to be introduced, but also permit multiple interface inheritance.

Generic interfaces are discussed in Section 14.2, p. 666.

Defining Interfaces

A top-level interface has the following general syntax:

<accessibility modifier>  interface <interface name>
                    <extends interface clause> // Interface header
   { // Interface body
      <constant declarations>
      <abstract method declarations>
      <nested class declarations>
      <nested interface declarations>
   }

In the interface header, the name of the interface is preceded by the keyword interface. The interface name can also include a list of formal type parameters (see Section 14.2, p. 666). In addition, the interface header can specify the following information:

• scope or accessibility modifier (see Section 4.6, p. 129)

• any interfaces it extends (see Section 7.6, p. 313)

The interface body can contain member declarations which comprise:

constant declarations (see Section 7.6, p. 314)

abstract method declarations (see Section 7.6, p. 313)

nested class and interface declarations (see Section 8.1, p. 352)

An interface does not provide any implementation and is, therefore, abstract by definition. This means that it cannot be instantiated. Declaring an interface abstract is superfluous and seldom done.

The member declarations can appear in any order in the interface body. Since interfaces are meant to be implemented by classes, interface members implicitly have public accessibility and the public modifier can be omitted.

Interfaces with empty bodies can be used as markers to tag classes as having a certain property or behavior. Such interfaces are also called ability interfaces. Java APIs provide several examples of such marker interfaces: java.lang.Cloneable, java.io.Serializable, java.util.EventListener.

Abstract Method Declarations

An interface defines a contract by specifying a set of abstract method declarations, but provides no implementations (see Section 4.10, p. 150). The methods in an interface are all implicitly abstract and public by virtue of their definition. Only the modifiers abstract and public are allowed, but these are invariably omitted. An abstract method declaration has the following form:

<optional type parameter list> <return type> <method name> (<parameter list>)
    <throws clause>

The optional list of formal type parameters is specified for generic method declarations (see Section 14.8, p. 697).

Example 7.7 declares two interfaces: IStack at (1) and ISafeStack at (5). These interfaces are discussed in the subsequent subsections.

Example 7.7 Interfaces

interface IStack {                                                                            // (1)
  void   push(Object item);
  Object pop();
}
//____________________________________________________
class StackImpl implements IStack {                                            // (2)
  protected Object[] stackArray;
  protected int      tos;  // top of stack

  public StackImpl(int capacity) {
    stackArray = new Object[capacity];
    tos        = -1;
  }

  public void push(Object item) { stackArray[++tos] = item; }     // (3)

  public Object pop() {                                                                    // (4)
    Object objRef = stackArray[tos];
    stackArray[tos] = null;
    tos--;
    return objRef;
  }

  public Object peek() { return stackArray[tos]; }
}
//____________________________________________________
interface ISafeStack extends IStack {                                            // (5)
  boolean isEmpty();
  boolean isFull();
}
//____________________________________________________
class SafeStackImpl extends StackImpl implements ISafeStack {     // (6)

  public SafeStackImpl(int capacity) { super(capacity); }
  public boolean isEmpty() { return tos < 0; }                                 // (7)
  public boolean isFull()  { return tos >= stackArray.length-1; }     // (8)
}
//____________________________________________________
public class StackUser {

  public static void main(String[] args) {                                         // (9)
    SafeStackImpl safeStackRef  = new SafeStackImpl(10);
    StackImpl     stackRef      = safeStackRef;
    ISafeStack    isafeStackRef = safeStackRef;
    IStack        istackRef     = safeStackRef;

    Object        objRef        = safeStackRef;

    safeStackRef.push("Dollars");                                                  // (10)
    stackRef.push("Kroner");
    System.out.println(isafeStackRef.pop());
    System.out.println(istackRef.pop());
    System.out.println(objRef.getClass());
  }
}

Output from the program:

Kroner
Dollars
class SafeStackImpl

Implementing Interfaces

Any class can elect to implement, wholly or partially, zero or more interfaces. A class specifies the interfaces it implements as a comma-separated list of unique interface names in an implements clause in the class header. The interface methods must all have public accessibility when implemented in the class (or its subclasses). A class can neither narrow the accessibility of an interface method nor specify new exceptions in the method’s throws clause, as attempting to do so would amount to altering the interface’s contract, which is illegal. The criteria for overriding methods also apply when implementing interface methods (see Section 7.2, p. 288).

A class can provide implementations of methods declared in an interface, but to reap the benefits of interfaces, the class must also specify the interface name in its implements clause.

In Example 7.7, the class StackImpl implements the interface IStack. It both specifies the interface name using the implements clause in its class header at (2) and provides the implementation for the methods in the interface at (3) and (4). Changing the public accessibility of these methods in the class will result in a compile-time error, as this would narrow their accessibility.

A class can choose to implement only some of the methods of its interfaces (i.e., give a partial implementation of its interfaces). The class must then be declared as abstract (see Section 4.8, p. 135). Note that interface methods cannot be declared static, because they comprise the contract fulfilled by the objects of the class implementing the interface. Interface methods are always implemented as instance methods.

The interfaces a class implements and the classes it extends (directly or indirectly) are called supertypes of the class. Conversely, the class is a subtype of its supertypes. Classes implementing interfaces introduce multiple interface inheritance into their implementation inheritance hierarchy. However, note that regardless of how many interfaces a class implements directly or indirectly, it only provides a single implementation of a member that might have been declared in multiple interfaces.

Extending Interfaces

An interface can extend other interfaces, using the extends clause. Unlike extending classes, an interface can extend several interfaces. The interfaces extended by an interface (directly or indirectly) are called superinterfaces. Conversely, the interface is a subinterface of its superinterfaces. Since interfaces define new reference types, superinterfaces and subinterfaces are also supertypes and subtypes, respectively.

A subinterface inherits all methods from its superinterfaces, as their method declarations are all implicitly public. A subinterface can override abstract method declarations from its superinterfaces. Overridden methods are not inherited. Abstract method declarations can also be overloaded, analogous to method overloading in classes.

Example 7.7 provides an example of multiple interface inheritance. In Example 7.7, the interface ISafeStack extends the interface IStack at (5). The class SafeStackImpl both extends the StackImpl class and implements the ISafeStack interface at (6). Both the implementation and the interface inheritance hierarchies for classes and interfaces defined in Example 7.7 are shown in Figure 7.2.

Figure 7.2 Inheritance Relations

Inheritance Relations

In UML, an interface resembles a class. One way to differentiate between them is to use an “interface” stereotype as in Figure 7.2. Interface inheritance is depicted in a similar manner to implementation inheritance, but uses an unbroken inheritance arrow. Thinking in terms of types, every reference type in Java is a subtype of the Object type. This means that any interface type is also a subtype of the Object type. We have augmented Figure 7.2 with an extra inheritance arrow to show this subtype relation.

It is instructive to note how the class SafeStackImpl implements the ISafeStack interface: it inherits implementations of the push() and pop() methods from its superclass StackImpl, and provides its own implementation of the isFull() and isEmpty() methods from the ISafeStack interface. The interface ISafeStack inherits two abstract method declarations from its superinterface IStack. All its methods are implemented by the SafeStackImpl class. The class SafeStackImpl implicitly implements the IStack interface: it implements the ISafeStack interface that it inherits from the IStack interface. This is readily evident from the diamond shape of the inheritance hierarchy in Figure 7.2. There is only one single implementation inheritance into the class SafeStackImpl, namely from its superclass StackImpl.

Note that there are three different inheritance relations at work when defining inheritance among classes and interfaces:

1. Single implementation inheritance hierarchy between classes: a class extends another class (subclasses–superclasses).

2. Multiple inheritance hierarchy between interfaces: an interface extends other interfaces (subinterfaces–superinterfaces).

3. Multiple interface inheritance hierarchy between interfaces and classes: a class implements interfaces.

Interface References

Although interfaces cannot be instantiated, references of an interface type can be declared. The reference value of an object can be assigned to references of the object’s supertypes. In Example 7.7, an object of the class SafeStackImpl is created in the main() method of the class StackUser at (9). The reference value of the object is assigned to references of all the object’s supertypes, which are used to manipulate the object. Polymorphic behavior of supertype references is discussed in Section 7.12, p. 340.

Constants in Interfaces

An interface can also define named constants. Such constants are defined by field declarations and are considered to be public, static, and final. These modifiers can be omitted from the declaration. Such a constant must be initialized with an initializer expression (see Section 9.8, p. 406).

An interface constant can be accessed by any client (a class or interface) using its fully qualified name, regardless of whether the client extends or implements its interface. However, if a client is a class that implements this interface or an interface that extends this interface, then the client can also access such constants directly by their simple names, without resorting to the fully qualified name. Such a client inherits the interface constants. Typical usage of constants in interfaces is illustrated in Example 7.8, showing both direct access and use of fully qualified names in the print statements at (1) and (2), respectively.

Extending an interface that has constants is analogous to extending a class having static variables. In particular, these constants can be hidden by the subinterfaces. In the case of multiple inheritance of interface constants, any name conflicts can be resolved by using fully qualified names for the constants involved.

When defining a set of related constants, the recommended practice is to use an enumerated type (Section 3.5, p. 54), rather than named constants in an interface.

Example 7.8 Variables in Interfaces

interface Constants {
  double PI_APPROXIMATION = 3.14;
  String AREA_UNITS                  = "sq.cm.";
  String LENGTH_UNITS            = "cm.";
}
//____________________________________________________
public class Client implements Constants {
  public static void main(String[] args)       {
    double radius = 1.5;

    // (1) Using direct access:
    System.out.printf("Area of circle is %.2f %s%n",
               PI_APPROXIMATION * radius*radius, AREA_UNITS);

    // (2) Using fully qualified name:
    System.out.printf("Circumference of circle is %.2f %s%n",
             2.0 * Constants.PI_APPROXIMATION * radius, Constants.LENGTH_UNITS);
  }
}

Output from the program:

Area of circle is 7.06 sq.cm.
Circumference of circle is 9.42 cm.

Review Questions

Review Questions

7.15 Which statements about interfaces are true?

Select the two correct answers.

(a) Interfaces allow multiple implementation inheritance.

(b) Interfaces can be extended by any number of interfaces.

(c) Interfaces can extend any number of interfaces.

(d) Members of an interface are never static.

(e) Members of an interface can always be declared static.

7.16 Which of these field declarations are legal within the body of an interface?

Select the three correct answers.

(a) public static int answer = 42;

(b) int answer;

(c) final static int answer = 42;

(d) public int answer = 42;

(e) private final static int answer = 42;

7.17 Which statements about the keywords extends and implements are true?

Select the two correct answers.

(a) The keyword extends is used to specify that an interface inherits from another interface.

(b) The keyword extends is used to specify that a class implements an interface.

(c) The keyword implements is used to specify that an interface inherits from another interface.

(d) The keyword implements is used to specify that a class inherits from an interface.

(e) The keyword implements is used to specify that a class inherits from another class.

7.18 Which statement is true about the following code?

// Filename: MyClass.java
abstract class MyClass implements Interface1, Interface2 {
  public void f() { }
  public void g() { }
}

interface Interface1 {
  int VAL_A = 1;
  int VAL_B = 2;

  void f();
  void g();
}

interface Interface2 {
  int VAL_B = 3;
  int VAL_C = 4;

  void g();
  void h();
}

Select the one correct answer.

(a) MyClass only implements Interface1. Implementation for void h() from Interface2 is missing.

(b) The declarations of void g() in the two interfaces conflict, therefore, the code will not compile.

(c) The declarations of int VAL_B in the two interfaces conflict, therefore, the code will not compile.

(d) Nothing is wrong with the code, it will compile without errors.

7.19 Which declaration can be inserted at (1) without causing a compilation error?

interface MyConstants {
  int r = 42;
  int s = 69;
  // (1) INSERT CODE HERE
}

Select the two correct answers.

(a) final double circumference = 2 * Math.PI * r;

(b) int total = total + r + s;

(c) int AREA = r * s;

(d) public static MAIN = 15;

(e) protected int CODE = 31337;

7.7 Arrays and Subtyping

Table 7.2 summarizes the types found in Java. Only primitive data and reference values can be stored in variables. Only class and array types can be explicitly instantiated to create objects.

Table 7.2 Types and Values

Table 7.2 Types and Values

Arrays and Subtype Covariance

Arrays are objects in Java. Array types (boolean[], Object[], StackImpl[]) implicitly augment the inheritance hierarchy. The inheritance hierarchy depicted in Figure 7.2 can be augmented by the corresponding array types. The resulting type hierarchy is shown in Figure 7.3. An array type is shown as a “class” with the [] notation appended to the name of the element type. The class SafeStackImpl is a subclass of the class StackImpl. The corresponding array types, SafeStackImpl[] and StackImpl[], are shown as subtype and supertype, respectively, in the type hierarchy. Figure 7.3 also shows array types corresponding to some of the primitive data types.

Figure 7.3 Reference Type Hierarchy: Arrays and Subtype Covariance

Reference Type Hierarchy: Arrays and Subtype Covariance

From the type hierarchy in Figure 7.3, we can summarize the following:

All reference types are subtypes of the Object type. This applies to classes, interfaces, enum, and array types, as these are all reference types.

• All arrays of reference types are also subtypes of the array type Object[], but arrays of primitive data types are not. Note that the array type Object[] is also a subtype of the Object type.

• If a non-generic reference type is a subtype of another non-generic reference type, the corresponding array types also have an analogous subtype-supertype relationship. This is called the subtype covariance relationship. This relationship however does not hold for parameterized types (see Section 14.4, p. 673).

• There is no subtype-supertype relationship between a type and its corresponding array type.

We can create an array of an interface type, but we cannot instantiate an interface (as is the case with abstract classes). In the declaration statement below, the reference iSafeStackArray has type ISafeStack[] (i.e., an array of the interface type ISafeStack).

ISafeStack[] iSafeStackArray = new ISafeStack[5];

The array creation expression creates an array whose element type is ISafeStack. The array object can accommodate five references of the type ISafeStack. However, the declaration statement does not initialize these references to refer to any objects, but they are initialized to the default value null.

Array Store Check

An array reference exhibits polymorphic behavior like any other reference, subject to its location in the type hierarchy (see Section 7.12, p. 340). However, a runtime check is necessary when objects are inserted in an array, as the following example illustrates.

The following assignment is valid, as a supertype reference (StackImpl[]) can refer to objects of its subtype (SafeStackImpl[]):

StackImpl[] stackImplArray = new SafeStackImpl[2];      // (1)

Since StackImpl is a supertype of SafeStackImpl, the following assignment is also valid:

stackImplArray[0] = new SafeStackImpl(10);              // (2)

The assignment at (2) inserts a SafeStackImpl object in the SafeStackImpl[] object (i.e., the array of SafeStackImpl) created at (1).

Since the type of stackImplArray[i], (0i < 2), is StackImpl, it should be possible to do the following assignment as well:

stackImplArray[1] = new StackImpl(20);                  // (3) ArrayStoreException

At compile time there are no problems, as the compiler cannot deduce that the array variable stackImplArray will actually denote a SafeStackImpl[] object at runtime. However, the assignment at (3) results in an ArrayStoreException to be thrown at runtime, as a SafeStackImpl[] object cannot possibly contain objects of type StackImpl.

In order to make the array store check feasible at runtime, an array retains information about its declared element type at runtime.

7.8 Reference Values and Conversions

A review of Section 5.1, p. 160, on conversions is recommended before proceeding with this section.

Reference values, like primitive values, can be assigned, cast, and passed as arguments. Conversions can occur in the following contexts:

• assignment

• method invocation

• casting

The rule of thumb for the primitive data types is that widening conversions are permitted, but narrowing conversions require an explicit cast. The rule of thumb for reference values is that widening conversions up the type hierarchy are permitted, but narrowing conversions down the hierarchy require an explicit cast. In other words, conversions that are from a subtype to its supertypes are allowed, other conversions require an explicit cast or are otherwise illegal. There is no notion of promotion for reference values.

Unchecked conversions involving generic and raw types are discussed in Section 14.2, p. 670.

7.9 Reference Value Assignment Conversions

In the context of assignments, the following conversions are permitted (Table 5.1, p. 163):

• widening primitive and reference conversions (longint, ObjectString)

• boxing conversion of primitive values, followed by optional widening reference conversion (Integerint, NumberIntegerint)

• unboxing conversion of a primitive value wrapper object, followed by optional widening primitive conversion (longintInteger)

And only for assigment conversions, we have the following:

• narrowing conversion for constant expressions of non-long integer type, with optional boxing (Bytebyteint)

Note that the above rules imply that a widening conversion cannot be followed by any boxing conversion, but the converse is permitted.

Widening reference conversions typically occur during assignment up the type hierarchy, with implicit conversion of the source reference value to that of the destination reference type:

Object obj = "Up the tree";    // Widening reference conversion: Object <--String
String str1 = obj;      // Not ok. Narrowing reference conversion requires a cast.
String str2 = new Integer(10); // Illegal. No relation between String and Integer.

The source value can be a primitive value, in which case the value is boxed in a wrapper object corresponding to the primitive type. If the destination reference type is a supertype of the wrapper type, a widening reference conversion can occur:

Integer iRef = 10;  // Only boxing
Number num = 10L;   // Boxing, followed by widening: Number <---Long <---long
Object obj = 100;   // Boxing, followed by widening: Object <---Integer <---int

More examples of boxing during assignment can be found in Section 5.1, p. 162.

Example 7.9 Assigning and Passing Reference Values

interface IStack                      { /* From Example 7.7 */ }
interface ISafeStack extends IStack   { /* From Example 7.7 */ }
class StackImpl implements IStack     { /* From Example 7.7 */ }

class SafeStackImpl extends StackImpl
              implements ISafeStack   { /* From Example 7.7 */ }

public class ReferenceConversion {

  public static void main(String[] args) {
    // Reference declarations:
    Object        objRef;
    StackImpl     stackRef;
    SafeStackImpl safeStackRef;
    IStack        iStackRef;
    ISafeStack    iSafeStackRef;

    // SourceType is a class type:
    safeStackRef  = new SafeStackImpl(10);
    objRef        = safeStackRef;    // (1) Always possible
    stackRef      = safeStackRef;    // (2) Subclass to superclass assignment
    iStackRef     = stackRef;        // (3) StackImpl implements IStack
    iSafeStackRef = safeStackRef;    // (4) SafeStackImpl implements ISafeStack

    // SourceType is an interface type:
    objRef    = iStackRef;           // (5) Always possible
    iStackRef = iSafeStackRef;       // (6) Subto super-interface assignment

    // SourceType is an array type:
    Object[]        objArray        = new Object[3];
    StackImpl[]     stackArray      = new StackImpl[3];
    SafeStackImpl[] safeStackArray  = new SafeStackImpl[5];
    ISafeStack[]    iSafeStackArray = new ISafeStack[5];
    int[]           intArray        = new int[10];

    // Reference value assignments:
    objRef     = objArray;           // (7) Always possible
    objRef     = stackArray;         // (8) Always possible
    objArray   = stackArray;         // (9) Always possible
    objArray   = iSafeStackArray;    // (10) Always possible
    objRef     = intArray;           // (11) Always possible
    //  objArray   = intArray;       // (12) Compile-time error
    stackArray = safeStackArray;     // (13) Subclass array to superclass array
    iSafeStackArray = safeStackArray;// (14) SafeStackImpl implements ISafeStack

    // Method Invocation Conversions:
    System.out.println("First call:");
    sendParams(stackRef, safeStackRef, iStackRef,
               safeStackArray, iSafeStackArray);                    // (15)
    //  Call Signature: sendParams(StackImpl, SafeStackImpl, IStack,
    //                             SafeStackImpl[], ISafeStack[]);

    System.out.println("Second call:");
    sendParams(iSafeStackArray, stackRef, iSafeStackRef,
               stackArray, safeStackArray);                         // (16)
    //  Call Signature: sendParams(ISafeStack[], StackImpl, ISafeStack,
    //                             StackImpl[], SafeStackImpl[]);
  }

  public static void sendParams(Object objRefParam, StackImpl stackRefParam,
      IStack iStackRefParam, StackImpl[] stackArrayParam,
      final IStack[] iStackArrayParam) {                            // (17)
    //  Signature: sendParams(Object, StackImpl, IStack, StackImpl[], IStack[])
    //  Print class name of object denoted by the reference at runtime.
    System.out.println(objRefParam.getClass());
    System.out.println(stackRefParam.getClass());
    System.out.println(iStackRefParam.getClass());
    System.out.println(stackArrayParam.getClass());
    System.out.println(iStackArrayParam.getClass());
  }
}

Output from the program:

First call:
class SafeStackImpl
class SafeStackImpl
class SafeStackImpl
class [LSafeStackImpl;
class [LSafeStackImpl;
Second call:
class [LSafeStackImpl;
class SafeStackImpl
class SafeStackImpl
class [LSafeStackImpl;
class [LSafeStackImpl;

The rules for reference value assignment are stated, based on the following code:

SourceType srcRef;
// srcRef is appropriately initialized.
DestinationType destRef = srcRef;

If an assignment is legal, the reference value of srcRef is said to be assignable (or assignment compatible) to the reference of DestinationType. The rules are illustrated by concrete cases from Example 7.9. Note that the code in Example 7.9 uses reference types from Example 7.7, p. 311.

• If the SourceType is a class type, the reference value in srcRef may be assigned to the destRef reference, provided the DestinationType is one of the following:

DestinationType is a superclass of the subclass SourceType.

DestinationType is an interface type that is implemented by the class SourceType.

     objRef        = safeStackRef;    // (1) Always possible
     stackRef      = safeStackRef;    // (2) Subclass to superclass assignment
     iStackRef     = stackRef;        // (3) StackImpl implements IStack
     iSafeStackRef = safeStackRef;    // (4) SafeStackImpl implements ISafeStack

If the SourceType is an interface type, the reference value in srcRef may be assigned to the destRef reference, provided the DestinationType is one of the following:

DestinationType is Object.

DestinationType is a superinterface of subinterface SourceType.

      objRef    = iStackRef;     // (5) Always possible
       iStackRef = iSafeStackRef; // (6) Subinterface to superinterface assignment

• If the SourceType is an array type, the reference value in srcRef may be assigned to the destRef reference, provided the DestinationType is one of the following:

DestinationType is Object.

DestinationType is an array type, where the element type of the SourceType is assignable to the element type of the DestinationType.

      objRef     = objArray;           // (7) Always possible
      objRef     = stackArray;         // (8) Always possible
      objArray   = stackArray;         // (9) Always possible
      objArray   = iSafeStackArray;    // (10) Always possible
      objRef     = intArray;           // (11) Always possible
      // objArray   = intArray;        // (12) Compile-time error
      stackArray = safeStackArray;     // (13) Subclass array to superclass array
      iSafeStackArray = safeStackArray;// (14) SafeStackImpl implements ISafeStack

The rules for assignment are enforced at compile time, guaranteeing that no type conversion error will occur during assignment at runtime. Such conversions are type safe. The reason the rules can be enforced at compile time is that they concern the declared type of the reference (which is always known at compile time) rather than the actual type of the object being referenced (which is known at runtime).

7.10 Method Invocation Conversions Involving References

The conversions for reference value assignment are also applicable for method invocation conversions, except for the narrowing conversion for constant expressions of non-long integer type (Table 5.1, p. 163). This is reasonable, as parameters in Java are passed by value (see Section 3.7, p. 81), requiring that values of actual parameters must be assignable to formal parameters of compatible types.

In Example 7.9, the method sendParams() at (17) has the following signature, showing the types of the formal parameters:

sendParams(Object, StackImpl, IStack, StackImpl[], IStack[])

The method call at (15) has the following signature, showing the types of the actual parameters:

sendParams(StackImpl, SafeStackImpl, IStack, SafeStackImpl[], ISafeStack[]);

Note that the assignment of the values of the actual parameters to the corresponding formal parameters is legal, according to the rules for assignment discussed earlier.

The method call at (16) provides another example of the parameter passing conversion. It has the following signature:

sendParams(ISafeStack[], StackImpl, ISafeStack, StackImpl[], SafeStackImpl[]);

Analogous to assignment, the rules for parameter passing conversions are based on the reference type of the parameters and are enforced at compile time. The output in Example 7.9 shows the class of the actual objects referenced by the formal parameters at runtime, which in this case turns out to be either SafeStackImpl or SafeStackImpl[]. The characters [L in the output indicate a onedimensional array of a class or interface type (see the Class.getName() method in the Java API documentation).

Overloaded Method Resolution

In this subsection, we take a look at some aspects regarding overloaded method resolution, i.e., how the compiler determines which overloaded method will be invoked by a given method call at runtime.

Resolution of overloaded methods selects the most specific method for execution. One method is more specific than another method if all actual parameters that can be accepted by the one can be accepted by the other. If there is more than one such method, the call is ambiguous. The following overloaded methods illustrate this situation.

private static void flipFlop(String str, int i, Integer iRef) { // (1)
    out.println(str + " ==> (String, int, Integer)");
}
private static void flipFlop(String str, int i, int j) {        // (2)
    out.println(str + " ==> (String, int, int)");
}

Their method signatures are, as follows:

flipFlop(String, int, Integer)                             // See (1) above
flipFlop(String, int, int)                                 // See (2) above

The following method call is ambiguous:

flipFlop("(String, Integer, int)", new Integer(4), 2004);  // (3) Ambiguous call.

It has the call signature:

flipFlop(String, Integer, int)                             // See (3) above

The method at (1) can be called with the second argument unboxed and the third argument boxed, as can the method at (2) with only the second argument unboxed. In other words, for the call at (3), none of the methods is more specific than the other one. Example 7.10 illustrates a simple case of how method resolution is done to choose the most specific one of the overloaded methods. The method testIfOn() is overloaded at (1) and (2) in the class Overload. The call client.testIfOn(tubeLight) at (3) satisfies the parameter lists in both implementations given at (1) and (2), as the reference tubeLight, which denotes an object of the class TubeLight, can also be assigned to a reference of its superclass Light. The most specific method, (2), is chosen, resulting in false being written on the terminal. The call client.testIfOn(light) at (4) only satisfies the parameter list in the implementation given at (1), resulting in true being written on the terminal.

Example 7.10 Choosing the Most Specific Method (Simple Case)

class Light { /* ... */ }

class TubeLight extends Light { /* ... */ }

public class Overload {
  boolean testIfOn(Light aLight)         { return true; }    // (1)
  boolean testIfOn(TubeLight aTubeLight) { return false; }   // (2)

  public static void main(String[] args) {

    TubeLight tubeLight = new TubeLight();
    Light     light     = new Light();

    Overload client = new Overload();
    System.out.println(client.testIfOn(tubeLight));// (3) ==> method at (2)
    System.out.println(client.testIfOn(light));    // (4) ==> method at (1)
  }
}

Output from the program:

false
true

The algorithm used by the compiler for the resolution of overloaded methods incorporates the following phases:

1. It first performs overload resolution without permitting boxing, unboxing, or the use of a varargs call.

2. If phase (1) fails, it performs overload resolution allowing boxing and unboxing, but excluding the use of a varargs call.

3. If phase (2) fails, it performs overload resolution combining a varargs call, boxing, and unboxing.

Example 7.11 provides some insight into how the compiler determines the most specific overloaded method using the phases outlined above. The example has six overloaded declarations of the method action(). The signature of each method is given by the local variable signature in each method. The first formal parameter of each method is the signature of the call that invoked the method. The printout from each method thus allows us to see which method call resolved to which method.

The main() method contains ten calls, (8) to (17), of the action() method. In each call, the first argument is the signature of that method call.

An important thing to note is that the compiler chooses a non-varargs call over a varargs call, as seen in the calls from (8) to (12).

(String) => (String)                               (8) calls (1)
(String, int) => (String, int)                     (9) calls (2)
(String, Integer) => (String, int)                 (10) calls (2)
(String, int, byte) => (String, int, int)          (11) calls (3)
(String, int, int) => (String, int, int)           (12) calls (3)

An unboxing conversion (Integer to int) takes place for the call at (10). A widening primitive conversion (byte to int) takes place for the call at (11).

Varargs calls are chosen from (13) to (17):

(String, int, long) => (String, Number[])          (13) calls (5)
(String, int, int, int) => (String, Integer[])     (14) calls (4)
(String, int, double) => (String, Number[])        (15) calls (5)
(String, int, String) => (String, Object[])        (16) calls (6)
(String, boolean) => (String, Object[])            (17) calls (6)

When a varargs call is chosen, the method determined has the most specific varargs parameter that is applicable for the actual argument. For example, in the method call at (14), the type Integer[] is more specific than Number[] or Object[]. Note also the boxing of the elements of the implicitly created array in the calls from (13) to (17).

Example 7.11 Overloaded Method Resolution

import static java.lang.System.out;

class OverloadResolution {

  public void action(String str) {                  // (1)
    String signature = "(String)";
    out.println(str + " => " + signature);
  }

  public void action(String str, int m) {           // (2)
    String signature = "(String, int)";
    out.println(str + " => " + signature);
  }

  public void action(String str, int m, int n) {    // (3)
    String signature = "(String, int, int)";
    out.println(str + " => " + signature);
  }

  public void action(String str, Integer... data) { // (4)
    String signature = "(String, Integer[])";
    out.println(str + " => " + signature);

  }

  public void action(String str, Number... data) {  // (5)
    String signature = "(String, Number[])";
    out.println(str + " => " + signature);
  }

  public void action(String str, Object... data) {  // (6)
    String signature = "(String, Object[])";
    out.println(str + " => " + signature);
  }

  public static void main(String[] args) {
    OverloadResolution ref = new OverloadResolution();
    ref.action("(String)");                                  // (8)  calls (1)
    ref.action("(String, int)",           10);               // (9)  calls (2)
    ref.action("(String, Integer)",       new Integer(10));  // (10) calls (2)
    ref.action("(String, int, byte)",     10, (byte)20);     // (11) calls (3)
    ref.action("(String, int, int)",      10,  20);          // (12) calls (3)
    ref.action("(String, int, long)",     10,  20L);         // (13) calls (5)
    ref.action("(String, int, int, int)", 10,  20,  30);     // (14) calls (4)
    ref.action("(String, int, double)",   10,  20.0);        // (15) calls (5)
    ref.action("(String, int, String)",   10,  "what?");     // (16) calls (6)
    ref.action("(String, boolean)",       false);            // (17) calls (6)
  }
}

Output from the program:

(String) => (String)                               (8) calls (1)
(String, int) => (String, int)                     (9) calls (2)
(String, Integer) => (String, int)                 (10) calls (2)
(String, int, byte) => (String, int, int)          (11) calls (3)
(String, int, int) => (String, int, int)           (12) calls (3)
(String, int, long) => (String, Number[])          (13) calls (5)
(String, int, int, int) => (String, Integer[])     (14) calls (4)
(String, int, double) => (String, Number[])        (15) calls (5)
(String, int, String) => (String, Object[])        (16) calls (6)
(String, boolean) => (String, Object[])            (17) calls (6)

7.11 Reference Casting and the instanceof Operator

The Cast Operator

The type cast expression for reference types has the following syntax:

(<destination type><reference expression>

where the <reference expression> evaluates to a reference value of an object of some reference type. A type cast expression checks that the reference value refers to an object whose type is compatible with the <destination type>, i.e. its type is a subtype of the <destination type>. If this is not the case, a ClassCastException is thrown. The literal null can be cast to any reference type. The construct (<destination type>) is usually called the cast operator.

The following conversions can be applied to the operand of a cast operator:

• both widening and narrowing reference conversions, followed optionally by an unchecked conversion

• both boxing and unboxing conversions

The implications that generics have for the cast operator, and the unchecked conversions that can occur, are discussed in Section 14.13, p. 724.

Boxing and unboxing conversions that can occur during casting is illustrated by the following code:

// (1) Boxing and casting: Number <-- Integer <-- int:
Number num = (Number) 100;
// (2) Casting, boxing, casting: Object <-- Integer <-- int <--double:
Object obj = (Object) (int) 10.5;
// (3) Casting, unboxing, casting: double <--- int <-- Integer <-- Object:
double d = (double) (Integer) obj;

Note that the resulting object from the cast expressions in (1) and (2) is an Integer. The boxing conversions from int to Integer in (1) and (2) are implicit, and the unboxing conversion from Integer to int in (3) is also implicit.

The instanceof Operator

The binary instanceof operator can be used for comparing types. It has the following syntax (note that the keyword is composed of only lowercase letters):

<reference expression> instanceof <destination type>

The instanceof operator returns true if the left-hand operand (that is, the reference value that results from the evaluation of <reference expression>) can be a subtype of the right-hand operand (<destination type>). It always returns false if the left-hand operand is null. If the instanceof operator returns true, the corresponding type cast expression will always be valid. Both the type cast expression and the instanceof operators require a compile-time check and a runtime check, as explained below.

The compile-time check determines whether there is a subclass-superclass relationship between the source and the destination types. Given that the type of the <reference expression> is <source type>, the compiler determines whether a reference of <source type> and a reference of <destination type> can refer to objects of a reference type that are a common subtype of both <source type> and <destination type> in the type hierarchy. If this is not the case, then obviously there is no relationship between the types, and neither the cast nor the instanceof operator application would be valid. At runtime, the <reference expression> evaluates to a reference value of an object. It is the type of the actual object that determines the outcome of the operation, as explained earlier.

What implications generics has for the instanceof operator is discussed in Section 14.13, p. 723.

With the classes Light and String as <source type> and <destination type>, respectively, there is no subtype-supertype relationship between the <source type> and <destination type>. The compiler would reject casting a reference of type Light to type String or applying the instanceof operator, as shown at (2) and (3) in Example 7.12 References of the classes Light and TubeLight can refer to objects of the class TubeLight (or its subclasses) in the inheritance hierarchy depicted in Figure 7.2. Therefore, it makes sense to apply the instanceof operator or cast a reference of the type Light to the type TubeLight as shown at (4) and (5), respectively, in Example 7.12.

At runtime, the result of applying the instanceof operator at (4) is false, because the reference light1 of the class Light will actually denote an object of the subclass LightBulb, and this object cannot be denoted by a reference of the peer class TubeLight. Applying the cast at (5) results in a ClassCastException for the same reason. This is the reason why cast conversions are said to be unsafe, as they may throw a ClassCastException at runtime. Note that if the result of the instanceof operator is false, the cast involving the operands will also throw a ClassCastException.

In Example 7.12, the result of applying the instanceof operator at (6) is also false, because the reference light1 will still denote an object of the class LightBulb, whose objects cannot be denoted by a reference of its subclass SpotLightBulb. Thus applying the cast at (7) causes a ClassCastException to be thrown at runtime.

The situation shown at (8), (9), and (10) illustrates typical usage of the instanceof operator to determine what object a reference is denoting so that it can be cast for the purpose of carrying out some specific action. The reference light1 of the class Light is initialized to an object of the subclass NeonLight at (8). The result of the instanceof operator at (9) is true, because the reference light1 will denote an object of the subclass NeonLight, whose objects can also be denoted by a reference of its superclass TubeLight. By the same token, the cast at (10) is also valid. If the result of the instanceof operator is true, the cast involving the operands will also be valid.

Example 7.12 The instanceof and Cast Operators

class Light { /* ... */ }
class LightBulb extends Light { /* ... */ }
class SpotLightBulb extends LightBulb { /* ... */ }
class TubeLight extends Light { /* ... */ }
class NeonLight extends TubeLight { /* ... */ }

public class WhoAmI {
  public static void main(String[] args) {
    boolean result1, result2, result3, result4, result5;

    Light light1 = new LightBulb();                    // (1)
    //  String str = (String) light1;                  // (2) Compile-time error.
    //  result1 = light1 instanceof String;            // (3) Compile-time error.

    result2 = light1 instanceof TubeLight;             // (4) false. Peer class.
    //  TubeLight tubeLight1 = (TubeLight) light1;     // (5) ClassCastException.

    result3 = light1 instanceof SpotLightBulb;         // (6) false: Superclass
    //  SpotLightBulb spotRef = (SpotLightBulb) light1;// (7) ClassCastException

    light1 = new NeonLight();                          // (8)
    if (light1 instanceof TubeLight) {                 // (9) true
      TubeLight tubeLight2 = (TubeLight) light1;       // (10) OK
      // Can now use tubeLight2 to access an object of the class NeonLight,
      // but only those members that the object inherits or overrides
      // from the class TubeLight.
    }
  }
}

As we have seen, the instanceof operator effectively determines whether the reference value in the reference on the left-hand side refers to an object whose class is a subtype of the type of the reference specified on the right-hand side. At runtime, it is the type of the actual object denoted by the reference on the left-hand side that is compared with the type specified on the right-hand side. In other words, what matters at runtime is the type of the actual object denoted by the reference, not the declared type of the reference.

Example 7.13 provides more examples of the instanceof operator. It is instructive to go through the print statements and understand the results printed out. The literal null is not an instance of any reference type, as shown in the print statements (1), (2), and (16). An instance of a superclass is not an instance of its subclass, as shown in the print statement (4). An instance of a class is not an instance of a totally unrelated class, as shown in the print statement (10). An instance of a class is not an instance of an interface type that the class does not implement, as shown in the print statement (6). Any array of non-primitive type is an instance of both Object and Object[] types, as shown in the print statements (14) and (15), respectively.

Example 7.13 Using the instanceof Operator

interface IStack                      { /* From Example 7.7 */ }
interface ISafeStack extends IStack   { /* From Example 7.7 */ }
class StackImpl implements IStack     { /* From Example 7.7 */ }
class SafeStackImpl extends StackImpl
              implements ISafeStack   { /* From Example 7.7 */ }

public class Identification {
  public static void main(String[] args) {
    Object obj = new Object();
    StackImpl stack = new StackImpl(10);

    SafeStackImpl safeStack = new SafeStackImpl(5);
    IStack iStack;

    System.out.println("(1): " +
        (null instanceof Object));     // Always false.
    System.out.println("(2): " +
        (null instanceof IStack));     // Always false.

    System.out.println("(3): " +
        (stack instanceof Object));    // true: instance of subclass of Object.
    System.out.println("(4): " +
        (obj instanceof StackImpl));   // false: Object not subtype of StackImpl.
    System.out.println("(5): " +
        (stack instanceof StackImpl)); // true: instance of StackImpl.

    System.out.println("(6): " +
        (obj instanceof IStack));      // false: Object does not implement IStack.
    System.out.println("(7): " +
        (safeStack instanceof IStack));// true: SafeStackImpl implements IStack.

    obj = stack;                       // Assigning subclass to superclass.
    System.out.println("(8): " +
        (obj instanceof StackImpl));   // true: instance of StackImpl.
    System.out.println("(9): " +
        (obj instanceof IStack));      // true: StackImpl implements IStack.
    System.out.println("(10): " +
        (obj instanceof String));      // false: No relationship.

    iStack = (IStack) obj;        // Cast required: superclass assigned subclass.
    System.out.println("(11): " +
        (iStack instanceof Object));     // true: instance of subclass of Object.
    System.out.println("(12): " +
        (iStack instanceof StackImpl));  // true: instance of StackImpl.

    String[] strArray = new String[10];
    //  System.out.println("(13): " +
    //      (strArray instanceof String);// Compile-time error, no relationship.
    System.out.println("(14): " +
        (strArray instanceof Object));   // true: array subclass of Object.
    System.out.println("(15): " +
        (strArray instanceof Object[])); // true: array subclass of Object[].
    System.out.println("(16): " +
        (strArray[0] instanceof Object));// false: strArray[0] is null.
    System.out.println("(17): " +
        (strArray instanceof String[])); // true: array of String.
    strArray[0] = "Amoeba strip";
    System.out.println("(18): " +
        (strArray[0] instanceof String));// true: instance of String.
  }
}

Output from the program:

(1): false
(2): false

(3): true
(4): false
(5): true
(6): false
(7): true
(8): true
(9): true
(10): false
(11): true
(12): true
(14): true
(15): true
(16): false
(17): true
(18): true

Review Questions

Review Questions

7.20 Which statement about the program is true?

// Filename: MyClass.java
public class MyClass {
  public static void main(String[] args) {
    A[] arrA;
    B[] arrB;

    arrA = new A[10];
    arrB = new B[20];
    arrA = arrB;       // (1)
    arrB = (B[]) arrA; // (2)
    arrA = new A[10];
    arrB = (B[]) arrA; // (3)
  }
}

class A {}

class B extends A {}

Select the one correct answer.

(a) The program will fail to compile because of the assignment at (1).

(b) The program will throw a java.lang.ClassCastException in the assignment at (2), when run.

(c) The program will throw a java.lang.ClassCastException in the assignment at (3), when run.

(d) The program will compile and run without errors, even if the cast operator (B[]) in the statements at (2) and (3) is removed.

(e) The program will compile and run without errors, but will not do so if the cast operator (B[]) in statements at (2) and (3) is removed.

7.21 What is the label of the first line that will cause compilation to fail in the following program?

// Filename: MyClass.java
class MyClass {
  public static void main(String[] args) {
    MyClass a;
    MySubclass b;

    a = new MyClass();             // (1)
    b = new MySubclass();          // (2)

    a = b;                         // (3)
    b = a;                         // (4)

    a = new MySubclass();          // (5)
    b = new MyClass();             // (6)
  }
}

class MySubclass extends MyClass {}

Select the one correct answer.

(a) (1)

(b) (2)

(c) (3)

(d) (4)

(e) (5)

(f) (6)

7.22 Given the following type and reference declarations, which assignment is legal?

// Type declarations:
interface I1 {}
interface I2 {}
class C1 implements I1 {}
class C2 implements I2 {}
class C3 extends C1 implements I2 {}

// Reference declarations:
  C1 obj1;
  C2 obj2;
  C3 obj3;

Select the one correct answer.

(a) obj2 = obj1;

(b) obj3 = obj1;

(c) obj3 = obj2;

(d) I1 a = obj2;

(e) I1 b = obj3;

(f) I2 c = obj1;

7.23 Given the following class and reference declarations, what can be said about the statement y = (Sub) x?

// Class declarations:
class Super {}
class Sub extends Super {}

// Reference declarations:
  Super x;
  Sub y;

Select the one correct answer.

(a) Illegal at compile time.

(b) Legal at compile time, but might be illegal at runtime.

(c) Definitely legal at runtime, but the cast operator (Sub) is not strictly needed.

(d) Definitely legal at runtime, and the cast operator (Sub) is needed.

7.24 Given the following class declarations and declaration statements, which assignment is legal at compile time?

// Class declarations:
interface A {}
class B {}
class C extends B implements A {}
class D implements A {}

// Declaration statements:
  B b = new B();
  C c = new C();
  D d = new D();

Select the one correct answer.

(a) c = d;

(b) d = c;

(c) A a = d;

(d) d = (D) c;

(e) c = b;

7.25 Which letters will be printed when the following program is run?

// Filename: MyClass.java
public class MyClass {
  public static void main(String[] args) {
    B b = new C();
    A a = b;
    if (a instanceof A) System.out.println("A");
    if (a instanceof B) System.out.println("B");
    if (a instanceof C) System.out.println("C");
    if (a instanceof D) System.out.println("D");
  }
}

class A {}
class B extends A {}

class C extends B {}
class D extends C {}

Select the three correct answers.

(a) A will be printed.

(b) B will be printed.

(c) C will be printed.

(d) D will be printed.

7.26 Given three classes A, B, and C, where B is a subclass of A, and C is a subclass of B, which one of these boolean expressions is true only when an object denoted by reference o has actually been instantiated from class B, as opposed to from A or C?

Select the one correct answer.

(a) (o instanceof B) && (!(o instanceof A))

(b) (o instanceof B) && (!(o instanceof C))

(c) !((o instanceof A) || (o instanceof B))

(d) (o instanceof B)

(e) (o instanceof B) && !((o instanceof A) || (o instanceof C))

7.27 When run, the following program will print all the letters I, J, C, and D. True or false?

public class MyClass {
  public static void main(String[] args) {
    I x = new D();
    if (x instanceof I) System.out.println("I");
    if (x instanceof J) System.out.println("J");
    if (x instanceof C) System.out.println("C");
    if (x instanceof D) System.out.println("D");
  }
}

interface I{}
interface J{}
class C implements I {}
class D extends C implements J {}

Select the one correct answer.

(a) True.

(b) False.

7.28 What will be the result of compiling and running the following program?

public class RQ200_10 {
  public static void main(String[] args) {
    Integer iRef;
    iRef = 786;                                  //(1)
    iRef = (Integer)(2007 - 786);                //(2)
    iRef = (int)3.14;                            //(3)
    iRef = (Integer)3.14;                        //(4)
    iRef = (Integer)(int)3.14;                   //(5)

  }
}

Select the one correct answer.

(a) The code will fail to compile because of errors in at least one of the lines (1), (2), and (3).

(b) The code will fail to compile because of errors in both the lines (4) and (5).

(c) The code will fail to compile because of error in line (4).

(d) The code will fail to compile because of error in line (5).

(e) The code will compile, but throw a ClassCastException.

(f) The code will compile and execute normally.

7.29 What will the program print when compiled and run?

public class RQ200_60 {
  public static void main(String[] args) {
    Integer i = -10;
    Integer j = -10;
    System.out.print(i==j);
    System.out.print(i.equals(j));
    Integer n = 128;
    Integer m = 128;
    System.out.print(n==m);
    System.out.print(n.equals(m));
  }
}

Select the one correct answer.

(a) falsetruefalsetrue

(b) truetruetruetrue

(c) falsetruetruetrue

(d) truetruefalsetrue

(e) None of the above.

7.30 What will the program print when compiled and run?

public class RQ200_70 {
  public static void main(String[] args) {
    Integer i = new Integer(-10);
    Integer j = new Integer(-10);
    Integer k = -10;
    System.out.print(i==j);
    System.out.print(i.equals(j));
    System.out.print(i==k);
    System.out.print(i.equals(k));
  }
}

Select the one correct answer.

(a) falsetruefalsetrue

(b) truetruetruetrue

(c) falsetruetruetrue

(d) truetruefalsetrue

(e) None of the above.

7.31 Given:

public class RQ200_20 {
  private Map<String, Integer> accounts = new HashMap<String, Integer>();
  public int getBalance(String accountName) {
    Integer total = (Integer) accounts.get(accountName);      // (1)
    if (total == null) total = new Integer(0);                // (2)
    return total.intValue();                                  // (3)
  }
  public void setBalance(String accountName, int amount) {
    accounts.put(accountName, new Integer(amount));           // (4)
  }
}

Which statements can be replaced so that the program still compiles and runs without errors?

Select the three correct answers.

(a) Replace (1)–(3) with:

int total = accounts.get(accountName);
if (total == null) total = 0;
return total;

(b) Replace (1)–(3) with:

int total = accounts.get(accountName);
return total == null ? 0 : total;

(c) Replace (1)–(3) with:

return accounts.get(accountName);

(d) Replace (4) with:

accounts.put(accountName, amount);

(e) Replace (4) with:

accounts.put(accountName, amount.intValue());

7.32 What is the result of compiling and running the following program?

class YingYang {
  void yingyang(Integer i) {
    System.out.println("Integer: " + i);
  }

  void yingyang(Integer[] ints) {
    System.out.println("Integer[]: " + ints[0]);
  }

  void yingyang(Integer... ints) {
    System.out.println("Integer...: " + ints[0]);
  }
}

public class RQ800_50 {
  public static void main(String[] args) {
    YingYang yy = new YingYang();
    yy.yingyang(10);
    yy.yingyang(10,12);
    yy.yingyang(new Integer[] {10, 20});
    yy.yingyang(new Integer(10), new Integer(20));
  }
}

Select the one correct answer.

(a) The class YingYang does not compile because of errors.

(b) The program compiles and prints:

Integer: 10
Integer...: 10
Integer...: 10
Integer...: 10

(c) The program compiles and prints:

Integer: 10
Integer...: 10
Integer[]: 10
Integer...: 10

7.33 What is the result of compiling and running the following program?

public class RQ800_60 {
  static void printFirst(Integer... ints) {
    System.out.println("Integer...: " + ints[0]);
  }

  static void printFirst(Number... nums) {
    System.out.println("Number...: " + nums[0]);
  }

  static void printFirst(Object... objs) {
    System.out.println("Object...: " + objs[0]);
  }

  public static void main(String[] args) {
    printFirst(10);
    printFirst((byte)20);
    printFirst('3', '0'),
    printFirst("40");
    printFirst((short)50, 55);
    printFirst((Number[])new Integer[] {70, 75});
  }
}

Select the one correct answer.

(a) The program does not compile because of ambiguous method calls.

(b) The program compiles and prints:

Integer...: 10
Integer...: 20
Integer...: 3
Object...: 40
Integer...: 50
Number...: 70

(c) The program compiles and prints:

Integer...: 10
Number...: 20
Object...: 3
Object...: 40
Number...: 50
Number...: 70

(d) The program compiles and prints:

Integer...: 10
Integer...: 20
Integer...: 3
Object...: 40
Number...: 50
Number...: 70

7.34 What is the result of compiling and running the following program?

public class RQ800_80 {
     static String compute(long... ls)             { return "ONE"; }
     static String compute(Long... ls)             { return "TWO"; }
     static String compute(Integer i1, Integer i2) { return "THREE"; }
     static String compute(Long l1, Long l2)       { return "FOUR"; }
     static String compute(Number n1, Number n2)   { return "FIVE"; }

  public static void main(String[] args) {
       System.out.println(compute((byte)5, (byte)10) + ", " + compute(5, 10));
       System.out.println(compute(5L, 10) + ", " + compute(5L, 10L));
  }
}

Select the one correct answer.

(a) The program does not compile because of errors.

(b) The program compiles and prints:

THREE, THREE
FOUR, FOUR

(c) The program compiles and prints:

FIVE, THREE
FIVE, FOUR

(d) The program compiles and prints:

FIVE, THREE
ONE, TWO

(e) The program compiles and prints:

ONE, THREE
ONE, ONE

7.12 Polymorphism and Dynamic Method Lookup

Which object a reference will actually denote during runtime cannot always be determined at compile time. Polymorphism allows a reference to denote objects of different types at different times during execution. A supertype reference exhibits polymorphic behavior since it can denote objects of its subtypes.

When a non-private instance method is invoked on an object, the method definition actually executed is determined both by the type of the object at runtime and the method signature. Dynamic method lookup is the process of determining which method definition a method signature denotes during runtime, based on the type of the object. However, a call to a private instance method is not polymorphic. Such a call can only occur within the class and gets bound to the private method implementation at compile time.

The inheritance hierarchy depicted in Figure 7.4 is implemented in Example 7.14. The implementation of the method draw() is overridden in all subclasses of the class Shape. The invocation of the draw() method in the two loops at (3) and (4) in Example 7.14 relies on the polymorphic behavior of references and dynamic method lookup. The array shapes holds Shape references denoting a Circle, a Rectangle and a Square, as shown at (1). At runtime, dynamic lookup determines the draw() implementation to execute, based on the type of the object denoted by each element in the array. This is also the case for the elements of the array drawables at (2), which holds IDrawable references that can be assigned the reference value of any object of a class that implements the IDrawable interface. The first loop will still work without any change if objects of new subclasses of the class Shape are added to the array shapes. If they did not override the draw() method, an inherited version of the method would be executed. This polymorphic behavior applies to the array drawables, where the subtype objects are guaranteed to have implemented the IDrawable interface.

Figure 7.4 Type Hierarchy to Illustrate Polymorphism

Type Hierarchy to Illustrate Polymorphism

Polymorphism and dynamic method lookup form a powerful programming paradigm that simplifies client definitions, encourages object decoupling, and supports dynamically changing relationships between objects at runtime.

Example 7.14 Polymorphism and Dynamic Method Lookup

interface IDrawable {
  void draw();
}

class Shape implements IDrawable {
  public void draw() { System.out.println("Drawing a Shape."); }
}

class Circle extends Shape {
  public void draw() { System.out.println("Drawing a Circle."); }
}

class Rectangle extends Shape {
  public void draw() { System.out.println("Drawing a Rectangle."); }
}

class Square extends Rectangle {
  public void draw() { System.out.println("Drawing a Square."); }
}

class Map implements IDrawable {
  public void draw() { System.out.println("Drawing a Map."); }
}

public class PolymorphRefs {
  public static void main(String[] args) {
    Shape[] shapes = {new Circle(), new Rectangle(), new Square()};     // (1)
    IDrawable[] drawables = {new Shape(), new Rectangle(), new Map()};  // (2)

    System.out.println("Draw shapes:");
    for (Shape shape : shapes)                                          // (3)
      shape.draw();

    System.out.println("Draw drawables:");
    for (IDrawable drawable : drawables)                                // (4)
      drawable.draw();
  }
}

Output from the program:

Draw shapes:
Drawing a Circle.
Drawing a Rectangle.
Drawing a Square.
Draw drawables:
Drawing a Shape.
Drawing a Rectangle.
Drawing a Map.

7.13 Inheritance Versus Aggregation

Figure 7.5 is a UML class diagram showing several aggregation relationships and one inheritance relationship. The class diagram shows a queue defined by aggregation and a stack defined by inheritance. Both are based on linked lists. A linked list is defined by aggregation. A non-generic implementation of these data structures is shown in Example 7.15. The purpose of the example is to illustrate inheritance and aggregation, not industrial-strength implementation of queues and stacks. The class Node at (1) is straightforward, defining two fields: one denoting the data and the other denoting the next node in the list. The class LinkedList at (2) keeps track of the list by managing a head and a tail reference. Nodes can be inserted in the front or back, but deleted only from the front of the list.

Figure 7.5 Implementing Data Structures by Inheritance and Aggregation

Implementing Data Structures by Inheritance and Aggregation

Example 7.15 Implementing Data Structures by Inheritance and Aggregation

class Node {                                                   // (1)
  private Object data;    // Data
  private Node   next;    // Next node

  // Constructor for initializing data and reference to the next node.
  Node(Object data, Node next) {
    this.data = data;
    this.next = next;
  }

  // Methods:
  public void   setData(Object obj) { data = obj; }
  public Object getData()           { return data; }
  public void   setNext(Node node)  { next = node; }
  public Node   getNext()           { return next; }
}
//____________________________________________________
class LinkedList {                                             // (2)
  protected Node head = null;
  protected Node tail = null;

  // Methods:
  public boolean isEmpty() { return head == null; }
  public void insertInFront(Object dataObj) {
    if (isEmpty()) head = tail = new Node(dataObj, null);
    else head = new Node(dataObj, head);
  }
  public void insertAtBack(Object dataObj) {
    if (isEmpty())
      head = tail = new Node(dataObj, null);
    else {
      tail.setNext(new Node(dataObj, null));
      tail = tail.getNext();
    }
  }
  public Object deleteFromFront() {
    if (isEmpty()) return null;
    Node removed = head;
    if (head == tail) head = tail = null;
    else head = head.getNext();
    return removed.getData();
  }
}
//____________________________________________________
class QueueByAggregation {                                     // (3)
  private LinkedList qList;

  // Constructor
  QueueByAggregation() {
    qList = new LinkedList();
  }

  // Methods:
  public boolean isEmpty() { return qList.isEmpty(); }
  public void enqueue(Object item) { qList.insertAtBack(item); }
  public Object dequeue() {
    if (qList.isEmpty()) return null;
    return qList.deleteFromFront();
  }
  public Object peek() {
    return (qList.isEmpty() ? null : qList.head.getData());
  }
}
//____________________________________________________

class StackByInheritance extends LinkedList {                  // (4)
  public void push(Object item) { insertInFront(item); }
  public Object pop() {
    if (isEmpty()) return null;
    return deleteFromFront();
  }
  public Object peek() {
    return (isEmpty() ? null : head.getData());
  }
}
//____________________________________________________
public class Client {                                           // (5)
  public static void main(String[] args) {
    String string1 = "Queues are boring to stand in!";
    int length1 = string1.length();
    QueueByAggregation queue = new QueueByAggregation();
    for (int i = 0; i<length1; i++)
      queue.enqueue(new Character(string1.charAt(i)));
    while (!queue.isEmpty())
      System.out.print(queue.dequeue());
    System.out.println();

    String string2 = "!no tis ot nuf era skcatS";
    int length2 = string2.length();
    StackByInheritance stack = new StackByInheritance();
    for (int i = 0; i<length2; i++)
      stack.push(new Character(string2.charAt(i)));
    stack.insertAtBack(new Character('!'));                     // (6)
    while (!stack.isEmpty())
      System.out.print(stack.pop());
    System.out.println();
  }
}

Output from the program:

Queues are boring to stand in!
Stacks are fun to sit on!!

Choosing between inheritance and aggregation to model relationships can be a crucial design decision. A good design strategy advocates that inheritance should be used only if the relationship is-a is unequivocally maintained throughout the lifetime of the objects involved; otherwise, aggregation is the best choice. A role is often confused with an is-a relationship. For example, given the class Employee, it would not be a good idea to model the roles an employee can play (such as a manager or a cashier) by inheritance if these roles change intermittently. Changing roles would involve a new object to represent the new role every time this happens.

Code reuse is also best achieved by aggregation when there is no is-a relationship. Enforcing an artificial is-a relationship that is not naturally present is usually not a good idea. This is illustrated in Example 7.15 at (6). Since the class StackByInheritance at (4) is a subclass of the class LinkedList at (2), any inherited method from the superclass can be invoked on an instance of the subclass. Also, methods that contradict the abstraction represented by the subclass can be invoked, as shown at (6). Using aggregation in such a case results in a better solution, as demonstrated by the class QueueByAggregation at (3). The class defines the operations of a queue by delegating such requests to the underlying class LinkedList. Clients implementing a queue in this manner do not have access to the underlying class and, therefore, cannot break the abstraction.

Both inheritance and aggregation promote encapsulation of implementation, as changes to the implementation are localized to the class. Changing the contract of a superclass can have consequences for the subclasses (called the ripple effect) and also for clients who are dependent on a particular behavior of the subclasses.

Polymorphism is achieved through inheritance and interface implementation. Code relying on polymorphic behavior will still work without any change if new subclasses or new classes implementing the interface are added. If no obvious is-a relationship is present, polymorphism is best achieved by using aggregation with interface implementation.

7.14 Basic Concepts in Object-Oriented Design

In this section, we provide a brief explanation of some basic concepts in object-oriented (OO) design. The reader is encouraged to consult the vast body of literature that is readily available on this subject.

Encapsulation

An object has properties and behaviors that are encapsulated inside the object. The services it offers to its clients comprises its contract, or public interface. Only the contract defined by the object is available to the clients. The implementation of its properties and behavior is not a concern of the clients. Encapsulation helps to make clear the distinction between an object’s contract and implementation. This has major consequences for program development. The implementation of an object can change without implications for the clients. Encapsulation also reduces complexity, as the internals of an object are hidden from the clients who cannot alter its implementation.

Encapsulation is achieved through information hiding, by making judicious use of language features provided for this purpose. Information hiding in Java can be achieved at different levels of granularity:

• method or block level

Localizing information in a method hides it from the outside.

• class level

The accessibility of members declared in a class can be controlled through member accessibility modifiers. One much-advocated information-hiding technique is to prevent direct access by clients to data maintained by an object. The fields of the object are private and its contract defines public methods for the services provided by the object. Such tight encapsulation helps to separate the use from the implementation of a class.

• package level

Classes that belong together can be grouped into relevant packages by using the package statement. Inter-package accessibility of classes can be controlled by class accessibility modifiers.

Cohesion

Cohesion is an inter-class measure of how well-structured and closely-related the functionality is in a class. The objective is to design classes with high cohesion, that perform well-defined and related tasks (also called functional cohesion). The public methods of a highly cohesive class typically implement a single specific task that is related to the purpose of the class. For example, in an MVC-based application, the respective classes for the Model, the View, and the Controller should be focused on providing functionality that only relates to their individual purpose. A method in one class should not perform a task that should actually be implemented by one of the other two classes.

Lack of cohesion in a class means that the purpose of the class is not focused, and unrelated functionality is ending up in the class (also called coincidental cohesion)—which will eventually impact the maintainability of the application.

Coupling

Coupling is a measure of intra-class dependencies. Objects need to interact with each other, therefore dependencies between classes are inherent in OO design. However, these dependencies should be minimized in order to achieve loose coupling, which aids in creating extensible applications.

One major source of intra-class dependencies is the exposure of implementation details of an object. Such details can be utilized by other objects, and this dependency can impede changes in the implementation, resulting in less extensible applications.

High cohesion and loose coupling help to achieve the main goals of OO design: maintainability, reusability, extensibility, and reliability.

Review Questions

Review Questions

7.35 What will be the result of compiling and running the following program?

public class Polymorphism {
  public static void main(String[] args) {
    A ref1 = new C();
    B ref2 = (B) ref1;
    System.out.println(ref2.f());
  }
}

class A           { int f() { return 0; } }
class B extends A { int f() { return 1; } }
class C extends B { int f() { return 2; } }

Select the one correct answer.

(a) The program will fail to compile.

(b) The program will compile but will throw a ClassCastException, when run.

(c) The program will compile and print 0, when run.

(d) The program will compile and print 1, when run.

(e) The program will compile and print 2, when run.

7.36 What will be the result of compiling and running the following program?

public class Polymorphism2 {
  public static void main(String[] args) {
    A ref1 = new C();
    B ref2 = (B) ref1;
    System.out.println(ref2.g());
  }
}

class A {
  private int f() { return 0; }
  public int g() { return 3; }
}
class B extends A {
  private int f() { return 1; }
  public int g() { return f(); }
}
class C extends B {
  public int f() { return 2; }
}

Select the one correct answer.

(a) The program will fail to compile.

(b) The program will compile and print 0, when run.

(c) The program will compile and print 1, when run.

(d) The program will compile and print 2, when run.

(e) The program will compile and print 3, when run.

7.37 Which statements about the program are true?

public interface HeavenlyBody { String describe(); }

class Star {
  String starName;
  public String describe() { return "star " + starName; }
}

class Planet extends Star {
  String name;
  public String describe() {
    return "planet " + name + " orbiting star " + starName;
  }
}

Select the two correct answers:

(a) The code will fail to compile.

(b) The code defines a Planet is-a Star relationship.

(c) The code will fail to compile if the name starName is replaced with the name bodyName throughout the declaration of the Star class.

(d) The code will fail to compile if the name starName is replaced with the name name throughout the declaration of the Star class.

(e) An instance of Planet is a valid instance of HeavenlyBody.

7.38 Given the following code, which statement is true?

public interface HeavenlyBody { String describe(); }

class Star implements HeavenlyBody {
  String starName;
  public String describe() { return "star " + starName; }
}

class Planet {
  String name;
  Star orbiting;
  public String describe() {
    return "planet " + name + " orbiting " + orbiting.describe();
  }
}

Select the one correct answer:

(a) The code will fail to compile.

(b) The code defines a Planet has-a Star relationship.

(c) The code will fail to compile if the name starName is replaced with the name bodyName throughout the declaration of the Star class.

(d) The code will fail to compile if the name starName is replaced with the name name throughout the declaration of the Star class.

(e) An instance of Planet is a valid instance of a HeavenlyBody.

7.39 Which statement is not true?

Select the one correct answer.

(a) Maximizing cohesion and minimizing coupling are the hallmarks of a welldesigned application.

(b) Coupling is an inherent property of any non-trivial OO design.

(c) Adhering to the JavaBeans naming standard can aid in achieving encapsulation.

(d) Dependencies between classes can be minimized by hiding implementation details.

(e) Each method implementing a single task will result in a class that has high cohesion.

(f) None of the above.

Chapter Summary

Chapter Summary

The following information was included in this chapter:

• inheritance and its implications in OOP

• overriding and hiding of superclass members

• method overriding versus method overloading

• usage of the super reference to access superclass members

• usage of this() and super() calls, including constructor chaining

• interfaces and multiple interface inheritance

• subtype-supertype relationship

• conversions when assigning, casting, and passing reference values

• identifying the type of objects using the instanceof operator

• polymorphism and dynamic method lookup

• inheritance (is-a) versus aggregation (has-a)

• best practices for OO design: tight encapsulation, loose coupling, and high cohesion in classes.

Programming Exercises

Programming Exercises

7.1 Declare an interface called Function that has a method named evaluate that takes an int parameter and returns an int value.

Create a class called Half that implements the Function interface. The implementation of the method evaluate() should return the value obtained by dividing the int argument by 2.

In a client, create a method that takes an arbitrary array of int values as a parameter, and returns an array that has the same length, but the value of an element in the new array is half that of the value in the corresponding element in the array passed as the parameter. Let the implementation of this method create an instance of Half, and use this instance to calculate values for the array that is returned.

7.2 Rewrite the method that operated on arrays from the previous exercise: the method should now also accept a Function reference as an argument, and use this argument instead of an instance of the Half class.

Create a class called Print that implements the method evaluate() in the Function interface. This method simply prints the int value passed as argument, and returns this value.

Now, write a program that creates an array of int values from 1 to 10, and does the following:

• Prints the array using an instance of the Print class and the method described earlier.

• Halves the values in the array and prints the values again, using the Half and Print classes, and the method described above.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.207.206