Java Reflection in Action, Part 2

Wednesday May 25th 2005 by Dr. Ira Forman & Nate Forman
Share:

Discover how to have your programs examine themselves, how to have them make changes to their own structure, and more. Learn how to use reflection to get the class of an object, examine the methods of a class, call a method discovered at runtime, and explore the inheritance hierarchy.

Editor's Note: This piece picks up where the article Java Reflection in Action left off.]

1.5 Representing types with class objects

The discussion of the methods from table 1 indicates that Java reflection uses instances of Class to represent types. For example, getMethod from listing 1 uses an array of Class to indicate the types of the parameters of the desired method. This seems fine for methods that take objects as parameters, but what about types not created by a class declaration?

Table 1.1 The methods defined by Class for method query

Method Description
Method getMethod ( String name,
                   Class[] parameterTypes )
Returns a Method object that represents a public method (either declared or inherited) of the target Class object with the signature specified by the second parameters
Method[] getMethods ()
Returns an array of Method objects that represent all of the public methods (either declared or inherited) supported by the target Class object
Method getDeclaredMethod (
                 String name,
                 Class[] parameterTypes )
Returns a Method object that represents a declared method of the target Class object with the signature specified by the second parameters
Method[] getDeclaredMethods ()
Returns an array of Method objects that represent all of the methods declared by the target Classobject

Listing 1 George's setObjectColor code



Click here for a larger image.

Consider listing 2, which shows a fragment of java.util.Vector. One method has an interface type as a parameter, another an array, and the third a primitive. To program effectively with reflection, you must know how to introspect on classes such as Vector that have methods with such parameters.

Listing 2 A fragment of java.util.Vector.

public class Vector ... {
public synchronized boolean       addAll( Collection c ) ...
public synchronized void          copyInto( Object[] anArray ) ...
public synchronized Object        get( int index ) ...
}

Java represents primitive, array, and interface types by introducing class objects to represent them. These class objects cannot do everything that many other class objects can. For instance, you cannot create a new instance of a primitive or interface. However, such class objects are necessary for performing introspection. Table 2 shows the methods of Class that support type representation.

Table 2 Methods defined by Class that deal with type representation

Method Description
String getName() Returns the fully qualified name of the target Class object
Class getComponentType() If the target object is a Class object for an array, returns the Class object representing the component type
boolean isArray() Returns true if and only if the target Class object represents an array
boolean isInterface() Returns true if and only if the target Class object represents an interface
boolean isPrimitive() Returns true if and only if the target Class object represents a primitive type or void.

The rest of this section explains in greater detail how Java represents primitive, interface, and array types using class objects. By the end of this section, you should know how to use methods such as getMethod to introspect on Vector.class for the methods shown in listing 2.

1.5.1 Representing primitive types

Although primitives are not objects at all, Java uses class objects to represent all eight primitive types. These class objects can be indicated using a class literal when calling methods such as those in table 1. For example, to specify type int, use int.class. Querying the Vector class for its get method can be accomplished with

Method m = Vector.class.getMethod("get", new Class[] {int.class});

A class object that represents a primitive type can be identified using isPrimitive.

The keyword void is not a type in Java; it is used to indicate a method that does not return a value. However, Java does have a class object to represent void. The isPrimitive method returns true for void.class. In section 1.6, we cover introspection on methods. When introspecting for the return type of a method, void.class is used to indicate that a method returns no value.

1.5.2 Representing interfaces

Java also introduces a class object to represent each declared interface. These class objects can be used to indicate parameters of interface type. The addAll method of Vector takes an implementation of the Collection interface as an argument. Querying the Vector class for its addAll method can be written as

Method m = Vector.class.getMethod( "addAll",
                                   new Class[] {Collection.class} );

A class object that represents an interface may be queried for the methods and constants supported by that interface. The isInterface method of Class can be used to identify class objects that represent interfaces.

1.5.3 Representing array types

Java arrays are objects, but their classes are created by the JVM at runtime. A new class is created for each element type and dimension. Java array classes implement both Cloneable and java.io.Serializable.

Class literals for arrays are specified like any other class literal. For instance, to specify a parameter of a single-dimension Object array, use the class literal Object[].class. A query of the Vector class for its copyInto method is written as

Method m = Vector.class.getMethod( "copyInto", new Class[]{Object[].class} );

