|
The Java Specialists' Newsletter
Issue 018 2001-05-03
Category:
Language
Java version: Class names don't identify a classby Dr. Heinz M. Kabutz
Welcome to the 18th issue of "The Java(tm) Specialists'
Newsletter", now sent to 38 countries on all continents of our
globe. Please remember to forward the newsletter to others who
might be interested.
Would you like to really understand Java concurrency? Join us for an
in-depth study of how threading works in Java. During the course,
you will learn how to write correct and fast multi-threaded Java code.
Please
click here if you would like to learn more. Class names don't identify a class
This week I want to introduce the concepts of having Class
objects in the VM with the exact same name, but being completely
different. I had the opportunity to question Dr. Jung, who has
been using this for a while now, and at long last, my small brain
has made "click" and I understand (I think). In two weeks time,
he is going to write the newsletter (he wrote the piece on
dynamic proxies) and he will demonstrate how you can build up a
sibling hierarchy of class loaders which you can use to
automatically redeploy classes and find dependencies, etc. I
didn't quite follow all of his explanations, so I'm looking
forward to read what he has to say about this topic. In the
meantime, to prepare us all, I've written a simple example to
demonstrate how it works.
In JDK 1.2, SUN added a new approach to class loading which
allows you to identify classes not only by the class name, but
also by the context in which it was loaded. We can set the
ClassLoader for a Thread, which we can then use to load classes.
By having a different classloader, you are effectively
constructing a new instance of Class, which is then completely
different from other instances of Class. This was, I think, also
possible in JDK 1.1, but it is much easier to make an instance of
a ClassLoader in JDK 1.2 as there is now a URLClassLoader which
you can point to a directory where it will load the classes from.
For example, say we have two directories, a1 and a2. In each of
these directories we have a class A:
//: a1/A.java
public class A {
public String toString() { return "This is the first class"; }
}
//: a2/A.java
public class A {
public String toString() { return "This is the second class"; }
}
As you will agree, the two classes are completely different.
They can have different method definitions, data members or
access control. The normal way of using these two classes is to
choose at compile/run time which one you wish to use. For
example, we may have a class NormalTest below:
//: NormalTest.java
public class NormalTest {
public static void main(String[] args) {
System.out.println(new A());
}
}
To compile this class, we have to specify the directory where A
resides, either a1 or a2. Since the signature is the same, we
can compile with one class and run with the other class if we
want.
javac -classpath .;a1 NormalTest.java
java -classpath .;a2 NormalTest
would result in "This is the second class" being displayed on the
console.
What happens if we want to have instances of both A classes in
use at the same time? Normally we cannot do that, but if we use
ClassLoaders we can.
//: Loader.java
import java.net.*;
public class Loader {
public static void main(String[] args) throws Exception {
ClassLoader a1 = new URLClassLoader(
new URL[] {new URL("file:a1/")}, null);
ClassLoader a2 = new URLClassLoader(
new URL[] {new URL("file:a2/")}, null);
Class c1 = a1.loadClass("A");
Class c2 = a2.loadClass("A");
System.out.println("c1.toString(): " + c1);
System.out.println("c2.toString(): " + c2);
System.out.println("c1.equals(c2): " + c1.equals(c2));
System.out.println("c1.newInstance(): " + c1.newInstance());
System.out.println("c2.newInstance(): " + c2.newInstance());
}
}
The two classes, both called "A", are loaded with different
ClassLoader objects, and are thus, as far as the VM is concerned,
different classes altogether. We can print the names of the
classes, compare the two classes (even though their names are the
same, the classes are not equal, you should therefore never
compare just the class names if you want to compare classes,
rather use the Class.equals() method), and make instances of them
by calling the newInstance() method.
The output if we run Loader is:
c1.toString(): class A
c2.toString(): class A
c1.equals(c2): false
c1.newInstance(): This is the first class
c2.newInstance(): This is the second class
We can also let these two "A" classes have a common superclass.
For example, say we have a superclass called Parent.java located
in the root directory:
//: Parent.java
public class Parent {
public String toString() {
return "Thanks for caring... but what do you want??? ";
}
}
And our A.java classes are now written as:
//: a1/A.java
public class A extends Parent {
public String toString() {
return super.toString() + "This is the first class";
}
}
//: a2/A.java
public class A extends Parent {
public String toString() {
return super.toString() + "This is the second class";
}
}
We then need to have a common parent ClassLoader which we use to
load the Parent.class file. We have to specify the location
to start looking for Parent.class, and the locations are searched
in a hierarchical fasion. Note that the compile-time Parent
class is loaded with a different ClassLoader to the one loaded
with the URLClassLoader called "parent" so they refer to a
different class altogether, which means we cannot type-cast an
instance of "A" to a Parent. Also, if we load the class "Parent"
without using the classloader, we will see that it is not equal
to the superclass of "c1".
//: Loader.java
import java.net.*;
public class Loader {
public static void main(String[] args) throws Exception {
ClassLoader parent = new URLClassLoader(
new URL[] {new URL("file:./")}, null);
ClassLoader a1 = new URLClassLoader(
new URL[] {new URL("file:a1/")}, parent);
ClassLoader a2 = new URLClassLoader(
new URL[] {new URL("file:a2/")}, parent);
Class c1 = a1.loadClass("A");
Class c2 = a2.loadClass("A");
System.out.println(c1.newInstance());
System.out.println(c2.newInstance());
System.out.println(
c1.getSuperclass().equals(c2.getSuperclass()));
System.out.println(
Class.forName("Parent").equals(c1.getSuperclass()));
try {
Parent p = (Parent)c1.newInstance();
} catch(ClassCastException ex) {
ex.printStackTrace();
}
System.out.println("We expected to get ClassCastException");
}
}
Thanks for caring... but what do you want??? This is the first class
Thanks for caring... but what do you want??? This is the second class
true
false
java.lang.ClassCastException: A
at Loader.main(Loader.java:18)
We expected to get ClassCastException
Note that the super classes of both "A" classes are equal.
Where is this all this useful? It is very useful when you have
an application server into which you want to deploy business
objects written by different people. It is entirely feasible
that you have two developers with different versions of classes
deploying their applications onto the same server. You don't
necessarily want to start a new VM for each deployment, and so
with ClassLoaders it is possible to have lots of classes with the
same name running in the same memory space but not conflicting
with one another. They would also share the common JDK classes
with one another, so we would not have to have a
java.util.ArrayList class loaded for each of the ClassLoaders.
Until next week, and please remember to forward this newsletter
in its entirety to as many Java users as you know who would be
interested.
Heinz
Language Articles
Related Java Course
Discuss at The Java Specialist Club
|