Using java.lang.reflect

Table of Contents

1. Introduction

In Project: RPG, several of the script types (Creature, etc.) have special behavior. It would be possible to simply add a lot of if statements in Script, saying if this is a Creature, do this, if it's an Item, do this, etc. This would be the Wrong Thing. The whole point of object oriented programming is that when you have a special case of an object, you create a subclass that deals with that case.

Now, however, you have the problem of how to create a new Script. You cannot simply use the new Script() constructor, because that will always make a Script object. So instead, you made a "factory" method, a static method of Script called createScript(). This method has the responsibility of deciding whether a more specific class exists to represent a certain script; if such a class exists, it will construct and return one of that class. Otherwise, it will return a generic script.

However, we still aren't where we want to be in terms of simplicity of code, because now we have a massive string of if statements in createScript(), doing something like this:

if(action.equals("Creature")) {
   return new Creature(x, y, action, argument);
} else if(action.equals("Item")) {
   return new Item(x, y, action, argument);
} else {
   return new Script(x, y, action, argument);
}

Every time I add a new subclass of Script, I have to go back here and insert another else if branch. It's not an onerous task, but it offers another opportunity for bugs, and offends our programmers' sensibilities. Can't we just tell the computer, "Find me a subclass of Script whose name is action and construct it with the parameters (x, y, action, argument)."

It turns out that such a capability exists in Java, and that it is the absolute Right Thing to do in this situation. Are you ready to be inducted into the mysteries of java.lang.reflect? This is "black belt" material - the techniques you will learn here are some of the most powerful techniques in Java, techniques that form a central part of almost every program you've used in this clas. And nine out of ten professional Java programmers don't know about them. As in, don't use these tecchniques on the AP test unless you comment heavily, because odds are the exam readers will think you're speaking a foreign language.

2. The class object

You know already that in a Java program, there is a special object representing each class. These are the tan boxes that you see in BlueJ. They are objects that are visible to every other object. You can talk to a class to instantiate it (new ArrayList()), or you can access a static method or variable (Math.sqrt(2), Color.RED).

It is also possible to gain access to the object representing the class. You can do this in a static way or by asking an object what its type is. Either way, you get an object of type Class:

Class myClass = Tile.class;      // Static variable of Tile stores its class object
Class myClass = tile.getClass(); // Every Object has a method to retrieve its class object

Notice that I can't just name my variables "class", which would be my first inclination, because "class" is a reserved "keyword" in Java.

There are dozens of things that I can do once I have this object; look here for the complete interface. For example, I can...
If you want to see example code that does all of the things discussed in here, look in the TestCases class in Turtles. The methods testAlternator() and testGeometer() had to be designed so that they would do their best to locate and instantiate those classes, and to send them the methods to be tested, even though you may not even have created those classes yet.

If I had written code in TestCases that used the Alternator class, or a method of the Geometer class, directly, TestCases would never have compiled until you created those classes and methods. So, I wrote code that said, "Look for a class called Alternator. If it exists, and it has a constructor that takes two color objects, make me one." Or, I wrote code that said, "Here's a Geometer object. If it knows how to polygon(int sides), go ahead and ask it to do that. If not, don't worry about it."

3. Asking for a class by name

The Class class has a method Class.forName(String className) that allows you to find any class by name. If the class is in the current package (your project), you just have to give its name. If you have imported the class you are trying to find, you still have to give the full package name. In other words, even if I have imported java.awt.Color, I can's just call Class.forName("Color"); I have to give the full name:

import java.awt.Color;
...
// I can just use the shorter name in my code...
Color red = Color.RED;
// ... but I have to give teh full name still to Class.forName().
Class colorClass = Class.forName("java.awt.Color");

One thing that I haven't shown above is that the method Class.forName() will throw a java.lang.ClassNotFoundException if no class of that name exists. So really, I should wrap the whole thing in a try block, and catch the exception.