Class objects that represent arrays can be identified using the isArray method of Class. The component type for an array class can be obtained using getComponentType. Java treats multidimensional arrays like nested single-dimension arrays. Therefore, the line

int[][].class.getComponentType()

evaluates to int[].class. Note the distinction between component type and element type. For the array type int[][], the component type is int[] while the element type is int.

Not all Java methods take non-interface, non-array object parameters like setColor from our George example. In many cases, it is important to introspect for methods such as the Vector methods of listing 2. Now that you understand how to introspect for any Java method, let's examine what can be done once a method is retrieved.

1.6 Understanding method objects

Most of the examples over the last few sections have used the identifier Method but not explained it. Method is the type of the result of all of the method queries in table 1. George uses this class in listing 1 to invoke setColor. From this context, it should be no surprise that java.lang.reflect.Method is the class of the metaobjects that represent methods. Table 3 shows some of the methods supported by the metaobject class Method.

Table 3 Methods defined by Method

Method Description
Class getDeclaringClass() Returns the Class object that declared the method represented by this Method object
Class[] getExceptionTypes() Returns an array of Class objects representing the types of the exceptions declared to be thrown by the method represented by this Method object
int getModifiers() Returns the modifiers for the method represented by this Method object encoded as an int
String getName() Returns the name of the method represented by this Method object
Class[] getParameterTypes() Returns an array of Class objects representing the formal parameters in the order in which they were declared
Class getReturnType() Returns the Class object representing the type returned by the method represented by this Method object
Object invoke(Object obj, Object[] args) Invokes the method represented by this Method object on the specified object with the arguments specified in the Object array

Each Method object provides information about a method including its name, parameter types, return type, and exceptions. A Method object also provides the ability to call the method that it represents. For our example, we are most interested in the ability to call methods, so the rest of this section focuses on the invoke method.

1.6.1 Using dynamic invocation

Dynamic invocation enables a program to call a method on an object at runtime without specifying which method at compile time. In section 1.2, George does not know which setColor method to call when he writes the program. His program relies upon introspection to examine the class of a parameter, obj, at runtime to find the right method. As a result of the introspection, the Method representing setColor is stored in the variable method.

Following the introspection in listing 1, setColor is invoked dynamically with this line:

method.invoke(obj, new Object[] {color});

where the variable color holds a value of type Color. This line uses the invoke method to call the setColor method found previously using introspection. The setColor method is invoked on obj and is passed the value of color as a parameter.

The first parameter to invoke is the target of the method call, or the Object on which to invoke the method. George passes in obj because he wants to call setColor (the method represented by method) on obj. However, if setColor is declared static by the class of obj, the first parameter is ignored because static methods do not need invocation targets. For a static method, null can be supplied as the first argument to invoke without causing an exception.

The second parameter to invoke, args, is an Object array. The invoke method passes the elements of this array to the dynamically invoked method as actual parameters. For a method with no parameters, the second parameter may be either a zero-length array or null.

1.7 Diagramming for reflection

Throughout this book, we use the Unified Modeling Language (UML) for diagrams like figure 4. Those familiar with UML will probably notice that figure 4 combines UML class and object diagrams. Reflection represents all of the class diagram entities at runtime using metaobjects. Therefore, combining class and object diagrams is useful for clearly communicating reflective designs.

UML diagrams typically include only classes or only non-class objects. Modeling reflection calls for combining the two and using the instanceOf dependency to connect an object with its instantiating class. UML defines the instanceOf dependency with same meaning as the Java instanceof operator. However, this book uses the instanceOf dependency only to show that an object is a direct instance of a class. For clarity, we partition figure 4 into its base level and meta-level, although that partition is not standard UML. For more detail on UML, see appendix C.

Figure 4 This is a Unified Modeling Language (UML) diagram describing Dolly the cloned sheep. The diagram shows an object, dolly, which is an instance of the class Sheep. It describes Sheep as a Mammal that implements Cloneable. The important thing to notice about this diagram is that it includes both objects and classes, as is necessary for describing reflective systems.

1.8.2 Exposing some surprises

In the Java reflection API, there are some relationships that may be surprising upon first glance. Discussing these relationships now prepares us for encountering them later in the book and in reflective programming in general. Being prepared in this manner allows for better reflective programming.

The isInstance method can be used to show a very interesting fact about the arrangement of the classes in the Java reflection API. The line

