X. Détecter les types▲
Le principe de l'identification dynamique de type (Run-Time Type Identification, RTTI) semble très simple à première vue : connaître le type exact d'un objet à partir d'une simple référence sur un type de base.
Cependant, le besoin de RTTI dévoile une pléthore de problèmes intéressants (et souvent complexes) en conception orientée objet, et renforce la question fondamentale de comment structurer ses programmes.
Ce chapitre indique de quelle manière Java permet de découvrir dynamiquement des informations sur les objets et les classes. On le retrouve sous deux formes : le RTTI « classique », qui suppose que tous les types sont disponibles à la compilation et à l'exécution, et le mécanisme de « réflexion », qui permet de découvrir des informations sur les classes uniquement à l'exécution. Le RTTI « classique » sera traité en premier, suivi par une discussion sur la réflexion.
X-A. The need for RTTI▲
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 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 (such as Rhomboid, derived from Shape), 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. In all of the derived classes, draw( ) is overridden, 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.
As a brief review of polymorphism and upcasting, you might code the preceding example as follows:
//: c10:Shapes.java
import
com.bruceeckel.simpletest.*;
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 {
private
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
// Array of Object, not Shape:
Object[] shapeList =
{
new
Circle
(
),
new
Square
(
),
new
Triangle
(
)
}
;
for
(
int
i =
0
; i <
shapeList.length; i++
)
((
Shape)shapeList[i]).draw
(
); // Must cast
monitor.expect
(
new
String[] {
"Circle.draw()"
,
"Square.draw()"
,
"Triangle.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 method 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 (polymorphically) printing something different in each case.
In main( ), specific types of Shape are created and added to an array. This array is a bit odd because it isn't an array of Shape (although it could be), but instead an array of the root class Object. The reason for this is to start preparing you for Chapter 11, which presents tools called collections (also called containers), whose sole job is to hold and manage other objects for you. However, to be generally useful these collections need to hold anything. Therefore they hold Objects. So an array of Object will demonstrate an important issue that you will encounter in the Chapter 11 collections.
In this example, the upcast occurs when the shape is placed in the array of Objects. Since everything in Java (with the exception of primitives) is an Object, an array of Objectscan also hold Shape objects. But during the upcast to Object,the fact is lost that the objects are Shapes. To the array, they are just Objects.
At the point that you fetch an element out of the array with the index operator, things get a little busy. Since the array holds only Objects, indexing naturally produces an Object reference. But we know it's really a Shape reference, 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, because 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 array 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 code that's executed 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 a 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. Or perhaps your method needs to « rotate » a list of shapes, but it makes no sense to rotate a circle so you'd like to skip only the circle, objects. With RTTI, you can ask a Shape reference the exact type that it's referring to, and thus select and isolate special cases.
X-A-1. The Class object▲
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. 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:
//: c10:SweetShop.java
// Examination of the way the class loader works.
import
com.bruceeckel.simpletest.*;
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 {
private
static
Test monitor =
new
Test
(
);
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) {
System.out.println
(
"Couldn't find Gum"
);
}
System.out.println
(
"After Class.forName(
\"
Gum
\"
)"
);
new
Cookie
(
);
System.out.println
(
"After creating Cookie"
);
monitor.expect
(
new
String[] {
"inside main"
,
"Loading Candy"
,
"After creating Candy"
,
"Loading Gum"
,
"After Class.forName(
\"
Gum
\"
)"
,
"Loading Cookie"
,
"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.
You can see from the output that each Class object is loaded only when it's needed, and the static initialization is performed upon class 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, 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, which is being ignored here; the call to forName( ) is being made for its side effect, which is to load the class Gum if it isn't already loaded. In the process of loading, Gum's static clause is executed.
In the preceding example, if Class.forName( ) fails because it can't find the class you're trying to load, it will throw a ClassNotFoundException (ideally, exception names tell you just about everything you need to know about the problem). Here, we simply report the problem and move on, but in more sophisticated programs, you might try to fix the problem inside the exception handler.
X-A-1-a. Class literals▲
Java provides a second way to produce the reference to the Class object: the class literal. In the preceding 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:
… is equivalent to … |
|
---|---|
boolean.class |
Boolean.TYPE |
char.class |
Character.TYPE |
byte.class |
Byte.TYPE |
short.class |
Short.TYPE |
int.class |
Integer.TYPE |
long.class |
Long.TYPE |
float.class |
Float.TYPE |
double.class |
Double.TYPE |
void.class |
Void.TYPE |
My preference is to use the « .class » versions if you can, since they're more consistent with regular classes.
X-A-2. Checking before a cast▲
So far, you've seen RTTI forms including:
- The classic cast; e.g., « (Shape), » which uses RTTI to make sure the cast is correct. This will throw a ClassCastException if you've performed a bad cast.
- The Class object representing the type of your object. The Class object can be queried for useful run time information.
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, which 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 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 by using instanceof. Suppose you have a family of Pet classes:
//: c10:Pet.java
package
c10;
public
class
Pet {}
///:~
//: c10:Dog.java
package
c10;
public
class
Dog extends
Pet {}
///:~
//: c10:Pug.java
package
c10;
public
class
Pug extends
Dog {}
///:~
//: c10:Cat.java
package
c10;
public
class
Cat extends
Pet {}
///:~
//: c10:Rodent.java
package
c10;
public
class
Rodent extends
Pet {}
///:~
//: c10:Gerbil.java
package
c10;
public
class
Gerbil extends
Rodent {}
///:~
//: c10:Hamster.java
package
c10;
public
class
Hamster extends
Rodent {}
///:~
In the coming example, we want to keep track of the number of any particular type of Pet, so we'll need a class that holds this number in an int. You can think of it as a modifiable Integer:
//: c10:Counter.java
package
c10;
public
class
Counter {
int
i;
public
String toString
(
) {
return
Integer.toString
(
i); }
}
///:~
Next, we need a tool that holds two things together: an indicator of the Pet type and a Counter to hold the pet quantity. That is, we want to be able to say « how may Gerbil objects are there? » An ordinary array won't work here, because you refer to objects in an array by their index numbers. What we want to do here is refer to the objects in the array by their Pet type. We want to associate Counter objects with Pet objects. There is a standard data structure, called an associative array, for doing exactly this kind of thing. Here is an extremely simple version:
//: c10:AssociativeArray.java
// Associates keys with values.
package
c10;
import
com.bruceeckel.simpletest.*;
public
class
AssociativeArray {
private
static
Test monitor =
new
Test
(
);
private
Object[][] pairs;
private
int
index;
public
AssociativeArray
(
int
length) {
pairs =
new
Object[length][2
];
}
public
void
put
(
Object key, Object value) {
if
(
index >=
pairs.length)
throw
new
ArrayIndexOutOfBoundsException
(
);
pairs[index++
] =
new
Object[] {
key, value }
;
}
public
Object get
(
Object key) {
for
(
int
i =
0
; i <
index; i++
)
if
(
key.equals
(
pairs[i][0
]))
return
pairs[i][1
];
throw
new
RuntimeException
(
"Failed to find key"
);
}
public
String toString
(
) {
String result =
""
;
for
(
int
i =
0
; i <
index; i++
) {
result +=
pairs[i][0
] +
" : "
+
pairs[i][1
];
if
(
i <
index -
1
) result +=
"
\n
"
;
}
return
result;
}
public
static
void
main
(
String[] args) {
AssociativeArray map =
new
AssociativeArray
(
6
);
map.put
(
"sky"
, "blue"
);
map.put
(
"grass"
, "green"
);
map.put
(
"ocean"
, "dancing"
);
map.put
(
"tree"
, "tall"
);
map.put
(
"earth"
, "brown"
);
map.put
(
"sun"
, "warm"
);
try
{
map.put
(
"extra"
, "object"
); // Past the end
}
catch
(
ArrayIndexOutOfBoundsException e) {
System.out.println
(
"Too many objects!"
);
}
System.out.println
(
map);
System.out.println
(
map.get
(
"ocean"
));
monitor.expect
(
new
String[] {
"Too many objects!"
,
"sky : blue"
,
"grass : green"
,
"ocean : dancing"
,
"tree : tall"
,
"earth : brown"
,
"sun : warm"
,
"dancing"
}
);
}
}
///:~
Your first observation might be that this appears to be a general-purpose tool, so why not put it in a package like com.bruceeckel.tools? Well, it is indeed a general-purpose tool-so useful, in fact, that java.util contains a number of associative arrays (which are also called maps) that do a lot more than this one does, and do it a lot faster. A large portion of Chapter 11 is devoted to associative arrays, but they are significantly more complicated, so using this one will keep things simple and at the same time begin to familiarize you with the value of associative arrays.
In an associative array, the « indexer » is called a key, and the associated object is called a value. Here, we associate keys and values by putting them in an array of two-element arrays, which you see here as pairs. This will just be a fixed-length array that is created in the constructor, so we need index to make sure we don't run off the end. When you put( ) in a new key-value pair, a new two-element array is created and inserted at the next available location in pairs. If index is greater than or equal to the length of pairs, then an exception is thrown.
To use the get( ) method, you pass in the key that you want it to look up, and it produces the associated value as the result or throws an exception if it can't be found. The get( ) method is using what is possibly the least efficient approach imaginable to locate the value: starting at the top of the array and using equals( ) to compare keys. But the point here is simplicity, not efficiency, and the real maps in Chapter 11 have solved the performance problems, so we don't need to worry about it here.
The essential methods in an associative array are put( ) and get( ), but for easy display, toString( ) has been overridden to print the key-value pairs. To show that it works, main( ) loads an AssociativeArray with pairs of strings and prints the resulting map, followed by a get( ) of one of the values.
Now that all the tools are in place, we can use instanceof to count Pets:
//: c10:PetCount.java
// Using instanceof.
package
c10;
import
com.bruceeckel.simpletest.*;
import
java.util.*;
public
class
PetCount {
private
static
Test monitor =
new
Test
(
);
private
static
Random rand =
new
Random
(
);
static
String[] typenames =
{
"Pet"
, "Dog"
, "Pug"
, "Cat"
,
"Rodent"
, "Gerbil"
, "Hamster"
,
}
;
// Exceptions thrown to console:
public
static
void
main
(
String[] args) {
Object[] pets =
new
Object[15
];
try
{
Class[] petTypes =
{
Class.forName
(
"c10.Dog"
),
Class.forName
(
"c10.Pug"
),
Class.forName
(
"c10.Cat"
),
Class.forName
(
"c10.Rodent"
),
Class.forName
(
"c10.Gerbil"
),
Class.forName
(
"c10.Hamster"
),
}
;
for
(
int
i =
0
; i <
pets.length; i++
)
pets[i] =
petTypes[rand.nextInt
(
petTypes.length)]
.newInstance
(
);
}
catch
(
InstantiationException e) {
System.out.println
(
"Cannot instantiate"
);
System.exit
(
1
);
}
catch
(
IllegalAccessException e) {
System.out.println
(
"Cannot access"
);
System.exit
(
1
);
}
catch
(
ClassNotFoundException e) {
System.out.println
(
"Cannot find class"
);
System.exit
(
1
);
}
AssociativeArray map =
new
AssociativeArray
(
typenames.length);
for
(
int
i =
0
; i <
typenames.length; i++
)
map.put
(
typenames[i], new
Counter
(
));
for
(
int
i =
0
; i <
pets.length; i++
) {
Object o =
pets[i];
if
(
o instanceof
Pet)
((
Counter)map.get
(
"Pet"
)).i++
;
if
(
o instanceof
Dog)
((
Counter)map.get
(
"Dog"
)).i++
;
if
(
o instanceof
Pug)
((
Counter)map.get
(
"Pug"
)).i++
;
if
(
o instanceof
Cat)
((
Counter)map.get
(
"Cat"
)).i++
;
if
(
o instanceof
Rodent)
((
Counter)map.get
(
"Rodent"
)).i++
;
if
(
o instanceof
Gerbil)
((
Counter)map.get
(
"Gerbil"
)).i++
;
if
(
o instanceof
Hamster)
((
Counter)map.get
(
"Hamster"
)).i++
;
}
// List each individual pet:
for
(
int
i =
0
; i <
pets.length; i++
)
System.out.println
(
pets[i].getClass
(
));
// Show the counts:
System.out.println
(
map);
monitor.expect
(
new
Object[] {
new
TestExpression
(
"%% class c10
\\
."
+
"(Dog|Pug|Cat|Rodent|Gerbil|Hamster)"
,
pets.length),
new
TestExpression
(
"%% (Pet|Dog|Pug|Cat|Rodent|Gerbil|Hamster)"
+
" :
\\
d+"
, typenames.length)
}
);
}
}
///:~
In main( ) an array petTypes of Class objects is created using Class.forName( ). Since the Pet objects are in package c09, the package name must be used when naming the classes.
Next, the pets array is filled by randomly indexing into petTypes and using the selected Class object to generate a new instance of that class with Class.newInstance( ), which uses the default (no-arg) class constructor to generate the new object.
Both forName( ) and newInstance( ) can generate exceptions, which you can see handled in the catch clauses following the try block. Again, the names of the exceptions are relatively useful explanations of what went wrong (IllegalAccessException relates to a violation of the Java security mechanism).
After creating the AssociativeArray, it is filled with key-value pairs of pet names and Counter objects. Then each Pet in the randomly-generated array is tested and counted using instanceof. The array and AssociativeArray are printed so you can compare the results.
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 preceding example 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 array 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 field 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.
X-A-2-a. Using class literals▲
It's interesting to see how the PetCount.java example can be rewritten using class literals. The result is cleaner in many ways:
//: c10:PetCount2.java
// Using class literals.
package
c10;
import
com.bruceeckel.simpletest.*;
import
java.util.*;
public
class
PetCount2 {
private
static
Test monitor =
new
Test
(
);
private
static
Random rand =
new
Random
(
);
public
static
void
main
(
String[] args) {
Object[] pets =
new
Object[15
];
Class[] petTypes =
{
// Class literals:
Pet.class
,
Dog.class
,
Pug.class
,
Cat.class
,
Rodent.class
,
Gerbil.class
,
Hamster.class
,
}
;
try
{
for
(
int
i =
0
; i <
pets.length; i++
) {
// Offset by one to eliminate Pet.class:
int
rnd =
1
+
rand.nextInt
(
petTypes.length -
1
);
pets[i] =
petTypes[rnd].newInstance
(
);
}
}
catch
(
InstantiationException e) {
System.out.println
(
"Cannot instantiate"
);
System.exit
(
1
);
}
catch
(
IllegalAccessException e) {
System.out.println
(
"Cannot access"
);
System.exit
(
1
);
}
AssociativeArray map =
new
AssociativeArray
(
petTypes.length);
for
(
int
i =
0
; i <
petTypes.length; i++
)
map.put
(
petTypes[i].toString
(
), new
Counter
(
));
for
(
int
i =
0
; i <
pets.length; i++
) {
Object o =
pets[i];
if
(
o instanceof
Pet)
((
Counter)map.get
(
"class c10.Pet"
)).i++
;
if
(
o instanceof
Dog)
((
Counter)map.get
(
"class c10.Dog"
)).i++
;
if
(
o instanceof
Pug)
((
Counter)map.get
(
"class c10.Pug"
)).i++
;
if
(
o instanceof
Cat)
((
Counter)map.get
(
"class c10.Cat"
)).i++
;
if
(
o instanceof
Rodent)
((
Counter)map.get
(
"class c10.Rodent"
)).i++
;
if
(
o instanceof
Gerbil)
((
Counter)map.get
(
"class c10.Gerbil"
)).i++
;
if
(
o instanceof
Hamster)
((
Counter)map.get
(
"class c10.Hamster"
)).i++
;
}
// List each individual pet:
for
(
int
i =
0
; i <
pets.length; i++
)
System.out.println
(
pets[i].getClass
(
));
// Show the counts:
System.out.println
(
map);
monitor.expect
(
new
Object[] {
new
TestExpression
(
"%% class c10
\\
."
+
"(Dog|Pug|Cat|Rodent|Gerbil|Hamster)"
,
pets.length),
new
TestExpression
(
"%% class c10
\\
."
+
"(Pet|Dog|Pug|Cat|Rodent|Gerbil|Hamster) :
\\
d+"
,
petTypes.length)
}
);
}
}
///:~
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.
X-A-2-b. 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:
//: c10:PetCount3.java
// Using isInstance()
package
c10;
import
com.bruceeckel.simpletest.*;
import
java.util.*;
public
class
PetCount3 {
private
static
Test monitor =
new
Test
(
);
private
static
Random rand =
new
Random
(
);
public
static
void
main
(
String[] args) {
Object[] pets =
new
Object[15
];
Class[] petTypes =
{
// Class literals:
Pet.class
,
Dog.class
,
Pug.class
,
Cat.class
,
Rodent.class
,
Gerbil.class
,
Hamster.class
,
}
;
try
{
for
(
int
i =
0
; i <
pets.length; i++
) {
// Offset by one to eliminate Pet.class:
int
rnd =
1
+
rand.nextInt
(
petTypes.length -
1
);
pets[i] =
petTypes[rnd].newInstance
(
);
}
}
catch
(
InstantiationException e) {
System.out.println
(
"Cannot instantiate"
);
System.exit
(
1
);
}
catch
(
IllegalAccessException e) {
System.out.println
(
"Cannot access"
);
System.exit
(
1
);
}
AssociativeArray map =
new
AssociativeArray
(
petTypes.length);
for
(
int
i =
0
; i <
petTypes.length; i++
)
map.put
(
petTypes[i].toString
(
), new
Counter
(
));
for
(
int
i =
0
; i <
pets.length; i++
) {
Object o =
pets[i];
// Using Class.isInstance() to eliminate
// individual instanceof expressions:
for
(
int
j =
0
; j <
petTypes.length; ++
j)
if
(
petTypes[j].isInstance
(
o))
((
Counter)map.get
(
petTypes[j].toString
(
))).i++
;
}
// List each individual pet:
for
(
int
i =
0
; i <
pets.length; i++
)
System.out.println
(
pets[i].getClass
(
));
// Show the counts:
System.out.println
(
map);
monitor.expect
(
new
Object[] {
new
TestExpression
(
"%% class c10
\\
."
+
"(Dog|Pug|Cat|Rodent|Gerbil|Hamster)"
,
pets.length),
new
TestExpression
(
"%% class c10
\\
."
+
"(Pet|Dog|Pug|Cat|Rodent|Gerbil|Hamster) :
\\
d+"
,
petTypes.length)
}
);
}
}
///:~
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).
X-A-2-c. 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:
//: c10:FamilyVsExactType.java
// The difference between instanceof and class
package
c10;
import
com.bruceeckel.simpletest.*;
class
Base {}
class
Derived extends
Base {}
public
class
FamilyVsExactType {
private
static
Test monitor =
new
Test
(
);
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
(
));
monitor.expect
(
new
String[] {
"Testing x of type class c10.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 c10.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"
}
);
}
}
///:~
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. 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.
X-B. RTTI syntax▲
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 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:
//: c10:ToyTest.java
// Testing class Class.
import
com.bruceeckel.simpletest.*;
interface
HasBatteries {}
interface
Waterproof {}
interface
Shoots {}
class
Toy {
// Comment out the following default constructor
// to see NoSuchMethodError from (*1*)
Toy
(
) {}
Toy
(
int
i) {}
}
class
FancyToy extends
Toy
implements
HasBatteries, Waterproof, Shoots {
FancyToy
(
) {
super
(
1
); }
}
public
class
ToyTest {
private
static
Test monitor =
new
Test
(
);
static
void
printInfo
(
Class cc) {
System.out.println
(
"Class name: "
+
cc.getName
(
) +
" is interface? ["
+
cc.isInterface
(
) +
"]"
);
}
public
static
void
main
(
String[] args) {
Class c =
null
;
try
{
c =
Class.forName
(
"FancyToy"
);
}
catch
(
ClassNotFoundException e) {
System.out.println
(
"Can't find FancyToy"
);
System.exit
(
1
);
}
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.out.println
(
"Cannot instantiate"
);
System.exit
(
1
);
}
catch
(
IllegalAccessException e) {
System.out.println
(
"Cannot access"
);
System.exit
(
1
);
}
printInfo
(
o.getClass
(
));
monitor.expect
(
new
String[] {
"Class name: FancyToy is interface? [false]"
,
"Class name: HasBatteries is interface? [true]"
,
"Class name: Waterproof is interface? [true]"
,
"Class name: Shoots is interface? [true]"
,
"Class name: Toy is interface? [false]"
}
);
}
}
///:~
You can see that class FancyToy is quite complicated, since it inherits from Toy and implements the interfaces HasBatteries, Waterproof, and Shoots. 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 preceding example, 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 (Application Programmer Interface).
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( ). Thus, with the Class object you can find out just about everything you want to know about an object.
X-C. Reflection: run time class information▲
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 reference 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 this class that shows up later while it's compiling the code for your program, 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, that it exposes parts of itself, and that it allows 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 provides a structure for component-based programming through JavaBeans (described in Chapter 14).
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 to have objects distributed across many machines. This distribution can happen for a number of reasons. For example, perhaps you're doing a computation-intensive task, and in order to speed things up, you want to break it up and put pieces on machines that are idle. In other situations you might want to place code that handles particular types of tasks (e.g., « Business Rules » in a multitier 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.
The class Class (described previously in this chapter) supports 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 the JDK 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.
X-C-1. A class method extractor▲
You'll rarely need to use the reflection tools directly; they're in the language to support other Java features, such as object serialization (Chapter 12) and JavaBeans (Chapter 14). 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 JDK 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. (50) Fortunately, reflection provides a way to write a simple tool that will automatically show you the entire interface. Here's the way it works:
//: c10:ShowMethods.java
// Using reflection to show all the methods of a class,
// even if the methods are defined in the base class.
// {Args: ShowMethods}
import
java.lang.reflect.*;
import
java.util.regex.*;
public
class
ShowMethods {
private
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'"
;
private
static
Pattern p =
Pattern.compile
(
"
\\
w+
\\
."
);
public
static
void
main
(
String[] args) {
if
(
args.length <
1
) {
System.out.println
(
usage);
System.exit
(
0
);
}
int
lines =
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
(
p.matcher
(
m[i].toString
(
)).replaceAll
(
""
));
for
(
int
i =
0
; i <
ctor.length; i++
)
System.out.println
(
p.matcher
(
ctor[i].toString
(
)).replaceAll
(
""
));
lines =
m.length +
ctor.length;
}
else
{
for
(
int
i =
0
; i <
m.length; i++
)
if
(
m[i].toString
(
).indexOf
(
args[1
]) !=
-
1
) {
System.out.println
(
p.matcher
(
m[i].toString
(
)).replaceAll
(
""
));
lines++
;
}
for
(
int
i =
0
; i <
ctor.length; i++
)
if
(
ctor[i].toString
(
).indexOf
(
args[1
]) !=
-
1
) {
System.out.println
(
p.matcher
(
ctor[i].toString
(
)).replaceAll
(
""
));
lines++
;
}
}
}
catch
(
ClassNotFoundException e) {
System.out.println
(
"No such class: "
+
e);
}
}
}
///:~
The Class methods getMethods( ) and getConstructors( ) return an array of Method and array of 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 extracts the command line information, determines if a particular signature matches your target string (using indexOf( )), and strips off the name qualifiers.
To strip the name qualifiers like « java.lang. » from « java.lang.String, » Java JDK 1.4 regular expressions offer a powerful and succinct tool that has been available in some languages for many years. You've already seen simple usage of regular expressions inside the expect( ) statements of the com.bruceeckel.simpletest.Test class. In the preceding example, you can see the basic coding steps necessary to use regular expressions in your own programs.
After importing java.util.regex, you first compile the regular expression by using the staticPattern.compile( ) method, which produces a Pattern object using the string argument. In this case, the argument is
"
\\
w+
\\
."
To understand this or any other regular expression, look at the JDK documentation under java.util.regex.Pattern. For this one, you'll find that '\w' means « a word character: [a-zA-Z_0-9]. » The '+' means « one or more of the preceding expression »-so in this case, one or more word characters-and the '\.' produces a literal period (rather than the period operator, which means « any character » in a regular expression). So this expression will match any sequence of word characters followed by a period, which is exactly what we need to strip off the qualifiers.
After you have a compiled Pattern object, you use it by calling the matcher( ) method, passing the string that you want to search. The matcher( ) method produces a Matcher object, which has a set of operations to choose from (you can see all of these in the JDK documentation for java.util.regex.Matcher). Here, the replaceAll( ) method is used to replace all the matches with empty strings-that is, to delete the matches.
As a more compact alternative, you can use the regular expressions built into the String class. For example, the last use of replaceAll( ) in the preceding program could be rewritten from:
p.matcher
(
ctor[i].toString
(
)).replaceAll
(
""
)
to
ctor[i].toString
(
).replaceAll
(
"
\\
w+
\\
."
, ""
)
without precompiling the regular expression. This form is good for single-shot uses of regular expressions, but the precompiled form is significantly more efficient if you need to use the regular expression more than once, as is the case with this example.
This example 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 the JDK 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 (there will be examples of this later in this book). Although initially this is something you may not think you'll ever need, the value of full reflection can be quite surprising.
An enlightening 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, package access), the synthesized default constructor no longer shows up in the output. The synthesized default constructor is automatically given the same access as the class.
Another interesting experiment is to invoke java ShowMethods java.lang.String with an extra argument of char, int, String, etc.
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 hunting through the index or class hierarchy in the JDK documentation, or if you don't know whether that class can do anything with, for example, Color objects.
Chapter 14 contains a GUI version of this program (customized to extract information for Swing components) so you can leave it running while you're writing code, to allow quick lookups.
X-D. Summary▲
RTTI allows you to discover type information from an anonymous base-class reference. 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, one 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. Suppose 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. Be wary, however, of programming for efficiency too soon. It's a seductive trap. It's best to get the program working first, then decide if it's running fast enough, and only then should you attack efficiency issues-with a profiler (see Chapter 15).
X-E. Exercises▲
Solutions to selected exercises can be found in the electronic document The Thinking in Java Annotated Solution Guide, available for a small fee from www.BruceEckel.com.
- Add Rhomboid to Shapes.java. Create a Rhomboid, upcast it to a Shape, then downcast it back to a Rhomboid. Try downcasting to a Circle and see what happens.
- Modify Exercise 1 so that it uses instanceof to check the type before performing the downcast.
- Modify Shapes.java so that it can « highlight » (set a flag) in all shapes of a particular type. The toString( ) method for each derived Shape should indicate whether that Shape is « highlighted. »
- Modify SweetShop.java so that each type of object creation is controlled by a command-line argument. That is, if your command line is « java SweetShop Candy, » then only the Candy object is created. Notice how you can control which Class objects are loaded via the command-line argument.
- Add a new type of Pet to PetCount3.java. Verify that it is created and counted correctly in main( ).
- Write a method that takes an object and recursively prints all the classes in that object's hierarchy.
- Modify Exercise 6 so that it uses Class. getDeclaredFields( ) to also display information about the fields in a class.
- In ToyTest.java, comment out Toy's default constructor and explain what happens.
- Incorporate a new kind of interface into ToyTest.java and verify that it is detected and displayed properly.
- Write a program to determine whether an array of char is a primitive type or a true object.
- Implement clearSpitValve( ) as described in the summary.
- Implement the rotate(Shape) method described in this chapter, such that it checks to see if it is rotating a Circle (and, if so, doesn't perform the operation).
- In ToyTest.java, use reflection to create a Toy object using the nondefault constructor.
- Look up the interface for java.lang.Class in the JDK documentation from java.sun.com. Write a program that takes the name of a class as a command-line argument, then uses the Class methods to dump all the information available for that class. Test your program with a standard library class and a class you create.
- Modify the regular expression in ShowMethods.java to additionally strip off the keywords native and final (hint: use the « or » operator '|').