What else can throw a ClassCastException in java?
One reason could be that the part of the code inserting the object uses a different classloader than the code retrieving it.
An instance of a class can not be cast to the same class loaded by a different classloader.
Response to the edit:
What would you do if this happened in production?
This generally happens when the reading and inserting modules each include the same jar containing C1
.
Since most containers try the parent classloader first, and then the local classloader (the Parent first strategy), the common solution to the problem is to instead load the class in the closest common parent to the inserting and reading modules.
If you move the module containing the C1
class to the parent module, you force both submodules to get the class from the parent, removing any classloader differences.
The ClassCastException
can occur if the same class was loaded by multiple different classloaders and instances of the classes are being shared between them.
Consider the following example hierarchy.
SystemClassloader <--- AppClassloader <--+--- Classloader1
|
+--- Classloader2
I think in general the following are true but custom classloaders can be written which stray from this.
- Instances of classes loaded by SystemClassloader are accessible in any of the classloader contexts.
- Instances of classes loaded by AppClassloader are accessible in any of the classloader contexts.
- Instances of classes loaded by Classloader1 are not accessible by Classloader2.
- Instances of classes loaded by Classloader2 are not accessible by Classloader1.
As mentioned a common scenario where this occurs is web app deployments where generally speaking AppClassloader closely resembles the classpath configured in the appserver and then the Classloader1 and Classloader2 represent the classpaths of the individually deployed web apps.
If multiple web apps deploy the same JARs/classes then the ClassCastException
can occur if there is any mechanism for the web apps to share objects such as a cache or shared session.
Another similar scenario where this can occur is if the classes are loaded by the web app and instances of these classes are stored in the user session or cache. If the web app is redeployed then these classes are reloaded by a new classloader and attempting to access the objects from the session or cache will throw this exception.
One method of avoiding this issue in Production is to move the JARs higher up in the classloader hierarchy. So instead of including the same JAR in each web app it may work better to include these in the classpath of the appserver. By doing this the classes are loaded only a single time and are accessible by all web apps.
Another method of avoiding this is to operate only on the interfaces that the shared objects. The interfaces then need to be loaded higher up in the classloader hierarchy but the classes themselves do not. Your example of getting the object from the cache would be the same but the C1
class would be replaced by an interface that C1
implements.
Below is some sample code that can be run independently to recreate this scenario. It's not the most concise and there certainly may be better ways to illustrate it but it does throw the exception for the reasons mentioned above.
In a.jar
package the following two classes, A
and MyRunnable
. These are loaded multiple times by two independent classloaders.
package classloadertest;
public class A {
private String value;
public A(String value) {
this.value = value;
}
@Override
public String toString() {
return "<A value=\"" + value + "\">";
}
}
And
package classloadertest;
import java.util.concurrent.ConcurrentHashMap;
public class MyRunnable implements Runnable {
private ConcurrentHashMap<String, Object> cache;
private String name;
public MyRunnable(String name, ConcurrentHashMap<String, Object> cache) {
this.name = name;
this.cache = cache;
}
@Override
public void run() {
System.out.println("Run " + name + ": running");
// Set the object in the cache
A a = new A(name);
cache.putIfAbsent("key", a);
// Read the object from the cache which may be differed from above if it had already been set.
A cached = (A) cache.get("key");
System.out.println("Run " + name + ": cache[\"key\"] = " + cached.toString());
}
}
Independent of the classes above run the following program. It must not share a classpath with the above classes to ensure that they are loaded from the JAR file.
package classloadertest;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void run(String name, ConcurrentHashMap<String, Object> cache) throws Exception {
// Create a classloader using a.jar as the classpath.
URLClassLoader classloader = URLClassLoader.newInstance(new URL[] { new File("a.jar").toURI().toURL() });
// Instantiate MyRunnable from within a.jar and call its run() method.
Class<?> c = classloader.loadClass("classloadertest.MyRunnable");
Runnable r = (Runnable)c.getConstructor(String.class, ConcurrentHashMap.class).newInstance(name, cache);
r.run();
}
public static void main(String[] args) throws Exception {
// Create a shared cache.
ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<String, Object>();
run("1", cache);
run("2", cache);
}
}
On running this the following output is displayed:
Run 1: running
Run 1: cache["key"] = <A value="1">
Run 2: running
Exception in thread "main" java.lang.ClassCastException: classloadertest.A cannot be cast to classloadertest.A
at classloadertest.MyRunnable.run(MyRunnable.java:23)
at classloadertest.Main.run(Main.java:16)
at classloadertest.Main.main(Main.java:24)
I put the source up on GitHub as well.
And finally, someone hacked the String
intern
table for the string "a"
.
See an example of how it can be done here.