Designing with Runtime Class Information

Using Runtime Class Information in Java Programs

by Bill Venners
January 15, 1999

First published in JavaWorld, January 1999
Summary
This article gives advice on using runtime class information in Java programs. It talks about the method area of the JVM and the structure of Java objects, upcasting and downcasting, polymorphism and dynamic binding, java.lang.Class and reflection, and -- perhaps most importantly -- reveals how best to ask a hippopotamus to dance.

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().

// In file rtci/ex2/Hippopotamus.java
class Hippopotamus {

    private String yawnAdjective;

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

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

// In file rtci/ex2/DancingHippopotamus.java
class DancingHippopotamus extends Hippopotamus {

    DancingHippopotamus(String yawnAdjective) {
        super(yawnAdjective);
    }

    void dance() {
        System.out.println("Dance!");
    }
}

// In file rtci/ex2/Example2.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

class Example2 {

    public static void main(String[] args) {

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

        makeHipposYawn(hippos);
        makeHipposDance(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();
        }
    }

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

        Iterator it = hippos.iterator();
        while (it.hasNext()) {
            Object hippo = it.next();
            if (hippo instanceof DancingHippopotamus) {
                DancingHippopotamus dh = (DancingHippopotamus) hippo;
                dh.dance();
            }
        }
    }
}

The previous example highlights the primary legitimate use of instanceof: to find out whether an object can do something for you (in this case, dance). When instanceof returns true, the reference is downcast to a more specific type so methods can be invoked.

A good general rule is that you should strive not to write code that doesn't know what's coming, but sometimes you can't avoid it. In such cases, instanceof is the preferred way to find out whether you've got a specific type you'll want to treat specially.

Another legitimate, though less common, way to use instanceof is to check for a tag interface. Sometimes an interface is defined without any members, merely to serve as a "tag" for classes. For example, the Serializable interface contains no fields or methods. Serializable's sole purpose is to indicate that an object allows serialization.

When you use instanceof to check for a tag interface, you are not so much asking an object Can you do a particular thing for me? but rather, Can I do a particular thing to you? Instead of determining whether the object has a particular method you want to invoke, you find out whether you are "allowed" to use the object in some way that doesn't necessarily involve invoking its methods.

Class 'Class'
Where casts, instanceof, and instance method invocations give you a taste of runtime class information, class java.lang.Class gives you a feast: Instances of class java.lang.Class represent the most abundant source of runtime class information in the JVM. Once you have a reference to an object's Class instance, you have access to a wealth of information about that object's class.