Often, once I get the class, I want to do some additional check to make sure it's the class I expected. By far the most common check you will perform is whether the class you just retrieved is a subclass of some other class. To do this, use the method assignableFrom() in Class:

// Asks a class object whether it is either the same as otherClass or a superclass of it.
public boolean assignableFrom(Class otherClass)

So, let's put everything together. In Turtles, I could retrieve the Alternator class, then check to make sure it is a subclass of Turtle:

import java.lang.ClassNotFoundException;
...
try {
   Class alternatorClass = Class.forName("Alternator");
   if(Turtle.class.assignableFrom(alternatorClass)) {
      // Now we can do something with Alternator
   }
} catch(ClassNotFoundException e) {
   // Alternator hasn't been defined yet
}

The next thing you will want to do with that class, of course, is to find out what methods, constructors, and fields it has. These things are represented by classes in java.lang.reflect.

4. Understanding java.lang.reflect

When you ask a class object to return an object representing a constructor, method, or field, it will return an object of the Constructor, Method, or Field class. These classes are defined in the package java.lang.reflect.

In order to retrieve one of these things, you will have to provide the name (in the case of a field or method) and/or a list of the parameter types (in the case of constructors and methods). You should also be prepared to deal with exceptions that might be thrown.

5. Listing parameters and parameter types

When asking a class for a Method or a Constructor, you will have to provide a list of parameter types. Then, when you want to use that Method or Constructor, you will have to provide a list of parameters.

In both cases, this list will be an object array. A type list is an array of Class objects. The only thing that is confusing is what to do with primitive types (double, int, boolean, etc) that don't seem to have a type. You can find Class objects representing these types in their respective wrapper classes (Double, Integer, Boolean, etc.) in the static variable TYPE. So, for example, if I want to list out the types for a method or constructor that takes a double, an int, and two Colors, I would do this:

Class[] types = {Double.TYPE, Integer.TYPE, Color.class, Color.class};

Using these types, I will be able to retrieve a Method or Constructor that takes objects of those types. When I provide the actual list of parameters to send to it, I will make an object array, wrapping all primitive values in their wrapper classes:

// Assume I have some double y that I need to wrap
Object[] parameters = {new Double(2.5), new Double(y), Color.RED, new Color(.6f, 0f, 1f)};

If a constructor or method has no parameters, you can use null for the types array and the parameters array.

6. Dealing with possible exceptions

First of all, note that if you don't have the patience for dealing with exceptions properly, you can wrap everything in one try...catch(Exception e) and then procede to ignore the exception. If all you want to do is get code that works, you can skim down and just read about InvocationTargetException, the most dangerous one. But sometimes, you will want to know more specifically what went wrong.

When you ask a class for a Constructor, Method, or Field, you should be prapared to catch three types of exceptions that might be thrown:When you actually use the Constructor, Method, or Field object, there are three more exceptions that might be thrown:

7. Instantiation using a Constructor object

If I want to instantiate a class that I retrieved with forName(), I have to first ask it for a java.lang.reflect.Constructor object representing the constructor I want, and then ask that Constructor to make a new object. The method |getConstructor(Class[] parameterTypes)| in Class will retrieve a Constructor; I can then use that Constructor by calling its method |newInstance(Object[] parameters)|:

Class myClass = Class.forName("MyClass");

Class[] types = {Double.TYPE, this.getClass()};
Constructor constructor = myClass.getConstructor(types);

Object[] parameters = {new Double(0), this};
Object instanceOfMyClass = constructor.newInstance(parameters);

Of course, really I would have to catch the exceptions this might generate, so I would do something like this:

import java.awt.Color;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Constructor;
...
try {
   Class myClass = Class.forName("MyClass");
   
Class[] types = {Double.TYPE, this.getClass()};
   Constructor constructor = myClass.getConstructor(types);
   
Object[] parameters = {new Double(0), this};
   Object instanceOfMyClass = constructor.newInstance(parameters);
} catch(InvocationTargetException e) {
   // An exception happened in the constructor - this is bad news, so report it
   e.printStackTrace();
} catch(Exception e) {
   // Any other exception just means that I haven't create the class or the constructor
   // yet, or I gave it the wron parameters or made it private. I'll ignore this for now.
}

