10:
Detecting Types
The idea of run-time type identification (RTTI) seems fairly simple at first: It lets you find the exact type of an object when you have only a reference to the base type.
However, the need for RTTI uncovers a whole plethora of interesting (and often perplexing) OO design issues, and raises fundamental questions of how you should structure your programs.
This chapter looks at the ways that Java allows you to discover information about objects and classes at run time. This takes two forms: “Traditional” RTTI, which assumes that you have all the types available at compile time and run time, and the “reflection” mechanism, which allows you to discover class information solely at run time. The “traditional” RTTI will be covered first, followed by a discussion of reflection.
The need for RTTI
Consider the now familiar example of a class hierarchy that uses polymorphism. The generic type is the base class Shape, and the specific derived types are Circle, Square, and Triangle:
This is a typical class hierarchy diagram, with the base class at the top and the derived classes growing downward. The normal goal in object-oriented programming is for your code to manipulate references to the base type (Shape, in this case), so if you decide to extend the program by adding a new class (such as Rhomboid, derived from Shape), the bulk of the code is not affected. In this example, the dynamically bound method in the Shape interface is draw( ), so the intent is for the client programmer to call draw( ) through a generic Shape reference. In all of the derived classes, draw( ) is overridden, and because it is a dynamically bound method, the proper behavior will occur even though it is called through a generic Shape reference. That’s polymorphism.
Thus, you generally create a specific object (Circle, Square, or Triangle), upcast it to a Shape (forgetting the specific type of the object), and use that anonymous Shape reference in the rest of the program.
As a brief review of polymorphism and upcasting, you might code the preceding example as follows:
//: c10:Shapes.java
import com.bruceeckel.simpletest.*;
class Shape {
void draw() { System.out.println(this + ".draw()"); }
}
class Circle extends Shape {
public String toString() { return "Circle"; }
}
class Square extends Shape {
public String toString() { return "Square"; }
}
class Triangle extends Shape {
public String toString() { return "Triangle"; }
}
public class Shapes {
private static Test monitor = new Test();
public static void main(String[] args) {
// Array of Object, not Shape:
Object[] shapeList = {
new Circle(),
new Square(),
new Triangle()
};
for(int i = 0; i < shapeList.length; i++)
((Shape)shapeList[i]).draw(); // Must cast
monitor.expect(new String[] {
"Circle.draw()",
"Square.draw()",
"Triangle.draw()"
});
}
} ///:~
The base class contains a draw( ) method that indirectly uses toString( ) to print an identifier for the class by passing this to System.out.println( ). If that method sees an object, it automatically calls the toString( ) method to produce a String representation. Each of the derived classes overrides the toString( ) method (from Object) so that draw( ) ends up (polymorphically) printing something different in each case.
In main( ), specific types of Shape are created and added to an array. This array is a bit odd because it isn’t an array of Shape (although it could be), but instead an array of the root class Object. The reason for this is to start preparing you for Chapter 11, which presents tools called collections (also called containers), whose sole job is to hold and manage other objects for you. However, to be generally useful these collections need to hold anything. Therefore they hold Objects. So an array of Object will demonstrate an important issue that you will encounter in the Chapter 11 collections.
In this example, the upcast occurs when the shape is placed in the array of Objects. Since everything in Java (with the exception of primitives) is an Object, an array of Objects can also hold Shape objects. But during the upcast to Object, the fact is lost that the objects are Shapes. To the array, they are just Objects.
At the point that you fetch an element out of the array with the index operator, things get a little busy. Since the array holds only Objects, indexing naturally produces an Object reference. But we know it’s really a Shape reference, and we want to send Shape messages to that object. So a cast to Shape is necessary using the traditional “(Shape)” cast. This is the most basic form of RTTI, because all casts are checked at run time for correctness. That’s exactly what RTTI means: at run time, the type of an object is identified.
In this case, the RTTI cast is only partial: The Object is cast to a Shape, and not all the way to a Circle, Square, or Triangle. That’s because the only thing we know at this point is that the array is full of Shapes. At compile time, this is enforced only by your own self-imposed rules, but at run time the cast ensures it.
Now polymorphism takes over and the exact code that’s executed for the Shape is determined by whether the reference is for a Circle, Square, or Triangle. And in general, this is how it should be; you want the bulk of your code to know as little as possible about specific types of objects, and to just deal with the general representation of a family of objects (in this case, Shape). As a result, your code will be easier to write, read, and maintain, and your designs will be easier to implement, understand, and change. So polymorphism is a general goal in object-oriented programming.
But what if you have a special programming problem that’s easiest to solve if you know the exact type of a generic reference? For example, suppose you want to allow your users to highlight all the shapes of any particular type by turning them purple. This way, they can find all the triangles on the screen by highlighting them. Or perhaps your method needs to “rotate” a list of shapes, but it makes no sense to rotate a circle so you’d like to skip only the circle, objects. With RTTI, you can ask a Shape reference the exact type that it’s referring to, and thus select and isolate special cases.