Class.class.isInstance(Class.class)

evaluates to true. This means that the class object for Class is an instance of itself, yielding the circular instanceOf dependency of figure 5. Class is an example of a metaclass, which is a term used to describe classes whose instances are classes. Class is Java's only metaclass.

In Java, all objects have an instantiating class, and all classes are objects. Without the circular dependency, the system must support an infinite tower of class objects, each one an instance of the one above it. Instead, Java uses this circularity to solve this problem.

Figure 5 The object fido is an instance of the Dog class. Dog is an instance of the class Class. Class is also an instance of Class. Class is a metaclass because it is a class whose instances are classes.

The circularity presented in figure 5 makes people uncomfortable because we instinctively mistrust circular definitions. However, as programmers, we are familiar with other kinds of circular definitions. For example, consider recursion. A method that uses recursion is defined in terms of itself; that is, it has a circular definition. When used properly, recursion works just fine. Similarly, there are constraints on the definition of java.lang.Class that make this circularity work just fine.

For more information about this circularity, see Putting Metaclasses to Work [33]. Putting Metaclasses to Work is an advanced book on reflection and metaobject protocols written by one of the authors of this book. It is a good resource for readers who are interested in the theoretical and conceptual basis for reflection.

1.8.3 Another reflective circularity

Adding inheritance to our previous diagram yields the arrangement in figure 1.6. Inheritance adds more circularity to the picture. Object is an instance Class, which can be validated because the following line returns true:

Class.class.isInstance(Object.class)

Class is also a subclass of Object, validated by

Object.class.isAssignableFrom(Class.class)

which also returns true. Conceptually, we already know these facts because in Java, each object has one instantiating class, and all classes are kinds of objects. However, it is comforting that the reflective model is consistent with our previous understanding of the language.

Figure 6 Object is the top of the Java inheritance hierarchy, so classes of metaobjects, including Class, are subclasses of Object. This means that the methods of Object are part of the reflection API. All Java classes are instances of its only metaclass, Class. These two conditions create a cycle in the diagram.



Click here for a larger image.

The new circularity implies additional constraints on the definitions of Object and Class. These constraints are satisfied when the Java Virtual Machine loads the java.lang package. Again, a full explanation of the constraints may be found in Putting Metaclasses to Work [33].

Figure 6 also illustrates why Object is considered part of the reflection API. All metaobjects extend Object, and so they inherit its methods. Therefore, each of those methods can be used in reflective programming.

1.9 Summary

Reflection allows programs to examine themselves and make changes to their structure and behavior at runtime. Even a simple use of reflection allows programmers to write code that does things that a programmer would normally do. These simple uses include getting the class of an object, examining the methods of a class, calling a method discovered at runtime, and exploring the inheritance hierarchy.

The metaobject classes Class and Method represent the classes and methods of running programs. Other metaobjects represent the other parts of the program such as fields, the call stack, and the loader. Class has additional methods to support these other metaobjects. Querying information from these metaobjects is called introspection.

Metaobjects also provide the ability to make changes to the structure and behavior of the program. Using dynamic invocation, a Method metaobject can be commanded to invoke the method that it represents. Reflection provides several other ways to affect the behavior and structure of a program such as reflective access, modification, construction, and dynamic loading.

There are several patterns for using reflection to solve problems. A reflective solution often starts with querying information about the running program from metaobjects. After gathering information using introspection, a reflective program uses that information to make changes in the behavior of the program.

Each new metaobject class allows us to grow our examples in scope and value. These examples reveal lessons that we have learned and techniques that we have applied. Each one follows the same basic pattern of gathering information with introspection and then using the information to change the program in some way.

About the Authors

Dr. Ira Forman is a senior software engineer at IBM. He started working on reflection in the early 1990s when he developed IBM's SOM Metaclass Framework. Nate Forman works for Ticom Geomatics where he uses reflection in day-to-day problems. Ira and Nate are father and son. They live in Austin, Texas.

About the Book

Java Reflection in Action By Ira R. Forman and Nate Forman


Published October 2004, Softbound, 300 pages
Published by Manning Publications Co.
ISBN 1932394184
Retail price: $44.95
Ebook price: $22.50. To purchase the ebook go to http://www.manning.com/forman.
This material is from Chapter 1 of the book.

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved