6.6 Assigning, Passing, and Casting Reference Values

Reference values, like primitive values, can be assigned, cast, and passed as arguments. For values of the primitive data types and reference types, conversions can occur during

  • assignment

  • parameter passing

  • explicit 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 conversions up the type hierarchy are permitted (upcasting), but conversions down the hierarchy require explicit casting (downcasting). In other words, conversions that are from a subtype to its supertypes are allowed, other conversions require an explicit cast or are illegal. There is no notion of promotion for reference values.

Reference Value Assignment Conversions

Reference value assignments are generally permitted up the type hierarchy, with implicit conversion of the source reference value to that of the destination reference type.

Example 6.11 Assigning and Passing Reference Values
interface IStack                      { /* From Example 6.9 */ }
interface ISafeStack extends IStack   { /* From Example 6.9 */ }
class StackImpl implements IStack     { /* From Example 6.9 */ }
class SafeStackImpl extends StackImpl
              implements ISafeStack   { /* From Example 6.9 */ }

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) Sub- to 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

        // Parameter Conversion
        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, then 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 6.11.

  • If SourceType is a class type, then the reference value in srcRef may be assigned to the destRef reference, provided 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 SourceType is an interface type, then the reference value in srcRef may be assigned to the destRef reference, provided 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 SourceType is an array type, then the reference value in srcRef may be assigned to the destRef reference, provided DestinationType is one of the following:

    • DestinationType is Object.

    • DestinationType is an array type, where the element type of SourceType is assignable to the element type of 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 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).

Parameter Passing Conversions

The rules for reference value assignment conversion also apply for parameter passing conversions. This is reasonable, as parameters in Java are passed by value (see Section 3.19, p. 89), requiring that values of actual parameters must be assignable to formal parameters of compatible types.

In Example 6.11, 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 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 6.11 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 one-dimensional array of a class or interface type (see the Class.getName() method in the API documentation).

The parameter passing conversion rules are useful in creating generic data types that can handle objects of arbitrary types. The classes in the java.util package make heavy use of Object as parameter type in their methods, to implement collections that can hold arbitrary objects (see Chapter 11).

Reference Casting and instanceof Operator

The expression to cast a <reference> of <source type> to <destination type> has the following syntax:


(<destination type>) <reference>

A cast expression checks that the reference value of the object denoted by the <reference> is assignable to a reference of the <destination type>, that is, that the <source type> is compatible to the <destination type>. If this is not the case, a ClassCastException is thrown. The null reference value can be cast to any reference type.

The binary instanceof operator has the following syntax (note that the keyword is composed of only lowercase letters):


<reference> instanceof <destination type>

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

The compile-time check determines whether a reference of <source type> and a reference of <destination type> can denote objects of a reference type that is 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, it is the type of the actual object denoted by the <reference> that determines the outcome of the operation.

With <source type> and <destination type> as classes Light and String, 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 6.12. With <source type> and <destination type> as classes Light and TubeLight, respectively, references of Light and TubeLight can denote objects of class TubeLight (or its subclasses) in the inheritance hierarchy depicted in Figure 6.3. Therefore, it makes sense to apply the instanceof operator or cast a reference of type Light to type TubeLight, as shown at (4) and (5), respectively, in Example 6.12.

At runtime, the result of applying the instanceof operator at (4) is false because the reference light1 of class Light will actually denote an object of 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, then the cast involving the operands will throw a ClassCastException.

In Example 6.12, the result of applying the instanceof operator at (6) is also false, because the reference light1 will still denote an object of 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 special action. The reference light1 of class Light is initialized to an object of subclass NeonLight at (8). The result of the instanceof operator at (9) is true, because the reference light1 will denote an object of 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, then the cast involving the operands will always be valid.

Example 6.12 instanceof and Cast Operator
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 object of class NeonLight.
        }
    }
}

As we have seen, the instanceof operator effectively determines whether the reference value of the object denoted by the reference on the left-hand side is assignable to a reference of the type that is specified on the right-hand side. Note that an instance of a subtype is-an instance of its supertypes. 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 is the type of the actual object denoted by the reference at runtime, not the type of the reference.

Example 6.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 6.13 Using instanceof Operator
interface IStack                      { /* From Example 6.9 */ }
interface ISafeStack extends IStack   { /* From Example 6.9 */ }
class StackImpl implements IStack     { /* From Example 6.9 */ }
class SafeStackImpl extends StackImpl
              implements ISafeStack   { /* From Example 6.9 */ }

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): " +         // true: instance of subclass of
            (stack instanceof Object));      //       Object.
        System.out.println("(4): " +
            (obj instanceof StackImpl));     // false: Downcasting.
        System.out.println("(5): " +
            (stack instanceof StackImpl));   // true: instance of StackImpl.

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

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

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

        String[] strArray = new String[10];
    //  System.out.println("(13): " +        // Compile-time error,
    //      (strArray instanceof String);    // 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.
        strArray[0] = "Amoeba strip";
        System.out.println("(17): " +
            (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

Converting References of Class and Interface Types

References of an interface type can be declared and these can denote objects of classes that implement this interface. This is another example of upcasting. Note that converting a reference value of interface type to the type of the class implementing the interface, requires explicit casting. This is an example of downcasting. The following code illustrates these cases:

IStack    istackOne = new StackImpl(5);                // Upcasting
StackImpl stackTwo  = (StackImpl) istackOne;         // Downcasting

Using the reference istackOne of interface type IStack, methods of the IStack interface can be invoked on objects of the StackImpl class that implements this interface. However, the additional members of the StackImpl class cannot be accessed via this reference without first casting it to the StackImpl class:

Object obj1 = istackOne.pop();        // OK. Method in IStack interface.
Object obj2 = istackOne.peek();       // Not OK. Method not in IStack interface.
Object obj3 = ((StackImpl) istackOne).peek(); // OK. Method in StackImpl class.