dcsimg
 

The need for RTTI

Thursday Mar 1st 2001

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 :

Bruce Eckel's Thinking in Java Contents | Prev | Next

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 the bulk of your code to manipulate handles to the base type ( Shape, in this case), so if you decide to extend the program by adding a new class ( Rhomboid, derived from Shape, for example), 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 handle. draw( ) is overridden in all of the derived classes, and because it is a dynamically bound method, the proper behavior will occur even though it is called through a generic Shape handle. 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 handle in the rest of the program.

As a brief review of polymorphism and upcasting, you might code the above example as follows: (See page 97 if you have trouble executing this program.)

//: Shapes.java
package c11;
import java.util.*;
 
interface Shape {
  void draw();
}
 
class Circle implements Shape {
  public void draw() {
    System.out.println("Circle.draw()");
  }
}
 
class Square implements Shape {
  public void draw() {
    System.out.println("Square.draw()");
  }
}
 
class Triangle implements Shape {
  public void draw() {
    System.out.println("Triangle.draw()");
  }
}
 
public class Shapes {
  public static void main(String[] args) {
    Vector s = new Vector();
    s.addElement(new Circle());
    s.addElement(new Square());
    s.addElement(new Triangle());
    Enumeration e = s.elements();
    while(e.hasMoreElements())
      ((Shape)e.nextElement()).draw();
  }
} ///:~ 

The base class could be coded as an interface, an abstract class, or an ordinary class. Since Shape has no concrete members (that is, members with definitions), and it’s not intended that you ever create a plain Shape object, the most appropriate and flexible representation is an interface. It’s also cleaner because you don’t have all those abstract keywords lying about.

Each of the derived classes overrides the base-class draw method so it behaves differently. In main( ), specific types of Shape are created and then added to a Vector. This is the point at which the upcast occurs because the Vector holds only Objects. Since everything in Java (with the exception of primitives) is an Object, a Vector can also hold Shape objects. But during an upcast to Object, it also loses any specific information, including the fact that the objects are shapes. To the Vector, they are just Objects.

At the point you fetch an element out of the Vector with nextElement( ), things get a little busy. Since Vector holds only Objects, nextElement( ) naturally produces an Object handle. But we know it’s really a Shape handle, 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, since in Java 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 Vector 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 method that’s called for the Shape is determined by whether the handle 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 the 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 handle? 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. This is what RTTI accomplishes: you can ask a handle to a Shape exactly what type it’s referring to.

The Class object

To understand how RTTI works in Java, you must first know how type information is represented at run time. This is accomplished through a special kind of object called the Class object, which contains information about the class. (This is sometimes called a meta-class.) In fact, the Class object is used to create all of the “regular” objects of your class.

There’s a Class object for each class that is part of your program. That is, each time you write a new class, a single Class object is also created (and stored, appropriately enough, in an identically named .class file). At run time, when you want to make an object of that class, the Java Virtual Machine (JVM) that’s executing your program first checks to see if the Class object for that type is loaded. If not, the JVM loads it by finding the .class file with that name. Thus, a Java program isn’t completely loaded before it begins, which is different from many traditional languages.

Once the Class object for that type is in memory, it is used to create all objects of that type.

If this seems shadowy or if you don’t really believe it, here’s a demonstration program to prove it:

//: SweetShop.java
// Examination of the way the class loader works
 
class Candy {
  static {
    System.out.println("Loading Candy");
  }
}
 
class Gum {
  static {
    System.out.println("Loading Gum");
  }
}
 
class Cookie {
  static {
    System.out.println("Loading Cookie");
  }
}
 
public class SweetShop {
  public static void main(String[] args) {
    System.out.println("inside main");
    new Candy();
    System.out.println("After creating Candy");
    try {
      Class.forName("Gum");
    } catch(ClassNotFoundException e) {
      e.printStackTrace();
    }
    System.out.println(
      "After Class.forName(\"Gum\")");
    new Cookie();
    System.out.println("After creating Cookie");
  }
} ///:~ 

