The idea of run-time type
identification (RTTI) seems fairly simple at first: it lets you find the exact
type of an object when you have a handle to only the base type.
However, the need for RTTI
uncovers a whole plethora of interesting (and often perplexing) OO design issues
and raises fundamental questions of how you should structure your programs.
This chapter looks at the ways that
Java allows you to discover information about objects and classes at run-time.
This takes two forms: “traditional” RTTI, which assumes that you
have all the types available at compile-time and run-time, and the
“reflection” mechanism in Java 1.1, which
allows you to discover class information solely at run-time. The
“traditional” RTTI will be covered first, followed by a discussion
of
reflection.
Consider the now familiar example
of a class hierarchy that uses polymorphism. The generic type is the base class
Shape, and the specific
derived types are Circle, Square, and
Triangle:
This is a typical class hierarchy
diagram, with the base class at the top and the derived classes growing
downward. The normal goal in object-oriented
programming is for the bulk of
your code to manipulate handles to the base type (Shape, in this case),
so if you decide to extend the program by adding a new class (Rhomboid,
derived from Shape, for example), the bulk of the code is not affected.
In this example, the dynamically bound method in the Shape interface is
draw( ), so the intent is for the client programmer to call
draw( ) through a generic Shape handle. draw( )
is overridden in all of the derived classes, and because it is a dynamically
bound method, the proper behavior will occur even though it is called through a
generic Shape handle. That’s
polymorphism.
Thus, you generally create a
specific object (Circle, Square, or Triangle), upcast it to
a Shape (forgetting the specific type of the object), and use that
anonymous Shape handle in the rest of the program.
//: Shapes.java package c11; import java.util.*; interface Shape { void draw(); } class Circle implements Shape { public void draw() { System.out.println("Circle.draw()"); } } class Square implements Shape { public void draw() { System.out.println("Square.draw()"); } } class Triangle implements Shape { public void draw() { System.out.println("Triangle.draw()"); } } public class Shapes { public static void main(String[] args) { Vector s = new Vector(); s.addElement(new Circle()); s.addElement(new Square()); s.addElement(new Triangle()); Enumeration e = s.elements(); while(e.hasMoreElements()) ((Shape)e.nextElement()).draw(); } } ///:~
The base class could be coded as an
interface, an abstract class, or an ordinary class. Since Shape
has no concrete members (that is, members with definitions), and it’s
not intended that you ever create a plain Shape object, the most
appropriate and flexible representation is an interface. It’s also
cleaner because you don’t have all those abstract keywords lying
about.
Each of the derived classes
overrides the base-class draw method so it behaves differently. In
main( ), specific types of Shape are created and then added
to a Vector. This is the point at which the upcast occurs because the
Vector holds only Objects. Since everything in Java (with the
exception of primitives) is an Object, a Vector can also hold
Shape objects. But during an upcast to Object, it also
loses any specific information, including the fact that the objects are
shapes. To the Vector, they are just
Objects.
At the point you fetch an element
out of the Vector with nextElement( ), things get a little
busy. Since Vector holds only Objects, nextElement( )
naturally produces an Object handle. But we know it’s really a
Shape handle, and we want to send Shape messages to that object.
So a cast to Shape is
necessary using the traditional “(Shape)” cast. This is the
most basic form of RTTI, since in Java all casts are checked at run-time for
correctness. That’s exactly what RTTI means: at run-time, the type of an
object is identified.
In this case, the RTTI cast is only
partial: the Object is cast to a Shape, and not all the way to a
Circle, Square, or Triangle. That’s because the only
thing we know at this point is that the Vector is full of
Shapes. At compile-time, this is enforced only by your own self-imposed
rules, but at run-time the cast ensures it.
Now polymorphism takes over and the
exact method that’s called for the Shape is determined by whether
the handle is for a Circle, Square, or Triangle. And in
general, this is how it should be; you want the bulk of your code to know as
little as possible about specific types of objects, and to just deal with
the general representation of a family of objects (in this case, Shape).
As a result, your code will be easier to write, read, and maintain, and your
designs will be easier to implement, understand, and change. So polymorphism is
the general goal in object-oriented programming.
But what if you have a special
programming problem that’s easiest to solve if you know the exact type of
a generic handle? For example,
suppose you want to allow your users to highlight all the shapes of any
particular type by turning them purple. This way, they can find all the
triangles on the screen by highlighting them. This is what RTTI accomplishes:
you can ask a handle to a Shape exactly what type it’s referring
to.
To understand how RTTI works in
Java, you must first know how type information is represented at run time. This
is accomplished through a special kind of object called the
Class
object, which contains information about the class. (This is sometimes
called a meta-class.) In
fact, the Class object is used to create all of the “regular”
objects of your class.
There’s a Class object
for each class that is part of your program. That is, each time you write a new
class, a single Class object is also created (and stored, appropriately
enough, in an identically named .class file). At run time, when you want
to make an object of that class, the
Java Virtual Machine (JVM)
that’s executing your program first checks to see if the Class
object for that type is loaded. If not, the JVM loads it by finding the
.class file with that name. Thus, a Java program isn’t completely
loaded before it begins, which is different from many traditional
languages.
Once the Class object for
that type is in memory, it is used to create all objects of that
type.
If this seems shadowy or if you
don’t really believe it, here’s a demonstration program to prove
it:
//: SweetShop.java // Examination of the way the class loader works class Candy { static { System.out.println("Loading Candy"); } } class Gum { static { System.out.println("Loading Gum"); } } class Cookie { static { System.out.println("Loading Cookie"); } } public class SweetShop { public static void main(String[] args) { System.out.println("inside main"); new Candy(); System.out.println("After creating Candy"); try { Class.forName("Gum"); } catch(ClassNotFoundException e) { e.printStackTrace(); } System.out.println( "After Class.forName(\"Gum\")"); new Cookie(); System.out.println("After creating Cookie"); } } ///:~
Each of the classes Candy,
Gum, and Cookie has a static clause
that is executed as the class is loaded for the first time. Information will be
printed out to tell you when loading occurs for that class. In
main( ), the object creations are spread out between print
statements to help detect the time of loading.
A particularly interesting line
is:
Class.forName("Gum");
This method is a static
member of Class (to which all Class objects belong). A
Class object is like any other object and so you can get and manipulate a
handle to it. (That’s what the loader does.) One of the ways to get a
handle to the Class object is
forName( ), which
takes a String containing the textual name (watch the spelling and
capitalization!) of the particular class you want a handle for. It returns a
Class handle.
The output of this program for one
JVM is:
inside main Loading Candy After creating Candy Loading Gum After Class.forName("Gum") Loading Cookie After creating Cookie
You can see that each Class
object is loaded only when it’s needed, and the static
initialization is performed upon class loading.
Interestingly enough, a different
JVM yielded:
Loading Candy Loading Cookie inside main After creating Candy Loading Gum After Class.forName("Gum") After creating Cookie
It appears that this JVM
anticipated the need for Candy and Cookie by examining the code in
main( ), but could not see Gum because it was created by a
call to forName( ) and not through a more typical call to
new. While this JVM produces the desired effect because it does get the
classes loaded before they’re needed, it’s uncertain whether the
behavior shown is precisely correct.
In Java 1.1
you have a second way to produce the handle to the Class object: use the
class literal. In the
above program this would look like:
Gum.class;
which is not only simpler, but also
safer since it’s checked at compile time. Because it eliminates the method
call, it’s also more efficient.
Class literals work with regular
classes as well as interfaces, arrays, and primitive types. In addition,
there’s a standard field called TYPE that
exists for each of the primitive wrapper classes. The TYPE field produces
a handle to the Class object for the associated primitive type, such
that:
So far, you’ve seen RTTI
forms including:
In
C++, the classic cast “(Shape)” does not perform RTTI.
It simply tells the compiler to treat the object as the new type. In Java, which
does perform the type check, this cast is often called a “type safe
downcast.”
The reason for the term “downcast” is the historical arrangement of
the class hierarchy diagram. If casting a Circle to a Shape is an
upcast, then casting a Shape to a Circle is a downcast. However,
you know a Circle is also a Shape, and the compiler freely allows
an upcast assignment, but you don’t know that a Shape is
necessarily a Circle, so the compiler doesn’t allow you to perform
a downcast assignment without
using an explicit cast.
There’s a third form of RTTI
in Java. This is the keyword
instanceof that tells you if an object is an instance of a particular
type. It returns a boolean so you use it in the form of a question, like
this:
if(x instanceof Dog) ((Dog)x).bark();
The above if statement
checks to see if the object x belongs to the class Dog
before casting x to a Dog. It’s important to use
instanceof before a downcast when you don’t have other information
that tells you the type of the object; otherwise you’ll end up with a
ClassCastException.
Ordinarily, you might be hunting
for one type (triangles to turn purple, for example), but the following program
shows how to tally all of the objects using
instanceof.
//: PetCount.java // Using instanceof package c11.petcount; import java.util.*; class Pet {} class Dog extends Pet {} class Pug extends Dog {} class Cat extends Pet {} class Rodent extends Pet {} class Gerbil extends Rodent {} class Hamster extends Rodent {} class Counter { int i; } public class PetCount { static String[] typenames = { "Pet", "Dog", "Pug", "Cat", "Rodent", "Gerbil", "Hamster", }; public static void main(String[] args) { Vector pets = new Vector(); try { Class[] petTypes = { Class.forName("c11.petcount.Dog"), Class.forName("c11.petcount.Pug"), Class.forName("c11.petcount.Cat"), Class.forName("c11.petcount.Rodent"), Class.forName("c11.petcount.Gerbil"), Class.forName("c11.petcount.Hamster"), }; for(int i = 0; i < 15; i++) pets.addElement( petTypes[ (int)(Math.random()*petTypes.length)] .newInstance()); } catch(InstantiationException e) {} catch(IllegalAccessException e) {} catch(ClassNotFoundException e) {} Hashtable h = new Hashtable(); for(int i = 0; i < typenames.length; i++) h.put(typenames[i], new Counter()); for(int i = 0; i < pets.size(); i++) { Object o = pets.elementAt(i); if(o instanceof Pet) ((Counter)h.get("Pet")).i++; if(o instanceof Dog) ((Counter)h.get("Dog")).i++; if(o instanceof Pug) ((Counter)h.get("Pug")).i++; if(o instanceof Cat) ((Counter)h.get("Cat")).i++; if(o instanceof Rodent) ((Counter)h.get("Rodent")).i++; if(o instanceof Gerbil) ((Counter)h.get("Gerbil")).i++; if(o instanceof Hamster) ((Counter)h.get("Hamster")).i++; } for(int i = 0; i < pets.size(); i++) System.out.println( pets.elementAt(i).getClass().toString()); for(int i = 0; i < typenames.length; i++) System.out.println( typenames[i] + " quantity: " + ((Counter)h.get(typenames[i])).i); } } ///:~
There’s a rather narrow
restriction on instanceof in Java 1.0: You can
compare it to a named type only, and not to a Class object. In the
example above you might feel that it’s tedious to write out all of those
instanceof expressions, and you’re right. But in Java 1.0 there is
no way to cleverly automate it by creating a Vector of Class
objects and comparing it to those instead. This isn’t as great a
restriction as you might think, because you’ll eventually understand that
your design is probably flawed if you end up writing a lot of instanceof
expressions.
Of course this example is contrived
– you’d probably put a static data member in each type and
increment it in the constructor to keep track of the counts. You would do
something like that if you had control of the source code for the class
and could change it. Since this is not always the case, RTTI can come in
handy.
It’s interesting to see how
the PetCount.java example can be rewritten using Java
1.1 class
literals. The result is cleaner
in many ways:
//: PetCount2.java // Using Java 1.1 class literals package c11.petcount2; import java.util.*; class Pet {} class Dog extends Pet {} class Pug extends Dog {} class Cat extends Pet {} class Rodent extends Pet {} class Gerbil extends Rodent {} class Hamster extends Rodent {} class Counter { int i; } public class PetCount2 { public static void main(String[] args) { Vector pets = new Vector(); Class[] petTypes = { // Class literals work in Java 1.1+ only: Pet.class, Dog.class, Pug.class, Cat.class, Rodent.class, Gerbil.class, Hamster.class, }; try { for(int i = 0; i < 15; i++) { // Offset by one to eliminate Pet.class: int rnd = 1 + (int)( Math.random() * (petTypes.length - 1)); pets.addElement( petTypes[rnd].newInstance()); } } catch(InstantiationException e) {} catch(IllegalAccessException e) {} Hashtable h = new Hashtable(); for(int i = 0; i < petTypes.length; i++) h.put(petTypes[i].toString(), new Counter()); for(int i = 0; i < pets.size(); i++) { Object o = pets.elementAt(i); if(o instanceof Pet) ((Counter)h.get( "class c11.petcount2.Pet")).i++; if(o instanceof Dog) ((Counter)h.get( "class c11.petcount2.Dog")).i++; if(o instanceof Pug) ((Counter)h.get( "class c11.petcount2.Pug")).i++; if(o instanceof Cat) ((Counter)h.get( "class c11.petcount2.Cat")).i++; if(o instanceof Rodent) ((Counter)h.get( "class c11.petcount2.Rodent")).i++; if(o instanceof Gerbil) ((Counter)h.get( "class c11.petcount2.Gerbil")).i++; if(o instanceof Hamster) ((Counter)h.get( "class c11.petcount2.Hamster")).i++; } for(int i = 0; i < pets.size(); i++) System.out.println( pets.elementAt(i).getClass().toString()); Enumeration keys = h.keys(); while(keys.hasMoreElements()) { String nm = (String)keys.nextElement(); Counter cnt = (Counter)h.get(nm); System.out.println( nm.substring(nm.lastIndexOf('.') + 1) + " quantity: " + cnt.i); } } } ///:~
Here, the typenames array
has been removed in favor of getting the type name strings from the Class
object. Notice the extra work for this: the class name is not, for example,
Gerbil, but instead c11.petcount2.Gerbil since the package name is
included. Notice also that the system can distinguish between classes and
interfaces.
You can also see that the creation
of petTypes does not need to be surrounded by a try block since
it’s evaluated at compile time and thus won’t throw any exceptions,
unlike Class.forName( ).
When the Pet objects are
dynamically created, you can see that the random number is restricted so it is
between 1 and petTypes.length and does not include zero. That’s
because zero refers to Pet.class, and presumably a generic Pet
object is not interesting. However, since Pet.class is part of
petTypes the result is that all of the pets get counted.
Java 1.1
has added the
isInstance
method to the class Class. This allows you to dynamically call the
instanceof operator, which you could do only statically in Java
1.0 (as previously shown). Thus, all those tedious
instanceof statements can be removed in the PetCount
example:
//: PetCount3.java // Using Java 1.1 isInstance() package c11.petcount3; import java.util.*; class Pet {} class Dog extends Pet {} class Pug extends Dog {} class Cat extends Pet {} class Rodent extends Pet {} class Gerbil extends Rodent {} class Hamster extends Rodent {} class Counter { int i; } public class PetCount3 { public static void main(String[] args) { Vector pets = new Vector(); Class[] petTypes = { Pet.class, Dog.class, Pug.class, Cat.class, Rodent.class, Gerbil.class, Hamster.class, }; try { for(int i = 0; i < 15; i++) { // Offset by one to eliminate Pet.class: int rnd = 1 + (int)( Math.random() * (petTypes.length - 1)); pets.addElement( petTypes[rnd].newInstance()); } } catch(InstantiationException e) {} catch(IllegalAccessException e) {} Hashtable h = new Hashtable(); for(int i = 0; i < petTypes.length; i++) h.put(petTypes[i].toString(), new Counter()); for(int i = 0; i < pets.size(); i++) { Object o = pets.elementAt(i); // Using isInstance to eliminate individual // instanceof expressions: for (int j = 0; j < petTypes.length; ++j) if (petTypes[j].isInstance(o)) { String key = petTypes[j].toString(); ((Counter)h.get(key)).i++; } } for(int i = 0; i < pets.size(); i++) System.out.println( pets.elementAt(i).getClass().toString()); Enumeration keys = h.keys(); while(keys.hasMoreElements()) { String nm = (String)keys.nextElement(); Counter cnt = (Counter)h.get(nm); System.out.println( nm.substring(nm.lastIndexOf('.') + 1) + " quantity: " + cnt.i); } } } ///:~
You can see that the Java
1.1 isInstance( ) method has eliminated the
need for the instanceof expressions. In addition, this means that you can
add new types of pets simply by changing the petTypes array; the rest of
the program does not need modification (as it did when using the
instanceof
expressions).
Java performs its
RTTI using the Class
object, even if you’re doing something like a cast. The class Class
also has a number of other ways you can use RTTI.
First, you must get a handle to the
appropriate Class object. One way to do this, as shown in the previous
example, is to use a string and the Class.forName( ) method. This is
convenient because you don’t need an object of that type in order to get
the Class handle. However, if you do already have an object of the type
you’re interested in, you can fetch the Class handle by calling a
method that’s part of the Object root class:
getClass( ). This
returns the Class handle representing the actual type of the object.
Class has several interesting and sometimes useful methods, demonstrated
in the following example:
//: ToyTest.java // Testing class Class interface HasBatteries {} interface Waterproof {} interface ShootsThings {} class Toy { // Comment out the following default // constructor to see // NoSuchMethodError from (*1*) Toy() {} Toy(int i) {} } class FancyToy extends Toy implements HasBatteries, Waterproof, ShootsThings { FancyToy() { super(1); } } public class ToyTest { public static void main(String[] args) { Class c = null; try { c = Class.forName("FancyToy"); } catch(ClassNotFoundException e) {} printInfo(c); Class[] faces = c.getInterfaces(); for(int i = 0; i < faces.length; i++) printInfo(faces[i]); Class cy = c.getSuperclass(); Object o = null; try { // Requires default constructor: o = cy.newInstance(); // (*1*) } catch(InstantiationException e) {} catch(IllegalAccessException e) {} printInfo(o.getClass()); } static void printInfo(Class cc) { System.out.println( "Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]"); } } ///:~
You can see that class
FancyToy is quite complicated, since it inherits from Toy and
implements the interfaces of HasBatteries,
Waterproof, and ShootsThings. In main( ), a
Class handle is created and initialized to the FancyToy
Class using forName( ) inside an appropriate try
block.
The
Class.getInterfaces( )
method returns an array of
Class objects representing the interfaces that are contained in the
Class object of interest.
If you have a Class object
you can also ask it for its direct base class using
getSuperclass( ).
This, of course, returns a Class handle that you can further query. This
means that, at run time, you can discover an object’s entire class
hierarchy.
The
newInstance( )
method of Class can, at first, seem like just another way to
clone( ) an object. However, you can create a new object with
newInstance( ) without an existing object, as seen here,
because there is no Toy object, only cy, which is a handle to
y’s Class object. This is a way to implement a
“virtual constructor,” which allows you to say “I don’t
know exactly what type you are, but create yourself properly anyway.” In
the example above, cy is just a Class handle with no further type
information known at compile time. And when you create a new instance, you get
back an Object handle. But that handle is pointing to a Toy
object. Of course, before you can send any messages other than those accepted by
Object, you have to investigate it a bit and do some casting. In
addition, the class that’s being created with newInstance( )
must have a default constructor. There’s no way to use
newInstance( ) to create objects that have non-default constructors,
so this can be a bit limiting in Java 1. However, the reflection API in
Java 1.1 (discussed in the next section) allows you to
dynamically use any constructor in a class.
The final method in the listing is
printInfo( ), which
takes a Class handle and gets its name with
getName( ), and
finds out whether it’s an interface with
isInterface( ).
The output from this program
is:
Class name: FancyToy is interface? [false] Class name: HasBatteries is interface? [true] Class name: Waterproof is interface? [true] Class name: ShootsThings is interface? [true] Class name: Toy is interface? [false]
Thus, with the Class object
you can find out just about everything you want to know about an
object.
If you don’t know the precise
type of an object, RTTI will tell you. However, there’s a limitation: the
type must be known at compile time in order for you to be able to detect it
using RTTI and do something useful with the information. Put another way, the
compiler must know about all the classes you’re working with for
RTTI.
This doesn’t seem like that
much of a limitation at first, but suppose you’re given a handle to an
object that’s not in your program space. In fact, the class of the object
isn’t even available to your program at compile time. For example, suppose
you get a bunch of bytes from a disk file or from a network connection and
you’re told that those bytes represent a class. Since the compiler
can’t know about the class while it’s compiling the code, how can
you possibly use such a class?
In a traditional programming
environment this seems like a far-fetched scenario. But as we move into a larger
programming world there are important cases in which this happens. The first is
component-based programming in which you build projects using
Rapid Application Development (RAD) in an
application builder tool. This is a visual approach to creating a program (which
you see on the screen as a form) by moving icons that represent
components onto the form. These components are then configured by setting some
of their values at program time. This design-time configuration requires that
any component be instantiable and that it expose some part of itself and allow
its values to be read and set. In addition, components that handle GUI events
must expose information about appropriate methods so that the RAD environment
can assist the programmer in overriding these event-handling methods. Reflection
provides the mechanism to detect the available methods and produce the method
names. Java 1.1 provides a structure for component-based
programming through Java Beans (described in Chapter 13).
Another compelling motivation for
discovering class information at run-time is to provide the ability to create
and execute objects on remote platforms across a network. This is called
Remote Method Invocation (RMI) and it allows a Java program (version 1.1
and higher) to have objects distributed across many machines. This distribution
can happen for a number of reasons: perhaps you’re doing a
computation-intensive task and you want to break it up and put pieces on
machines that are idle in order to speed things up. In some situations you might
want to place code that handles particular types of tasks (e.g. “Business
Rules” in a multi-tier client/server architecture) on a particular machine
so that machine becomes a common repository describing those actions and it can
be easily changed to affect everyone in the system. (This is an interesting
development since the machine exists solely to make software changes easy!)
Along these lines, distributed computing also supports specialized hardware that
might be good at a particular task – matrix inversions, for example
– but inappropriate or too expensive for general purpose
programming.
In Java
1.1, the class Class (described previously in
this chapter) is extended to support the concept of reflection, and
there’s an additional library, java.lang.reflect, with classes
Field,
Method, and
Constructor (each of
which implement the Member interface). Objects of these types are created
by the JVM at run-time to represent the corresponding member in the unknown
class. You can then use the Constructors to create new objects, the
get( ) and set( ) methods to read and modify the fields
associated with Field objects, and the invoke( ) method to
call a method associated with a Method object. In addition, you can call
the convenience methods getFields( ), getMethods( ),
getConstructors( ), etc., to return arrays of the objects
representing the fields, methods, and constructors. (You can find out more by
looking up the class Class in your online documentation.) Thus,
the class information for anonymous objects can be completely determined at run
time, and nothing need be known at compile time.
It’s important to realize
that there’s nothing magic about reflection. When you’re using
reflection to interact with an object of an unknown type, the JVM will simply
look at the object and see that it belongs to a particular class (just like
ordinary RTTI) but then, before it can do anything else, the Class object
must be loaded. Thus, the .class file for that particular type must still
be available to the JVM, either on the local machine or across the network. So
the true difference between RTTI
and reflection is that with RTTI, the compiler opens and examines the
.class file at compile time. Put another way, you can call all the
methods of an object in the “normal” way. With reflection, the
.class file is unavailable at compile time; it is opened and examined by
the run-time environment.
You’ll rarely need to use the
reflection tools directly; they’re in the language to support the other
Java features such as object serialization (described in Chapter 10), Java
Beans, and RMI (described later in the book). However, there are times when
it’s quite useful to be able to dynamically extract information about a
class. One extremely useful tool is a class method extractor. As mentioned
before, looking at a class definition source code or online documentation shows
only the methods that are defined or overridden within that class
definition. But there could be dozens more available to you that have come
from base classes. To locate these is both tedious and time consuming.
Fortunately, reflection provides a way to write a simple tool that will
automatically show you the entire interface. Here’s the way it
works:
//: ShowMethods.java // Using Java 1.1 reflection to show all the // methods of a class, even if the methods are // defined in the base class. import java.lang.reflect.*; public class ShowMethods { static final String usage = "usage: \n" + "ShowMethods qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethods qualified.class.name word\n" + "To search for methods involving 'word'"; public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } try { Class c = Class.forName(args[0]); Method[] m = c.getMethods(); Constructor[] ctor = c.getConstructors(); if(args.length == 1) { for (int i = 0; i < m.length; i++) System.out.println(m[i].toString()); for (int i = 0; i < ctor.length; i++) System.out.println(ctor[i].toString()); } else { for (int i = 0; i < m.length; i++) if(m[i].toString() .indexOf(args[1])!= -1) System.out.println(m[i].toString()); for (int i = 0; i < ctor.length; i++) if(ctor[i].toString() .indexOf(args[1])!= -1) System.out.println(ctor[i].toString()); } } catch (ClassNotFoundException e) { System.out.println("No such class: " + e); } } } ///:~
The Class methods
getMethods( ) and
getConstructors( )
return an array of Method and Constructor, respectively. Each of
these classes has further methods to dissect the names, arguments, and return
values of the methods they represent. But you can also just use
toString( ), as is done here, to produce a String with the
entire method signature. The rest of the code is just for extracting command
line information, determining if a particular signature matches with your target
string (using
indexOf( )), and
printing the results.
This shows reflection in action,
since the result produced by Class.forName( ) cannot be known at
compile-time, and therefore all the method signature information is being
extracted at run-time. If you investigate your online documentation on
reflection, you’ll see that there is enough support to actually set up and
make a method call on an object that’s totally unknown at compile-time.
Again, this is something you’ll probably never need to do yourself –
the support is there for Java and so a programming environment can manipulate
Java Beans – but it’s interesting.
An interesting experiment is to run
java ShowMethods ShowMethods. This produces a listing that includes a
public default constructor, even though you can see from the code that no
constructor was defined. The constructor you see is the one that’s
automatically synthesized by the compiler. If you then make ShowMethods a
non-public class (that is, friendly), the synthesized default constructor
no longer shows up in the output. The synthesized default constructor is
automatically given the same access as the class.
The output for ShowMethods
is still a little tedious. For example, here’s a portion of the output
produced by invoking java ShowMethods java.lang.String:
public boolean java.lang.String.startsWith(java.lang.String,int) public boolean java.lang.String.startsWith(java.lang.String) public boolean java.lang.String.endsWith(java.lang.String)
It would be even nicer if the
qualifiers like java.lang could be stripped off. The
StreamTokenizer class
introduced in the previous chapter can help solve this problem:
//: ShowMethodsClean.java // ShowMethods with the qualifiers stripped // to make the results easier to read import java.lang.reflect.*; import java.io.*; public class ShowMethodsClean { static final String usage = "usage: \n" + "ShowMethodsClean qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethodsClean qualif.class.name word\n" + "To search for methods involving 'word'"; public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } try { Class c = Class.forName(args[0]); Method[] m = c.getMethods(); Constructor[] ctor = c.getConstructors(); // Convert to an array of cleaned Strings: String[] n = new String[m.length + ctor.length]; for(int i = 0; i < m.length; i++) { String s = m[i].toString(); n[i] = StripQualifiers.strip(s); } for(int i = 0; i < ctor.length; i++) { String s = ctor[i].toString(); n[i + m.length] = StripQualifiers.strip(s); } if(args.length == 1) for (int i = 0; i < n.length; i++) System.out.println(n[i]); else for (int i = 0; i < n.length; i++) if(n[i].indexOf(args[1])!= -1) System.out.println(n[i]); } catch (ClassNotFoundException e) { System.out.println("No such class: " + e); } } } class StripQualifiers { private StreamTokenizer st; public StripQualifiers(String qualified) { st = new StreamTokenizer( new StringReader(qualified)); st.ordinaryChar(' '); // Keep the spaces } public String getNext() { String s = null; try { if(st.nextToken() != StreamTokenizer.TT_EOF) { switch(st.ttype) { case StreamTokenizer.TT_EOL: s = null; break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = new String(st.sval); break; default: // single character in ttype s = String.valueOf((char)st.ttype); } } } catch(IOException e) { System.out.println(e); } return s; } public static String strip(String qualified) { StripQualifiers sq = new StripQualifiers(qualified); String s = "", si; while((si = sq.getNext()) != null) { int lastDot = si.lastIndexOf('.'); if(lastDot != -1) si = si.substring(lastDot + 1); s += si; } return s; } } ///:~
The class ShowMethodsClean
is quite similar to the previous ShowMethods, except that it takes the
arrays of Method and Constructor and converts them into a single
array of String. Each of these String objects is then passed
through StripQualifiers.Strip( ) to remove all the method
qualification. As you can see, this uses the StreamTokenizer and
String manipulation to do its work.
This tool can be a real time-saver
while you’re programming, when you can’t remember if a class has a
particular method and you don’t want to go walking through the class
hierarchy in the online documentation, or if you don’t know whether that
class can do anything with, for example, Color objects.
Chapter 17 contains a GUI version
of this program so you can leave it running while you’re writing code, to
allow quick
lookups.
RTTI allows you to discover type
information from an anonymous base-class handle. Thus, it’s ripe for
misuse by the novice since it might make sense before
polymorphic method calls do. For many people coming from a procedural
background, it’s difficult not to organize their programs into sets of
switch statements. They could accomplish this with RTTI and thus lose the
important value of polymorphism in code development and
maintenance. The intent of Java is that you use polymorphic method calls
throughout your code, and you use RTTI only when you must.
However, using polymorphic method
calls as they are intended requires that you have control of the base-class
definition because at some point in the extension of your program you might
discover that the base class doesn’t include the method you need. If the
base class comes from a library or is otherwise controlled by someone else, a
solution to the problem is RTTI: You can inherit a new type and add your extra
method. Elsewhere in the code you can detect your particular type and call that
special method. This doesn’t destroy the polymorphism and extensibility of
the program because adding a new type will not require you to hunt for switch
statements in your program. However, when you add new code in your main body
that requires your new feature, you must use RTTI to detect your particular
type.
Putting a feature in a base class
might mean that, for the benefit of one particular class, all of the other
classes derived from that base require some meaningless stub of a method. This
makes the interface less clear and annoys those who must override abstract
methods when they derive from that base class. For example, consider a class
hierarchy representing musical instruments. Suppose you wanted to clear the spit
valves of all the appropriate instruments in your orchestra. One option is to
put a ClearSpitValve( ) method in the base class Instrument,
but this is confusing because it implies that Percussion and
Electronic instruments also have spit valves. RTTI provides a much more
reasonable solution in this case because you can place the method in the
specific class (Wind in this case), where it’s appropriate.
However, a more appropriate solution is to put a
prepareInstrument( ) method in the base class, but you might not see
this when you’re first solving the problem and could mistakenly assume
that you must use RTTI.
Finally, RTTI will sometimes solve
efficiency problems. If your code nicely uses polymorphism, but it turns out
that one of your objects reacts to this general purpose code in a horribly
inefficient way, you can pick out that type using RTTI and write case-specific
code to improve the efficiency.