Data Type of an Expression
If an expression produces a value, that value is of some particular data type. In some cases, it is possible to determine the exact type that is produced by an expression, based on the types of the literals, variables, and methods that an expression references. For those expressions that produce object references, it is typically only possible to determine the type of the referenced object when the expression is evaluated at runtime.
The types of many expressions are ambiguous because of the way Java data types are defined. There is no ambiguity for variables, array elements, and method return values of primitive types, however. These expressions always produce the exact types specified in their declarations.
There can be ambiguity when a variable, array element, or method return value is declared to have a class or interface reference type. The ambiguity exists because a class reference may actually refer to an object of the intended class or a subclass of that class. For example, consider a variable that is declared to contain a reference to a Number
object:
double square(Number n){ return n.doubleValue()*n.doubleValue(); }
When the Java compiler sees the variable n
used in an expression, it knows that the object that is referenced could be an Integer
, Long
, Float
, or Double
object because the java.lang
package defines those subclasses of Number
. It is also possible, however, that the variable refers to some other subclass of Number
defined elsewhere. All that the compiler can be certain of is that at runtime n
will refer to an object of a subclass of Number
. The variable n
cannot refer to a Number
object because Number
is an abstract
class, so there are no Number
objects.
The one exception to the ambiguity of class-type object references occurs when the class used to declare a variable, array element, or method return type is a final
class. If a class is declared to be final
, it cannot be subclassed, so there is no ambiguity.
Ambiguity also exists if the type of a reference is an interface type, since the reference can refer to an object of any class that implements the interface. The actual class is not usually known until runtime.
The fact that the type of value produced by an object reference expression cannot be determined until it is evaluated at runtime can affect the evaluation of other expressions in the following ways:
- If a method is called through an object reference expression, the actual method to be called may depend on the type of the object. The selection of the appropriate method in the object is made at compile-time. For example,
f.read()
causes the selection of a method namedread()
that takes no arguments.However, if the compiler cannot determine the actual class of the object, the actual method to be called is determined at runtime, when the class is known. The compiler generates code to handle a runtime selection process called dynamic method lookup. The process begins by searching the actual class for an appropriate method. If there is no such method, the superclass of the class is searched, followed by its superclass and on up the inheritance hierarchy, until an appropriate method is found. This process ensures that the appropriate method gets called, even if the actual class of the object is a subclass of the type used for the object reference.
Even if the compiler cannot determine the actual class of the object, there is one case in which it does not need to generate code to handle dynamic method lookup. When the compiler selects the appropriate method in the object, if it finds that the method is declared
final
, it can be sure that it is the method to be called. - The success of a cast operation may need to be determined at runtime. When a class-type object reference is cast to another class, the operation can only succeed if the actual class of the object is the same class or a subclass of the class being cast to. If the compiler cannot determine the actual class of the object, it generates runtime code that can verify that the cast is permitted. If the actual class of the object at runtime makes the cast illegal, a
ClassCastException
is thrown. - The value produced by the
instanceof
operator may need to be determined at runtime. If the compiler cannot determine the type of the left operand in aninstanceof
expression, it generates runtime code to decide whether the expression producestrue
orfalse
. - The legality of an assignment to an array element may need to be determined at runtime. If a variable contains a reference to an array and the type of elements in the array is specified with a class or an interface name, it may not be possible to determine the actual type of the array elements until runtime. Consider the following example:
void foo (InputStream a[]) { a[0] = new FileInputStream("/dev/null"); }
Any array with elements that contain references to objects of class
InputStream
or any of its subclasses can be passed to the methodfoo()
shown above.FileInputStream f[] = new FileInputStream[3]; foo(f);
Since
FileInputStream
is a subclass ofInputStream
, the call tofoo()
does not cause any type-related problems at runtime. However, the following call tofoo()
does cause problems:DataInputStream f[] = new DataInputStream[3]; foo(f);
This call causes an
ArrayStoreException
to be thrown at runtime. AlthoughDataInputStream
is a subclass ofInputStream
, it is not a superclass ofFileInputStream
, so the assignment is not legal. - The type of object thrown by a
throw
statement may need to be determined at runtime. If the object thrown by athrow
statement is obtained through a reference that comes from a variable, an array element, or a method return value, the compiler generates runtime code that determines the type of the object that is thrown. In addition, this runtime code determines whether or not the object is caught.
References Array Types; Assignment Operators; Casts; Class Types; Interface Types; Method Call Expression; The instanceof Operator; The throw Statement