Each of the classes Candy, Gum, and Cookie has a static clause that is executed as the class is loaded for the first time. Information will be printed out to tell you when loading occurs for that class. In main( ), the object creations are spread out between print statements to help detect the time of loading.

A particularly interesting line is:

Class.forName("Gum");

This method is a static member of Class (to which all Class objects belong). A Class object is like any other object and so you can get and manipulate a handle to it. (That’s what the loader does.) One of the ways to get a handle to the Class object is forName( ), which takes a String containing the textual name (watch the spelling and capitalization!) of the particular class you want a handle for. It returns a Class handle.

The output of this program for one JVM is:

inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie

You can see that each Class object is loaded only when it’s needed, and the static initialization is performed upon class loading.

Interestingly enough, a different JVM yielded:

Loading Candy
Loading Cookie
inside main
After creating Candy
Loading Gum
After Class.forName("Gum")
After creating Cookie

It appears that this JVM anticipated the need for Candy and Cookie by examining the code in main( ), but could not see Gum because it was created by a call to forName( ) and not through a more typical call to new. While this JVM produces the desired effect because it does get the classes loaded before they’re needed, it’s uncertain whether the behavior shown is precisely correct.

Class literals
In Java 1.1 you have a second way to produce the handle to the Class object: use the class literal . In the above program this would look like:

Gum.class;

which is not only simpler, but also safer since it’s checked at compile time. Because it eliminates the method call, it’s also more efficient.

Class literals work with regular classes as well as interfaces, arrays, and primitive types. In addition, there’s a standard field called TYPE that exists for each of the primitive wrapper classes. The TYPE field produces a handle to the Class object for the associated primitive type, such that:

... is equivalent to ...

boolean.class

Boolean.TYPE

char.class

Character.TYPE

byte.class

Byte.TYPE

short.class

Short.TYPE

int.class

Integer.TYPE

long.class

Long.TYPE

float.class

Float.TYPE

double.class

Double.TYPE

void.class

Void.TYPE

Checking before a cast

So far, you’ve seen RTTI forms including:

  1. The classic cast, e.g. “ (Shape),” which uses RTTI to make sure the cast is correct and throws a ClassCastException if you’ve performed a bad cast.
  2. The Class object representing the type of your object. The Class object can be queried for useful runtime information.
In C++, the classic cast “ (Shape)” does not perform RTTI. It simply tells the compiler to treat the object as the new type. In Java, which does perform the type check, this cast is often called a “type safe downcast.” The reason for the term “downcast” is the historical arrangement of the class hierarchy diagram. If casting a Circle to a Shape is an upcast, then casting a Shape to a Circle is a downcast. However, you know a Circle is also a Shape, and the compiler freely allows an upcast assignment, but you don’t know that a Shape is necessarily a Circle, so the compiler doesn’t allow you to perform a downcast assignment without using an explicit cast.

There’s a third form of RTTI in Java. This is the keyword instanceof that tells you if an object is an instance of a particular type. It returns a boolean so you use it in the form of a question, like this:

if(x instanceof Dog)
  ((Dog)x).bark();

The above if statement checks to see if the object x belongs to the class Dog before casting x to a Dog. It’s important to use instanceof before a downcast when you don’t have other information that tells you the type of the object; otherwise you’ll end up with a ClassCastException.

Ordinarily, you might be hunting for one type (triangles to turn purple, for example), but the following program shows how to tally all of the objects using instanceof.

//: PetCount.java
// Using instanceof
package c11.petcount;
import java.util.*;
 
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
 
class Counter { int i; }
 