You can get a reference to any object's Class instance by invoking getClass() on the reference, as in:

    static void printClassInfo(Object o) {

        System.out.println("------------------------");

        Class c = o.getClass();
        // ...

If you know the type at compile time, you can get a reference to the class instance for a type with the syntax: <typename>.class. For example, the expression Hippopotamus.class would yield a reference to the Class instance for class Hippopotamus. Once you have a reference to a Class, you can get at all kinds of information about the represented type, some of it via the reflection API.

Here's an application that accesses and prints some of the information available from Class objects:

// In file rtci/ex2/Hippopotamus.java
class Hippopotamus {

    private String yawnAdjective;

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

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

// In file rtci/ex2/DancingHippopotamus.java
class DancingHippopotamus extends Hippopotamus {

    DancingHippopotamus(String yawnAdjective) {
        super(yawnAdjective);
    }

    void dance() {
        System.out.println("Dance!");
    }
}

// In file rtci/ex3/Example3.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.lang.reflect.*;

class Example3 {

    public static void main(String[] args) {

        ArrayList hippos = new ArrayList();
        hippos.add(new Hippopotamus("Big"));
        hippos.add(new DancingHippopotamus("Little"));
        hippos.add(hippos);
        hippos.add(new String("Hi There!"));

        Iterator it = hippos.iterator();
        while (it.hasNext()) {
            printClassInfo(it.next());
        }
    }

    static void printModifiers(int modifiers) {

        if (Modifier.isPrivate(modifiers)) {
            System.out.print("private ");
        }
        else if (Modifier.isProtected(modifiers)) {
            System.out.print("protected ");
        }
        else if (Modifier.isPublic(modifiers)) {
            System.out.print("public ");
        }

        if (Modifier.isAbstract(modifiers)) {
            System.out.print("abstract ");
        }
        if (Modifier.isStatic(modifiers)) {
            System.out.print("static ");
        }
        if (Modifier.isFinal(modifiers)) {
            System.out.print("final ");
        }
        if (Modifier.isTransient(modifiers)) {
            System.out.print("transient ");
        }
        if (Modifier.isVolatile(modifiers)) {
            System.out.print("volatile ");
        }
        if (Modifier.isSynchronized(modifiers)) {
            System.out.print("synchronized ");
        }
        if (Modifier.isNative(modifiers)) {
            System.out.print("native ");
        }
    }

    static void printClassInfo(Object o) {

        System.out.println("------------------------");

        Class c = o.getClass();

        System.out.println("Name: " + c.getName());

        Class superclass = c.getSuperclass();
        if (superclass != null) {
            System.out.println("Superclass: " + superclass.getName());
        }

        System.out.println("Interfaces:");
        Class[] interfaces = c.getInterfaces();
        for (int i = 0; i < interfaces.length; ++i) {
            System.out.println("\t" + interfaces[i].getName());
        }

        System.out.println("Fields:");
        Field[] fields = c.getDeclaredFields();
        for (int i = 0; i < fields.length; ++i) {
            System.out.print("\t");
            printModifiers(fields[i].getModifiers());
            System.out.println((fields[i].getType()).getName()
                + " " + fields[i].getName() + ";");
        }

        System.out.println("Methods:");
        Method[] methods = c.getDeclaredMethods();
        for (int i = 0; i < methods.length; ++i) {

            System.out.print("\t");
            printModifiers(methods[i].getModifiers());

            System.out.print((methods[i].getReturnType()).getName()
                + " " + methods[i].getName() + "(");

            Class[] params = methods[i].getParameterTypes();
            for (int j = 0; j < params.length; ++j) {
                System.out.print(params[j].getName());
                if (j != 0 && j != params.length - 1) {
                    System.out.print(", ");
                }
            }

            System.out.println(");");
        }
    }
}

Here's the output of the Example3 application:

------------------------
Name: Hippopotamus
Superclass: java.lang.Object
Interfaces:
Fields:
        private java.lang.String yawnAdjective;
Methods:
        void yawn();
------------------------
Name: DancingHippopotamus
Superclass: Hippopotamus
Interfaces:
Fields:
Methods:
        void dance();
------------------------
Name: java.util.ArrayList
Superclass: java.util.AbstractList
Interfaces:
        java.util.List
        java.lang.Cloneable
        java.io.Serializable
Fields:
        private transient [Ljava.lang.Object; elementData;
        private int size;
Methods:
        private void RangeCheck(int);
        public void add(intjava.lang.Object);
        public boolean add(java.lang.Object);
        public boolean addAll(intjava.util.Collection);
        public boolean addAll(java.util.Collection);
        public void clear();
        public java.lang.Object clone();
        public boolean contains(java.lang.Object);
        public void ensureCapacity(int);
        public java.lang.Object get(int);
        public int indexOf(java.lang.Object);
        public boolean isEmpty();
        public int lastIndexOf(java.lang.Object);
        private synchronized void readObject(java.io.ObjectInputStream);
        public java.lang.Object remove(int);
        public java.lang.Object set(intjava.lang.Object);
        public int size();
        public [Ljava.lang.Object; toArray();
        public [Ljava.lang.Object; toArray([Ljava.lang.Object;);
        public void trimToSize();
        private synchronized void writeObject(java.io.ObjectOutputStream);
------------------------
Name: java.lang.String
Superclass: java.lang.Object
Interfaces:
        java.io.Serializable
        java.lang.Comparable
Fields:
        private [C value;
        private int offset;
        private int count;
        private static final long serialVersionUID;
        public static final [Ljava.io.ObjectStreamField; serialPersistentFields;
        public static final java.util.Comparator CASE_INSENSITIVE_ORDER;
Methods:
        public char charAt(int);
        public int compareTo(java.lang.Object);
        public int compareTo(java.lang.String);
        public int compareToIgnoreCase(java.lang.String);
        public java.lang.String concat(java.lang.String);
        public static java.lang.String copyValueOf([C);
        public static java.lang.String copyValueOf([Cint, int);
        public boolean endsWith(java.lang.String);
        public boolean equals(java.lang.Object);
        public boolean equalsIgnoreCase(java.lang.String);
        public [B getBytes();
        public void getBytes(intint, [B, int);
        public [B getBytes(java.lang.String);
        private [B getBytes(sun.io.CharToByteConverter);
        public void getChars(intint, [C, int);
        public int hashCode();
        public int indexOf(int);
        public int indexOf(intint);
        public int indexOf(java.lang.String);
        public int indexOf(java.lang.Stringint);
        public native java.lang.String intern();
        public int lastIndexOf(int);
        public int lastIndexOf(intint);
        public int lastIndexOf(java.lang.String);
        public int lastIndexOf(java.lang.Stringint);
        public int length();
        public boolean regionMatches(intjava.lang.String, int, int);
        public boolean regionMatches(booleanint, java.lang.String, int, int);
        public java.lang.String replace(charchar);
        public boolean startsWith(java.lang.String);
        public boolean startsWith(java.lang.Stringint);
        public java.lang.String substring(int);
        public java.lang.String substring(intint);
        public [C toCharArray();
        public java.lang.String toLowerCase();
        public java.lang.String toLowerCase(java.util.Locale);
        public java.lang.String toString();
        public java.lang.String toUpperCase();
        public java.lang.String toUpperCase(java.util.Locale);
        public java.lang.String trim();
        public static java.lang.String valueOf(char);
        public static java.lang.String valueOf(double);
        public static java.lang.String valueOf(float);
        public static java.lang.String valueOf(int);
        public static java.lang.String valueOf(long);
        public static java.lang.String valueOf(java.lang.Object);
        public static java.lang.String valueOf(boolean);
        public static java.lang.String valueOf([C);
        public static java.lang.String valueOf([Cint, int);

Well, that's a lot of information. To get at this information, the Example3 class uses java.lang.Class and the reflection API. Other aspects of the reflection API can be used not just to get information about a type, but to actually invoke methods or alter fields.

So how can you take advantage of java.lang.Class and reflection in your designs? In general, you should avoid using reflection in your designs. Reflection is intended for special cases like JavaBeans builder tools, object serialization mechanisms, object inspectors, debuggers, and so on. Reflection is useful in situations in which you have absolutely no idea what's coming. When a programmer drops a JavaBean into a bean builder, for example, the builder hasn't a clue about the class of the bean. The builder must use reflection to introspect the bean class to discover its class, its properties, its methods, and so on. Because the bean builder has no idea what's coming, it must use reflection to figure out how to use the beans it is given.

One situation in which you may know what's coming, but will still want to use java.lang.Class and possibly the reflection API, is when you are creating an instance of a class loaded into a different name space. One of the main reasons for using class loaders and dynamic extension is so your program doesn't have to know at compile time all the classes and interfaces it will load and use at runtime. Although you should prefer designs in which you assume that dynamically loaded objects will implement a known interface or descend from a known class, in order to instantiate an object of the dynamically loaded class, you'll still have to use either Class.newInstance() or the newInstance() method of class java.lang.reflect.Constructor().

Whenever possible, you should design programs such that they know what's coming to a sufficient extent that objects can be used without consulting instanceof or reflection. In cases where you need to ask an object, Can you do a particular thing for me?, you should choose instanceof over reflection to make the query.

Creative uses of java.lang.Class
To every rule, of course, there are exceptions, and this is also true of the rule that you should always strive to design code that knows what's coming. Occasionally, I have encountered a few creative uses of runtime class information that I felt didn't negatively impact flexibility and that made for a good design. In these situations, the code doesn't know what's coming, but, nevertheless, probably represents the best way to offer the services provided.

The most common examples of reasonably-designed Java classes that don't know what's coming are collection classes. Collection classes accept references of type Object to add to their collection, so they can be used with any type of object. Since Java objects all share a common base class (java.lang.Object), and since Java doesn't have templates like C++, accepting Object references in the add() method probably is the best way to design collection classes in Java.

For another example of a useful class that doesn't know what's coming, take a look at this ObjectSorterclass:

// In file rtci/ex4/ObjectSorter.java
import java.util.*;

public class ObjectSorter {

    private HashMap hm = new HashMap();

    public void add(Object o) {

        Class key = o.getClass();

        if (hm.containsKey(key)) {

            ArrayList value = (ArrayList) hm.get(key);
            value.add(o);
        }
        else {

            ArrayList value = new ArrayList();
            value.add(o);
            hm.put(key, value);
        }
    }

    // Returns an iterator for all the Class objects
    public Iterator getClassIterator() {
        return (hm.keySet()).iterator();
    }

    public Collection getObjects(Class key) {
        return (Collection) hm.get(key);
    }
}

This ObjectSorter class was inspired by a class originally designed by Larry O'Brien and ultimately presented by Bruce Eckel in the "Design Patterns" chapter of his book, Thinking in Java. (My version of the class uses Java 2 collections and follows my own naming sensibilities.) Class ObjectSorter sorts objects passed to its add() method based on class, by placing objects into a HashMap using the objects' Class object as a key.

Here's an application that uses ObjectSorter:

// In file rtci/ex4/Example4.java
import java.util.Iterator;
import java.util.Collection;

class Example4 {

    public static void main(String[] args) {

        ObjectSorter os = new ObjectSorter();

        os.add(new String("Hi"));
        os.add(new String("There"));
        os.add(new String("!"));
        os.add(new String("What's"));
        os.add(new String("Happening"));
        os.add(new String("?"));
        os.add(new Object());
        os.add(new Object[3]);
        os.add(new Object[3]);
        os.add(new Object[3]);
        os.add(new Object[2]);
        os.add(new Object[2]);
        os.add(new Example4());
        os.add(new Example4());
        os.add(new Example4());
        os.add(new Example4());

        Iterator classIt = os.getClassIterator();

        while (classIt.hasNext()) {

            Class key = (Class) classIt.next();

            Collection col = os.getObjects(key);

            String className = key.getName();
            int objectCount = col.size();
            System.out.println(className + ": " + objectCount);
        }
    }
}

The Example4 application prints:

[Ljava.lang.Object;: 5
java.lang.String: 6
java.lang.Object: 1
Example4: 4

Example4 demonstrates that the ObjectSorter is able to assign objects into bins by class. Note that all five one-dimensional arrays of Object share the same class, even though some are of different lengths. The length of an array does not affect its class -- just its dimensionality and element type. (The string "[Ljava.lang.Object" is a type descriptor that means one-dimensional array of element type java.lang.Object.) Example4 itself uses runtime class information when it calls getName() on the Class object to print out the name.

Guidelines
To summarize the advice given in this article, here are four guidelines:

    Try to keep the types of your variables as high up the inheritance hierarchy as possible.
    This guideline encourages you to treat objects as generically as possible, which in turn encourages you to take advantage of dynamic binding and polymorphism. When the type of a reference is a supertype of the actual class of the referenced object, the JVM will use dynamic binding to locate the correct implementation of a method you invoke on that reference.

    Prefer polymorphism and dynamic binding over instanceof and downcasting.
    This guideline encourages you to program in the object-oriented way, letting polymorphism and dynamic binding work for you. A telltale sign of code that is disobeying this fundamental rule of thumb is a series of if-else statements doing instanceof tests.

    Prefer code that 'knows what's coming.'
    This guideline counterbalances the above guidelines, stating that although you should make the type of your variables as far up the inheritance hierarchy as possible, you shouldn't make them so far up that you can't use the object without downcasting. You should give variables the type furthest up the inheritance hierarchy that still offers the methods you need to manipulate the object. That very type is the type of object you know is coming.

    Use instanceof to ask 'What can you do for me?' and downcasting to access the functionality.
    When it comes time to ask the question, What can you do for me? prefer instanceof over reflection. You may know you have a reference to some subclass of Animal and may invoke methods declared in Animal on that object. But you still may want to also invoke playFetch() on the object if it is a Dog. Rather than using reflection to look for a playFetch() method, use instanceof to see if the object really is a Dog. Or perhaps better yet, use instanceof to see if the object implements the PlaysFetch interface.

    Once you determine that an object is an instance of some class or interface in which you are interested, use downcasting -- not reflection -- to get at the interesting methods. For example, once you determine you have an Animal object that implements the PlaysFetch interface, don't use reflection to invoke playFetch() on the Animal reference. Instead, downcast the Animal reference to PlaysFetch, and invoke playFetch on the PlaysFetch reference.

In short, don't use reflection for mainstream designs. Use it only for things like bean builders, object serialization mechanisms, object inspectors and debuggers.

Next month
In next month's Design Techniques article, I'll talk about designing with static members.

A request for reader participation
I encourage your comments, criticisms, suggestions, flames -- all kinds of feedback -- about the material presented in this column. If you disagree with something, or have something to add, please let me know.

You can either participate in a discussion forum devoted to this material or e-mail me directly at bv@artima.com.

Resources

This article was first published under the name Design with Runtime Class Information in JavaWorld, a division of Web Publishing, Inc., January 1999.

Talk back!

Have an opinion? Be the first to post a comment about this article.

About the author

Bill Venners has been writing software professionally for 12 years. Based in Silicon Valley, he provides software consulting and training services under the name Artima Software Company. Over the years he has developed software for the consumer electronics, education, semiconductor, and life insurance industries. He has programmed in many languages on many platforms: assembly language on various microprocessors, C on Unix, C++ on Windows, Java on the Web. He is author of the book: Inside the Java Virtual Machine, published by McGraw-Hill.