8. Retrieving a static variable with the Field object

Getting a field (static or instance variable) is probably the easiest thing that I can do with the Class object. I just ask the class to getField(String variableName), which retrieves a java.lang.reflect.Field object; I can then ask for the value of that variable in some object of the appropriate class by calling the Field object's method get(Object objectToGetFrom). If the variable I want to get is static (and it nearly always is, since only static final variables ought to be public), I can give null as the parameter to get().

So, if I wanted to get the color red in the most inefficient way possible, I would do this:

import java.lang.ClassNotFoundException;
import java.lang.NoSuchFieldException;
import java.lang.SecurityException;
import java.reflect.Field;
import java.awt.Color;
import java.lang.IllegalArgumentException;
import java.lang.IllegalAccessException;
...
try {
   Class colorClass = Class.forName("java.awt.Color");
   Field redField = colorClass.getField("RED");
   // The "null" just means it is a static variable I'm getting; to get an instance
   // variable I would supply the object from which to get that variable.
   // Since all instance variables are supposed to be private, you rarely have to
   // get anything but a class variable.
   Color red = redField.get(null);
} catch(ClassNotFoundException e) {
   // The Color class has suddenly ceased to exist!
} catch(NoSuchFieldException e) {
   // There is no longer any red in the world!
} catch(SecurityException e) {
   // Color has gotten all protective of RED all of a sudden.
} catch(IllegalArgumentException e) {
   // Objecting to the "null" argument in get() would never happen - the class object is always there.
   // Normally this would mean that you gave an argument that wasn't the right class.
} catch(IllegalAccessException e) {
   // This would never happen if you follow the procedure above - you would have gotten
   // a SecurityException before you got to this point.
}

Of course, none of the exceptions I caught above would ever actually happen, in this case, since we know exactly how the Color class works. I'm just listing them all here so that you can see what might go wrong if it's your own class that you're using.

9. Invoking a method with the Method object

If I want to call ("invoke") a method of some Object, and I don't know whether that object can do that method, I want to use java.lang.reflect.Method. So, for example, the KeyInterpreter class used in Asteroids and other projects is given an Object as its target, and whenever a key is pressed or released it checks if that Object has a method named key...Pressed() or key...Released() for that key. Usually only about 5 of the possible 100 or so keys will do anything, so it would be silly to ask any object that can be the target of a KeyInterpreter to implement them all.

To get a Method object, call the class's method |getMethod(Class[] parameterTypes)|. Once you have a method, you can ask a particular object to perform that method by calling the Method's method |invoke(Object target, Object[] parameters)|.

If the method returns a value, it will be given as the return value of invoke(). If it is a primitive type, it will be packaged in its wrapper class. If the method is void, the return value will be null.

So, for example, suppose I am given an object and I want to tell it to increase its count instance variable, but I don't know what class it will be and hence don't know whether it can getCount() and setCount(). I would use this code:

import java.reflect.Method;
...
public void setCountOfObject(Object object, int count) {
   Class objectClass = object.getClass();
   
   // No parameters, so use "null" for types list.
   Method getter = objectClass.getMethod("getCount", null);
   Integer returnValue = (Integer)getter.invoke(object, null);
   
   int newCount = count + returnValue.intValue();
   
Class[] parameterTypes = {Integer.TYPE};
Object[] parameters = {new Integer(newCount)};
   Method setter = objectClass.getMethod("setCount", parameterTypes);
   setter.invoke(object, parameters);
}

Of course, really I would have to wrap that whole thing in a try block and catch all the exceptions we've talked about before, plus ClassCastException for good measure (just in case the return value of getCount isn't an Integer). I've omitted that for clarity. You would probably be able to do one specific case for InvocationTargetException and then one general case for everything else, as I showed in the constructors section.