public class PetCount {
  static String[] typenames = {
    "Pet", "Dog", "Pug", "Cat",
    "Rodent", "Gerbil", "Hamster",
  };
  public static void main(String[] args) {
    Vector pets = new Vector();
    try {
      Class[] petTypes = {
        Class.forName("c11.petcount.Dog"),
        Class.forName("c11.petcount.Pug"),
        Class.forName("c11.petcount.Cat"),
        Class.forName("c11.petcount.Rodent"),
        Class.forName("c11.petcount.Gerbil"),
        Class.forName("c11.petcount.Hamster"),
      };
      for(int i = 0; i < 15; i++)
        pets.addElement(
          petTypes[
            (int)(Math.random()*petTypes.length)]
            .newInstance());
    } catch(InstantiationException e) {}
      catch(IllegalAccessException e) {}
      catch(ClassNotFoundException e) {}
    Hashtable h = new Hashtable();
    for(int i = 0; i < typenames.length; i++)
      h.put(typenames[i], new Counter());
    for(int i = 0; i < pets.size(); i++) {
      Object o = pets.elementAt(i);
      if(o instanceof Pet)
        ((Counter)h.get("Pet")).i++;
      if(o instanceof Dog)
        ((Counter)h.get("Dog")).i++;
      if(o instanceof Pug)
        ((Counter)h.get("Pug")).i++;
      if(o instanceof Cat)
        ((Counter)h.get("Cat")).i++;
      if(o instanceof Rodent)
        ((Counter)h.get("Rodent")).i++;
      if(o instanceof Gerbil)
        ((Counter)h.get("Gerbil")).i++;
      if(o instanceof Hamster)
        ((Counter)h.get("Hamster")).i++;
    }
    for(int i = 0; i < pets.size(); i++)
      System.out.println(
        pets.elementAt(i).getClass().toString());
    for(int i = 0; i < typenames.length; i++)
      System.out.println(
        typenames[i] + " quantity: " +
        ((Counter)h.get(typenames[i])).i);
  }
} ///:~ 

There’s a rather narrow restriction on instanceof in Java 1.0: You can compare it to a named type only, and not to a Class object. In the example above you might feel that it’s tedious to write out all of those instanceof expressions, and you’re right. But in Java 1.0 there is no way to cleverly automate it by creating a Vector of Class objects and comparing it to those instead. This isn’t as great a restriction as you might think, because you’ll eventually understand that your design is probably flawed if you end up writing a lot of instanceof expressions.

Of course this example is contrived – you’d probably put a static data member in each type and increment it in the constructor to keep track of the counts. You would do something like that if you had control of the source code for the class and could change it. Since this is not always the case, RTTI can come in handy.

Using class literals
It’s interesting to see how the PetCount.java example can be rewritten using Java 1.1 class literals. The result is cleaner in many ways:

//: PetCount2.java
// Using Java 1.1 class literals
package c11.petcount2;
import java.util.*;
 
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
 
class Counter { int i; }
 
public class PetCount2 {
  public static void main(String[] args) {
    Vector pets = new Vector();
    Class[] petTypes = {
      // Class literals work in Java 1.1+ only:
      Pet.class,
      Dog.class,
      Pug.class,
      Cat.class,
      Rodent.class,
      Gerbil.class,
      Hamster.class,
    };
    try {
      for(int i = 0; i < 15; i++) {
        // Offset by one to eliminate Pet.class:
        int rnd = 1 + (int)(
          Math.random() * (petTypes.length - 1));
        pets.addElement(
          petTypes[rnd].newInstance());
      }
    } catch(InstantiationException e) {}
      catch(IllegalAccessException e) {}
    Hashtable h = new Hashtable();
    for(int i = 0; i < petTypes.length; i++)
      h.put(petTypes[i].toString(),
        new Counter());
    for(int i = 0; i < pets.size(); i++) {
      Object o = pets.elementAt(i);
      if(o instanceof Pet)
        ((Counter)h.get(
          "class c11.petcount2.Pet")).i++;
      if(o instanceof Dog)
        ((Counter)h.get(
          "class c11.petcount2.Dog")).i++;
      if(o instanceof Pug)
        ((Counter)h.get(
          "class c11.petcount2.Pug")).i++;
      if(o instanceof Cat)
        ((Counter)h.get(
          "class c11.petcount2.Cat")).i++;
      if(o instanceof Rodent)
        ((Counter)h.get(
          "class c11.petcount2.Rodent")).i++;
      if(o instanceof Gerbil)
        ((Counter)h.get(
          "class c11.petcount2.Gerbil")).i++;
      if(o instanceof Hamster)
        ((Counter)h.get(
          "class c11.petcount2.Hamster")).i++;
    }
    for(int i = 0; i < pets.size(); i++)
      System.out.println(
        pets.elementAt(i).getClass().toString());
    Enumeration keys = h.keys();
    while(keys.hasMoreElements()) {
      String nm = (String)keys.nextElement();
      Counter cnt = (Counter)h.get(nm);
      System.out.println(
        nm.substring(nm.lastIndexOf('.') + 1) + 
        " quantity: " + cnt.i);
    }
  }
} ///:~ 

