Design with runtime class information

How to make use of runtime class information in Java programs

One of the cool things about Java's object model is that Java objects are "conscious": given a reference to an object, you can get information about that object's class. This "runtime class information" makes possible all kinds of interesting designs and implementation techniques. But how are we to use this capability?

When I think about Java's support for runtime class information, two clich├ęs come to my mind:

1. Information is power.

In our case, runtime class information gives power to the programmer because he or she has more information to work with. This is a good thing.

2. Power corrupts.

Alas, in our case, runtime class information can be abused in designs.

The question I attempt to answer in this article is: How can you take advantage of the cool runtime class information offered by Java objects without being accused of abuse of power?

How runtime class information works

Before I delve into guidelines, I'd like to give you some background on how runtime class information works in Java.

Every class or interface you write in the Java programming language defines a new type for your program to use. Once you define a new type, you can declare variables of that type. A type defines a set of operations that may be performed on variables of that kind and the meaning of those operations.

If you define a class Hippopotamus, for example, you then can declare variables of type Hippopotamus. On such variables, the Java virtual machine (JVM) will allow you to perform operations defined for type Hippopotamus, but won't allow any other operations. If class Hippopotamus declares a public takeBath() method, for example, the JVM will allow you to invoke takeBath() on a variable of type Hippopotamus.

When you compile your class or interface, the Java compiler (if it is fully pleased with your work) will give you a class file. The class file is an intermediate-compiled binary format for describing a Java type (a class or interface). The class file for Hippopotamus, for example, would contain all information needed to define that type, including things like:

  • The type's name (Hippopotamus)
  • The superclass's name
  • Any modifiers (such as public or abstract)
  • The number of interfaces the type directly implements
  • The names of those interfaces
  • The number of fields
  • For each field, a field descriptor (field name, type, modifiers)
  • The number of methods
  • For each method:
    • A method descriptor (method name, return type or void, number and types of parameters)
    • Modifiers (static, abstract, private, and so on)
    • The bytecodes
    • An exception table

When a JVM loads a type (a class or interface), the JVM stores information about that type in a logical portion of its memory called the method area. The type information stored in the method area corresponds to the information stored in the class file, but unlike the class file, the structure and organization of the information in the method area is not defined by the Java specifications. The designers of each JVM decide how to store the information they parse from Java class files in the method area of their JVM implementation.

For every type it loads, the JVM creates an instance of class java.lang.Class to "represent" the type to the application. Arrays, because they are full-class objects in Java, get Class objects too. (Every array of the same element type and dimension shares the same Class object.) A Class object gives you access to the information stored in the method area for the type the Class object represents.

Although the Java specifications don't define a layout or organization for objects on the heap -- that is the decision of each JVM designer -- the specifications require that object images be in some way connected to the type data for the object's class. One approach open to a JVM designer, for example, is to include a pointer into the method area as part of each object image. Given only a reference to an object, every JVM must be able to get at the type information stored in the method area for that object.

How to get some action

Now that you know that all this class information is available for every object on the heap, how do you get at it? Java gives you many ways to make use of the information stored in the method area:

  • Perform a cast
  • Use the instanceof operator
  • Invoke an instance method
  • Use java.lang.Class

Upcasts

Perhaps the simplest way to use runtime class information is to perform a cast. As one of Java's security mechanisms, JVMs check all casts at runtime for validity. If you have a reference of type Object and try to cast it to type Hippopotamus, for example, the JVM will in some way check to make sure the cast is valid. This check makes use of runtime class information. If the cast isn't valid, the JVM will throw a ClassCastException exception.

Casts, depending on how they are used, can help or hinder code flexibility. It is usually helpful to upcast -- to cast from a subtype to a supertype. A basic guideline for making flexible object-oriented designs is to treat objects as generically as possible -- to make the type of your variables as far up the inheritance hierarchy as possible.

If you have code that must manipulate a Hippopotamus object, for example, you can hold a reference to it in a variable of type Hippopotamus. But if your class Hippopotamus extends class Animal and what you are doing is something you might consider doing to any kind of animal, not just hippopotami, you should consider upcasting your reference to Animal. You could use that same code later on for instances of other subclasses of Animal, such as Cats or Dogs, not just Hippopotamus objects.

Downcasts and instanceof

In contrast to upcasts, downcasts (casts from a supertype to a subtype) can be more troublesome to flexibility. In general, instead of doing explicit downcasts, you should strive to let dynamic binding make polymorphism work for you.

Downcasts often are used in combination with the instanceof operator. instanceof is another of Java's mechanisms that use runtime class information. When the JVM executes an instanceof operation, it consults the runtime class information associated with an object reference to determine whether the object is or is not an instance of the specified class. Like downcasts, instanceof can potentially work against the goal of flexibility.

Here's an example showing how downcasting and instanceof can be used to the detriment of flexibility:

// In file rtci/ex5/Animal.java class Animal { //... }

// In file rtci/ex5/Dog.java class Dog extends Animal { public void woof() { System.out.println("Woof!"); } //... }

// In file rtci/ex5/Cat.java class Cat extends Animal { public void meow() { System.out.println("Meow!"); } //... }

