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;
Class myClass = tile.getClass();
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...- Get the name of the class with
getName():
Class myClass = getClass();
System.out.println("I'm a " + myClass.getName() + "!");
- Get the superclass with
getSuperclass():
public void listSuperclasses(Object object) {
listSuperclasses(object.getClass());
}
private void listSuperclasses(Class aClass) {
System.out.print(aClass.getName();
if(aClass != Object.class) {
System.out.print("extends ");
listSuperclasses(aClass.getSuperclass);
System.out.println(")");
}
}
Output of listSuperclasses(new Applet()):
java.applet.Applet (extends java.awt.Panel (extends java.awt.Container (extends java.awt.Component (extends java.lang.Object))))
- Get objects representing the methods, fields (static and instance variables), and constructors of the class. This is what the rest of this page will discuss.
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;
...
Color red = Color.RED;
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:
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)) {
}
} catch(ClassNotFoundException e) {
}
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:
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:- A
java.lang.NoSuchMethodException or java.lang.NoSuchFieldException indicates that the class has no constructor, method, or field matching the information you gave. This might mean that you got the name wrong, or that you listed the wrong parameters.
Usually, if you have to use java.lang.reflect, you are expecting that, at least some of the time, the requested thing won't be there. So, you should always deal with this exception in an appropriate manner. Don't print the stack trace; just do nothing, or return null or whatever you need to do to indicate failure.
- A
java.lang.SecurityException indicates that you tried to get a private property of another class. With java.lang.reflect, there are actually ways to get around this (BlueJ can see all your variables, public and private) but there is usually no good reason to do so.
When you actually use the Constructor, Method, or Field object, there are three more exceptions that might be thrown:- A
java.lang.IllegalArgumentException means that when you tried to invoke a method or get a field, the object you asked to do this to was not of the class that you retrieved the Method or Field from.
- A
java.lang.IllegalAccessException means that you're not allowed to use that method, constructor, or field. I think that if this were the case and you were following the procedure I outlined to get that object, you should already have gotten a SecurityException before this happened.
- A
java.lang.reflect.InvocationTargetException means that an exception was thrown within the method you called. This is an important one to be aware of and be able to deal with. You should always print the stack trace on this one, because it usually means that you need to debug your code elsewhere, and you don't want to sit there wondering why all that java.lang.reflect code isn't working if it's really the fault of another method.
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) {
e.printStackTrace();
} catch(Exception e) {
}
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");
Color red = redField.get(null);
} catch(ClassNotFoundException e) {
} catch(NoSuchFieldException e) {
} catch(SecurityException e) {
} catch(IllegalArgumentException e) {
} catch(IllegalAccessException e) {
}
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();
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.