Here, the typenames array has been removed in favor of getting the type name strings from the Class object. Notice the extra work for this: the class name is not, for example, Gerbil, but instead c11.petcount2.Gerbil since the package name is included. Notice also that the system can distinguish between classes and interfaces.

You can also see that the creation of petTypes does not need to be surrounded by a try block since it’s evaluated at compile time and thus won’t throw any exceptions, unlike Class.forName( ).

When the Pet objects are dynamically created, you can see that the random number is restricted so it is between 1 and petTypes.length and does not include zero. That’s because zero refers to Pet.class, and presumably a generic Pet object is not interesting. However, since Pet.class is part of petTypes the result is that all of the pets get counted.

A dynamic instanceof

Java 1.1 has added the isInstance method to the class Class. This allows you to dynamically call the instanceof operator, which you could do only statically in Java 1.0 (as previously shown). Thus, all those tedious instanceof statements can be removed in the PetCount example:

//: PetCount3.java
// Using Java 1.1 isInstance()
package c11.petcount3;
import java.util.*;
 
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
 
class Counter { int i; }
 
public class PetCount3 {
  public static void main(String[] args) {
    Vector pets = new Vector();
    Class[] petTypes = {
      Pet.class,
      Dog.class,
      Pug.class,
      Cat.class,
      Rodent.class,
      Gerbil.class,
      Hamster.class,
    };
    try {
      for(int i = 0; i < 15; i++) {
        // Offset by one to eliminate Pet.class:
        int rnd = 1 + (int)(
          Math.random() * (petTypes.length - 1));
        pets.addElement(
          petTypes[rnd].newInstance());
      }
    } catch(InstantiationException e) {}
      catch(IllegalAccessException e) {}
    Hashtable h = new Hashtable();
    for(int i = 0; i < petTypes.length; i++)
      h.put(petTypes[i].toString(),
        new Counter());
    for(int i = 0; i < pets.size(); i++) {
      Object o = pets.elementAt(i);
      // Using isInstance to eliminate individual
      // instanceof expressions:
      for (int j = 0; j < petTypes.length; ++j)
        if (petTypes[j].isInstance(o)) {
          String key = petTypes[j].toString();
          ((Counter)h.get(key)).i++;
        }
    }
    for(int i = 0; i < pets.size(); i++)
      System.out.println(
        pets.elementAt(i).getClass().toString());
    Enumeration keys = h.keys();
    while(keys.hasMoreElements()) {
      String nm = (String)keys.nextElement();
      Counter cnt = (Counter)h.get(nm);
      System.out.println(
        nm.substring(nm.lastIndexOf('.') + 1) + 
        " quantity: " + cnt.i);
    }
  }
} ///:~ 

You can see that the Java 1.1 isInstance( ) method has eliminated the need for the instanceof expressions. In addition, this means that you can add new types of pets simply by changing the petTypes array; the rest of the program does not need modification (as it did when using the instanceof expressions).

Contents | Prev | Next
Home
Mobile Site | Full Site
Copyright 2018 © QuinStreet Inc. All Rights Reserved