|
[ Seminars ] [ Seminars on CD ROM ] [ Consulting ] Thinking in Java, 2nd edition, Revision 12©2000 by Bruce Eckel[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]12: Run-time Type IdentificationHowever, 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, which allows you to discover class
information solely at run-time. The “traditional” RTTI will be
covered first, followed by a discussion of reflection.
The need for RTTIConsider 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 references 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 reference.
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 reference. 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 reference in the rest of the program.
//: c12:Shapes.java import java.util.*; class Shape { void draw() { System.out.println(this + ".draw()"); } } class Circle extends Shape { public String toString() { return "Circle"; } } class Square extends Shape { public String toString() { return "Square"; } } class Triangle extends Shape { public String toString() { return "Triangle"; } } public class Shapes { public static void main(String[] args) { ArrayList s = new ArrayList(); s.add(new Circle()); s.add(new Square()); s.add(new Triangle()); Iterator e = s.iterator(); while(e.hasNext()) ((Shape)e.next()).draw(); } } ///:~ The base class contains a
draw( ) method that indirectly uses toString( ) to print
an identifier for the class by passing this to
System.out.println( ). If that function sees an object, it
automatically calls the toString( ) method to produce a
String representation.
Each of the derived classes overrides the
toString( ) method (from Object) so that draw( )
ends up printing something different in each case. In main( ),
specific types of Shape are created and then added to a At the point you fetch an element out of
the 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 Now polymorphism takes over and the exact
method that’s called for the Shape is determined by whether the
reference 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 reference? 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 Shape reference the exact type that it’s referring
to.
The Class objectTo 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 and
compile 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: //: c12: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.err); } System.out.println( "After Class.forName(\"Gum\")"); new Cookie(); System.out.println("After creating Cookie"); } } ///:~ Each of the classes Candy,
Gum, and Cookie have a static
clause that is executed as the class is loaded for the first time. Information
will be printed 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 reference to it.
(That’s what the loader does.) One of the ways to get a reference 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 reference for. It returns a
Class reference.
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.
Class literalsJava provides a second way to produce the
reference to the Class object, using a
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 reference to
the Class object for the associated primitive type, such
that: My preference is to use the
“.class” versions if you can, since they’re more
consistent with regular classes.
Checking before a castSo 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 you can easily tally
all of the objects using instanceof. Suppose you have a family of
Pet classes: //: c12:Pets.java 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; } ///:~ The Counter class is used to keep
track of the number of any particular type of Pet. You could think of it
as an Integer that can be modified.
Using instanceof, all the pets can
be counted: //: c12:PetCount.java // Using instanceof. import java.util.*; public class PetCount { static String[] typenames = { "Pet", "Dog", "Pug", "Cat", "Rodent", "Gerbil", "Hamster", }; // Exceptions thrown out to console: public static void main(String[] args) throws Exception { ArrayList pets = new ArrayList(); try { Class[] petTypes = { Class.forName("Dog"), Class.forName("Pug"), Class.forName("Cat"), Class.forName("Rodent"), Class.forName("Gerbil"), Class.forName("Hamster"), }; for(int i = 0; i < 15; i++) pets.add( petTypes[ (int)(Math.random()*petTypes.length)] .newInstance()); } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } catch(ClassNotFoundException e) { System.err.println("Cannot find class"); throw e; } HashMap h = new HashMap(); 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.get(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.get(i).getClass()); 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: 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 there is no way to cleverly automate instanceof by creating an
ArrayList of Class objects and comparing it to those instead (stay
tuned—you’ll see an alternative). 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.
Using class literalsIt’s interesting to see how the
PetCount.java example can be rewritten using class
literals. The result is cleaner
in many ways: //: c12:PetCount2.java // Using class literals. import java.util.*; public class PetCount2 { public static void main(String[] args) throws Exception { ArrayList pets = new ArrayList(); Class[] petTypes = { // Class literals: 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.add( petTypes[rnd].newInstance()); } } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } HashMap h = new HashMap(); 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.get(i); if(o instanceof Pet) ((Counter)h.get("class Pet")).i++; if(o instanceof Dog) ((Counter)h.get("class Dog")).i++; if(o instanceof Pug) ((Counter)h.get("class Pug")).i++; if(o instanceof Cat) ((Counter)h.get("class Cat")).i++; if(o instanceof Rodent) ((Counter)h.get("class Rodent")).i++; if(o instanceof Gerbil) ((Counter)h.get("class Gerbil")).i++; if(o instanceof Hamster) ((Counter)h.get("class Hamster")).i++; } for(int i = 0; i < pets.size(); i++) System.out.println(pets.get(i).getClass()); Iterator keys = h.keySet().iterator(); while(keys.hasNext()) { String nm = (String)keys.next(); 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 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 one 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.
A dynamic instanceof
The Class
isInstance
method provides a way to dynamically call the instanceof operator. Thus,
all those tedious instanceof statements can be removed in the
PetCount example: //: c12:PetCount3.java // Using isInstance(). import java.util.*; public class PetCount3 { public static void main(String[] args) throws Exception { ArrayList pets = new ArrayList(); 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.add( petTypes[rnd].newInstance()); } } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } HashMap h = new HashMap(); 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.get(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.get(i).getClass()); Iterator keys = h.keySet().iterator(); while(keys.hasNext()) { String nm = (String)keys.next(); Counter cnt = (Counter)h.get(nm); System.out.println( nm.substring(nm.lastIndexOf('.') + 1) + " quantity: " + cnt.i); } } } ///:~ You can see that the
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).
instanceof vs. Class equivalence
When querying for type information,
there’s an important difference between either form of instanceof
(that is, instanceof or isInstance( ), which produce
equivalent results) and the direct comparison of the Class objects.
Here’s an example that demonstrates the difference: //: c12:FamilyVsExactType.java // The difference between instanceof and class class Base {} class Derived extends Base {} public class FamilyVsExactType { static void test(Object x) { System.out.println("Testing x of type " + x.getClass()); System.out.println("x instanceof Base " + (x instanceof Base)); System.out.println("x instanceof Derived " + (x instanceof Derived)); System.out.println("Base.isInstance(x) " + Base.class.isInstance(x)); System.out.println("Derived.isInstance(x) " + Derived.class.isInstance(x)); System.out.println( "x.getClass() == Base.class " + (x.getClass() == Base.class)); System.out.println( "x.getClass() == Derived.class " + (x.getClass() == Derived.class)); System.out.println( "x.getClass().equals(Base.class)) " + (x.getClass().equals(Base.class))); System.out.println( "x.getClass().equals(Derived.class)) " + (x.getClass().equals(Derived.class))); } public static void main(String[] args) { test(new Base()); test(new Derived()); } } ///:~ The test( ) method performs
type checking with its argument using both forms of instanceof. It then
gets the Class reference and uses == and equals( ) to
test for equality of the Class objects. Here is the
output: Testing x of type class Base x instanceof Base true x instanceof Derived false Base.isInstance(x) true Derived.isInstance(x) false x.getClass() == Base.class true x.getClass() == Derived.class false x.getClass().equals(Base.class)) true x.getClass().equals(Derived.class)) false Testing x of type class Derived x instanceof Base true x instanceof Derived true Base.isInstance(x) true Derived.isInstance(x) true x.getClass() == Base.class false x.getClass() == Derived.class true x.getClass().equals(Base.class)) false x.getClass().equals(Derived.class)) true Reassuringly, instanceof and
isInstance( ) produce exactly the same results, as do
equals( ) and ==. But the tests themselves draw different
conclusions. In keeping with the concept of type, instanceof says
“are you this class, or a class derived from this class?” On the
other hand, if you compare the actual Class objects using ==,
there is no concern with inheritance—it’s either the exact type or
it isn’t.
RTTI syntaxJava 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 reference 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 reference. However, if you do already have an object of the
type you’re interested in, you can fetch the Class reference by
calling a method that’s part of the Object root class:
getClass( ). This
returns the Class reference representing the actual type of the object.
Class has many interesting methods, demonstrated in the following
example: //: c12: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) throws Exception { Class c = null; try { c = Class.forName("FancyToy"); } catch(ClassNotFoundException e) { System.err.println("Can't find FancyToy"); throw 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) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw 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 reference 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 reference 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 reference
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 reference with no further
type information known at compile-time. And when you create a new instance, you
get back an Object reference. But that reference 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. In the next section,
you’ll see how to dynamically create objects of classes using any
constructor, with the Java reflection API.
The final method in the listing is
printInfo( ), which
takes a Class reference 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.
Reflection: run-time
|