// In file rtci/ex5/Hippopotamus.java class Hippopotamus extends Animal { public void roar() { System.out.println("Roar!"); } //... }

// In file rtci/ex5/Example5.java class Example5 {

public static void main(String[] args) {

makeItTalk(new Cat()); makeItTalk(new Dog()); makeItTalk(new Hippopotamus()); }

public static void makeItTalk(Animal animal) {

if (animal instanceof Cat) { Cat cat = (Cat) animal; cat.meow(); } else if (animal instanceof Dog) { Dog dog = (Dog) animal; dog.woof(); } else if (animal instanceof Hippopotamus) { Hippopotamus hippopotamus = (Hippopotamus) animal; hippopotamus.roar(); } } }

Although functionally the previous example is correct, I am hoping its design will trigger alarms in the object-oriented brains of most readers. The makeItTalk() method's use of instanceof and downcasting represent one of the fundamental ways runtime class information can be abused in Java programs.

The trouble with this approach is that if you add a new Animal subtype, say Orangutan, you would also have to add a new else-if clause to the makeItTalk() method. Polymorphism and dynamic binding will take care of this for you automatically. (Polymorphism means you can use a variable of a superclass type to hold a reference to an object whose class is the superclass or any of its subclasses. Dynamic binding means the JVM will decide at runtime which method implementation to invoke based on the class of the object. For more on these concepts, see Resources.)

Your object-oriented brain will hopefully be more comfortable with the next version of the program, in which downcasting is cast aside in favor of polymorphism and dynamic binding.

// In file rtci/ex6/Animal.java abstract class Animal { public abstract void talk(); //... }

// In file rtci/ex6/Dog.java class Dog extends Animal { public void talk() { System.out.println("Woof!"); } //... }

// In file rtci/ex6/Cat.java class Cat extends Animal { public void talk() { System.out.println("Meow!"); } //... }

// In file rtci/ex6/Hippopotamus.java class Hippopotamus extends Animal { public void talk() { System.out.println("Roar!"); } //... }

// In file rtci/ex6/Example6.java class Example6 {

public static void main(String[] args) {

makeItTalk(new Cat()); makeItTalk(new Dog()); makeItTalk(new Hippopotamus()); }

public static void makeItTalk(Animal animal) {

animal.talk(); } }

Because class Animal declares a talk() method that Dog, Cat, and Hippopotamus override, the makeItTalk() method only needs to invoke talk() on the Animal reference. Dynamic binding ensures that the correct version of talk() will be invoked. If the Animal passed to makeItTalk() is a Dog, makeItTalk() will invoke Dog's implementation of talk(), which says, "Woof!".

Polymorphism and dynamic binding enable you to write code that doesn't need to know about subtypes. You don't have to use runtime class information via the instanceof operator precisely because when you invoke an instance method, the JVM uses runtime class information to figure out which implementation of the method to execute.

Legitimate uses of downcast and instanceof

Although downcasting and instanceof can be abused as described above, they also have legitimate uses. One common use of a downcast is to cast an Object reference extracted from a Collection to a more specific subtype. Here's an example:

class Hippopotamus {

private String yawnAdjective;

Hippopotamus(String yawnAdjective) { this.yawnAdjective = yawnAdjective; }

void yawn() { System.out.println(yawnAdjective + " yawn!"); } }

// In file rtci/ex1/Example1.java import java.util.ArrayList; import java.util.Collection; import java.util.Iterator;

class Example1 {

public static void main(String[] args) {

ArrayList hippos = new ArrayList(); hippos.add(new Hippopotamus("Big")); hippos.add(new Hippopotamus("Little")); hippos.add(new Hippopotamus("Technicolor"));

makeHipposYawn(hippos); }

// Client must pass a collection of Hippopotami static void makeHipposYawn(Collection hippos) {

Iterator it = hippos.iterator(); while (it.hasNext()) { Hippopotamus hippo = (Hippopotamus) it.next(); hippo.yawn(); } } }

In this example, Iterator's next() method returns a reference to a Hippopotamus object, but the type of the reference is Object. The reference is immediately downcast to Hippopotamus, so that the yawn() method can be invoked on the object.

Note that instanceof was not needed here, because a precondition of the makeHipposYawn() method states that the Collection, passed in as parameter hippos, be full of Hippopotamus objects. Nevertheless, this code makes use of runtime class information because the JVM checks to see whether the cast is valid. In other words, the JVM makes sure the object referenced by the return value of next() really is a Hippopotamus. If not, the JVM will throw a ClassCastException and the makeHipposYawn() method will complete abruptly.

instanceof and Can you dance()?

The primary use of instanceof is to find out whether you can perform some kind of operation on an object. In the next example, class DancingHippopotamus (a subclass of Hippopotamus) declares a dance() method. The makeHipposDance() method of class Example2 assumes it is receiving a collection of hippopotami, but not necessarily DancingHippopotamus objects. It uses instanceof to find out whether each object in the collection is an instance of DancingHippopotamus objects. For each instance of DancingHippopotamus it finds, it downcasts the reference to DancingHippopotamus and invokes dance().

1 2 3 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more