Java Reflection in Action, Page 2
1.2.1 Choosing Reflection
Given a component, the team's code must accomplish two steps:
- Discover a setColor method supported by the component.
- Call that setColor method with the desired color.
There are many alternatives for accomplishing these steps manually. Let's examine the results of each of these.
If George's team controlled all of the source code, the components could be refactored to implement a common interface that declares setColor. Then, each component could be referenced by that interface type and setColor could be invoked without knowing the concrete type. However, the team does not control the standard Java components or third-party components. Even if they changed the open source components, the open source project might not accept the change, leaving the team with additional maintenance.
Alternatively, the team could implement an adapter for each component. Each such adapter could implement a common interface and delegate the setColor call to the concrete component. However, because of the large number of component classes that the team is using, the solution would cause an explosion in the number of classes to maintain. In addition, because of the large number of component instances, this solution would cause an explosion of the number of objects in the system at runtime. These trade-offs make implementing an adapter an undesirable option.
Using instanceof and casting to discover concrete types at runtime is another alternative, but it leaves several maintenance problems for George's team. First, the code would become bloated with conditionals and casts, making it difficult to read and understand. Second, the code would become coupled with each concrete type. This coupling would make it more difficult for the team to add, remove, or change components. These problems make instanceof and casting an unfavorable alternative.
Each of these alternatives involves program changes that adjust or discover the type of a component. George understands that it is only necessary to find a setColor method and call it. Having studied a little reflection, he understands how to query an object's class for a method at runtime. Once it is found, he knows that a method can also be invoked using reflection. Reflection is uniquely suited to solving this problem because it does not over-constrain the solution with type information.
1.2.2 Programming a reflective solution
To solve his team's problem, George writes the static utility method setObjectColor in listing 1. George's team can pass a visual component to this utility method along with a color. This method finds the setColor method supported by the object's class and calls it with the color as an argument.
Listing 1 George's setObjectColor code
This utility method satisfies the team's goal of being able to set a component's color without knowing its concrete type. The method accomplishes its goals without invading the source code of any of the components. It also avoids source code bloating, memory bloating, and unnecessary coupling. George has implemented an extremely flexible and effective solution.
Two lines in listing 1 use reflection to examine the structure of the parameter obj:
Editor's Note: The following numbers relate to the code callouts in Listing 1.1.
- This line of code queries the object for its class.
- This line queries the class for a setColor method that takes a Color argument.
In combination, these two lines accomplish the first task of finding a setColor method to call.
These queries are each a form of introspection, a term for reflective features that allow a program to examine itself. We say that setObjectColor introspects on its parameter, obj. There is a corresponding form of introspection for each feature of a class. We will examine each of these forms of introspection over the next few chapters.
One line in listing 1 actually affects the behavior of the program:
- This line calls the resulting method on obj, passing it the color—This reflective method call can also be referred to as dynamic invocation. Dynamic invocation is a feature that enables a program to call a method on an object at runtime without specifying which method at compile time.
In the example, George does not know which setColor method to call when writing the code because he does not know the type of the obj parameter. George's program discovers which setColor method is available at runtime through introspection. Dynamic invocation enables George's program to act upon the information gained through introspection and make the solution work. Other reflective mechanisms for affecting program behavior will be covered throughout the rest of the book.
Not every class supports a setColor method. With a static call to setColor, the compiler reports an error if the object's class does not support setColor. When using introspection, it is not known until runtime whether or not a setColor method is supported:
- The class of obj does not support a setColor method—It is important for introspective code to handle this exceptional case. George has been guaranteed by his team that each visual component supports setColor. If that method is not supported by the type of the obj parameter, his utility method has been passed an illegal argument. He handles this by having setObjectColor throw an IllegalArgumentException.
The setObjectColor utility method may not have access to nonpublic setColor methods. In addition, during the dynamic invocation, the setColor method may throw an exception:
- The class containing listing 1.1 does not have access privileges to call a protected, package, or private visibility setColor method.
- The invoked setColor method throws an exception.
Editor's Note: This ends the numbers relating to the code callouts in Listing 1.
It is important for methods using dynamic invocation to handle these cases properly. For simplicity's sake, the code in listing 1 handles these exceptions by wrapping them in runtime exceptions. For production code, of course, this would be wrapped in an exception that the team agrees on and declared in the utility method's throws clause.
All of this runtime processing also takes more time than casts and static invocation. The method calls for introspection are not necessary if the information is known at compile time. Dynamic invocation introduces latency by resolving which method to call and checking access at runtime rather than at compile time. Chapter 9 of my book discusses analysis techniques for balancing performance trade-offs with the tremendous flexibility benefits that reflection can give you.
The rest of this chapter focuses on the concepts necessary to fully understand listing 1. We examine, in detail, the classes that George uses to make it work. We also discuss the elements supported by Java that allow George such a flexible solution.
1.3 Examining running programs
Reflection is a program's ability to examine and change its behavior and structure at runtime. The scenarios previously mentioned have already implied that reflection gives programmers some pretty impressive benefits. Let's take a closer look at what reflective abilities mean for the structure of Java.
Think of introspection as looking at yourself in a mirror. The mirror provides you with a representation of yourself—your reflection—to examine. Examining yourself in a mirror gives you all sorts of useful information, such as what shirt goes with your brown pants or whether you have something green stuck in your teeth. That information can be invaluable in adjusting the structure of your wardrobe and hygiene.
A mirror can also tell you things about your behavior. You can examine whether a smile looks sincere or whether a gesture looks too exaggerated. This information can be critical to understanding how to adjust your behavior to make the right impression on other people.
Similarly, in order to introspect, a program must have access to a representation of itself. This self-representation is the most important structural element of a reflective system. By examining its self-representation, a program can obtain the right information about its structure and behavior to make important decisions.
Listing 1.1 uses instances of Class and Method to find the appropriate setColor method to invoke. These objects are part of Java's self-representation. We refer to objects that are part of a program's self-representation as metaobjects. Meta is a prefix that usually means about or beyond. In this case, metaobjects are objects that hold information about the program.
Class and Method are classes whose instances represent the program. We refer to these as classes of metaobjects or metaobject classes. Metaobject classes are most of what make up Java's reflection API.
We refer to objects that are used to accomplish the main purposes of an application as base-level objects. In the setObjectColor example above, the application that calls George's method as well as the objects passed to it as parameters are base-level objects. We refer to the nonreflective parts of a program as the base program.
Metaobjects represent parts of the running application, and, therefore, may describe the base program. Figure 1 shows the instanceof relationship between base-level objects and the objects that represent their classes. The diagramming convention used for figure 1.1 is the Unified Modeling Language (UML). For readers unfamiliar with UML, we will describe the conventions briefly in section 1.7. For the moment, it is important to understand that the figure can be read as "fido, a base-level object, is an instance of Dog, a class object on the metalevel."
Metaobjects are a convenient self-representation for reflective programming. Imagine the difficulty that George would have in accomplishing his task if he had tried to use the source code or the bytecodes as a representation. He would have to parse the program to even begin examining the class for its methods. Instead, Java metaobjects provide all of the information he needs without additional parsing.
Metaobjects often also provide ways of changing program structure, behavior, or data. In our example, George uses dynamic invocation to call a method that he finds through introspection. Other reflective abilities that make changes include reflective construction, dynamic loading, and intercepting method calls. This book shows how to use these mechanisms and others to solve common but difficult software problems.
Figure 1 Dog is a class object, a metaobject that represents the class Dog. The object fido is an instance of Dog operating within the application. The instanceof relationship, represented in this diagram by a dependency, connects objects on the base level to an object that represents their class on the metalevel.