What else can throw a ClassCastException in java?
Asked Answered
S

4

60

This is an interview question.

The interview is over, but this question is still on my mind.

I can't ask the interviewer, as I did not get the job.

Scenario:

  • put object of class C1 in to a cache with key "a"

Later code:

C1 c1FromCache = (C1) cache.get("a");

This code throws a ClassCastException.

What can the reasons be?

I said because someone else put another object with the same key and so overwrote it. I was told no, think of other possibilities.

I said maybe the jar defining class C1 was not available on this node (not sure if this would result in a class cast or a ClassNotFoundException, but I was grasping for any lead now. Then I said maybe wrong version of class? They said the same jar of class C1 is there in all nodes).

Edit/ Add Asked if the get was throwing the ClassCast but was told no. after that i told him my action to resolve such an issue would be to drop in a test jsp that would mimic the actions and put better logging (stack trace) after the exception. that was the 2nd part of the question (why and what would you do if this happened in production)

Does anyone else have any ideas about why a cache get would result in a cast issue?

Scan answered 18/4, 2013 at 20:8 Comment(6)
this is a sick/tricky question for an interviewPhan
Maybe get's implementation is throw new ClassCastException();..?Issuant
oh forgot this asked him and he said no its the local castScan
Maybe it was com.abc.C1 and cache.get("a") returns com.xyz.C1 :PDari
Silly question ..cache is the Map instance?Resting
@sandeep-pandey Sandeep Not a silly question. Is this case I think cache would be a remote cache in other OS process. Like Redis or memcache. That is why the class changed/ or not available in the GETScan
M
52

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.

Month answered 18/4, 2013 at 20:15 Comment(15)
some code proving this will make most probably reach a gold batch. +1Phan
This exact scenario bit me in Tomcat. Tomcat uses Hierarchical class loaders and if you have class dependencies that cross the hierarchical boundary you essentially CAN'T cast unless you move all classes to the high common levelExcept
Wow I was going to say were there 2 different web apps in same JVM but then thought better not why would that cause class cast. Learning : just like in code & in Db its good to have a function in one places, seems the same is true elsewhere too (common lib for all web apps/ add container main class path if you can or 1 app per container).Scan
@Eugene: It is difficult to present code where this exception would occur in normal applications, since it needs both some kind of container with multiple classloaders, and multiple modules using the same class.Month
@Month yup I have to agree, I tried to do it yesterday and could not.Phan
This is absolutely the right answer. Just one addition: if the cache is accessed as an remote EJB, the object will be serialized and deserialized and as such the class cast exception won't happen (but you may get serialization exceptions if you have incompatible class versions). So a deeper answer to the OP is really about architecture of an Java EE application, specifically that you must not make a direct calls to an object in a different Java EE container. Sounds like the interviewer had broken the rules and been bitten by it.Esbensen
@andrew-alcock we do that with our cache. node 1 saved 'obj' in cache. node 2 (different physical or same physical system but with different jvm both pointing to same jars in the main jboss path - not in particular web app lib) ; memcache from code.google.com/p/quickcached it works fine. also use it in another place - 2 web apps, same jboss. works fine. as christian-bongiorno said we have it in the high common level. never thought of this during the interview though :)Scan
keppil thank you for the answer. choose Ed Plese for bounty as he made a sampleScan
@tgkprog: Not sure if you're agreeing with Remote EJB/serialization or not. But, if you are sharing a cache between JVMs (or same or different servers), objects are definitely passed by serialization and it's irrelevant if the JAR is the same file or a copy. If you load the JAR in Tomcat at a higher level than the EAR (the system classpath?) then you can make it work, but i) you can't dynamically reload the shared JAR any more and ii) you may get very strange behaviour when reloading, migrating or updating your WAR/EAR, esp. for instances of classes you think you've unloaded.Esbensen
what you said about ejb make's sense andrew-alcock About the dynamic part - that is true, we happily restart when we need to. have 2 extra nodes and a web layer where we can stop sending requests to a node. so that is what we do. did not understand the last part - when will that occur -> "you may get very strange behaviour when reloading, migrating or updating your WAR/EAR, esp. for instances of classes you think you've unloaded."Scan
do i say @andrew-alcock or just andrew-alcock ?Scan
@tgkprog. Please call me Andrew :) Strange behaviour: If you unload an EAR/WAR and an object instance from one of its classes remain in a cache, then i) the class itself won't GC leading to a memory leak, ii) and any class or instance referenced by the object will also remain, and iii) therefore you will have objects and classes present from the previous version of the EAR/WAR which will not only cause class cast exceptions but (if called via reflection of Java APIs) will have the behaviour of the previous version of the class.Esbensen
do i say @andrew-alcock or just andrew-alcock in context to getting stack overflow to show an item in your inboxScan
need to investigate this in our environment. but am not sure we have seen it as yet. maybe its cause when we have a big 'core' change we do it at 1am and bring down all the nodes one after the other. also we use remote cache so maybe this does not apply but is good to know.Scan
@tgkprog: You need the @, I think. If it's a remote cache, then the objects are serialized and so all is good (just be careful of incompatible class changes and your serial uid). It's only when caching i) objects created in different classloaders and ii) in the same jvm that you will get these effects. If you place the JAR at the top of the classloader hierarchy, then you can't unload the class - you have to restart the whole JVM.Esbensen
B
32

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.

Byandby answered 21/4, 2013 at 4:10 Comment(1)
thank you it worked on my local. changed the src so the package name is different in a-src (as expected still behaved the same). will do it in 2 web apps with an external memcache someday :) @ed-pleseScan
S
3

And finally, someone hacked the String intern table for the string "a".

See an example of how it can be done here.

Severus answered 21/4, 2013 at 20:21 Comment(5)
interesting though not sure how it helps here?Scan
the blog post sums it up "The practical application of this blog? Let's face it, none. "Scan
@Scan - Question was how could the retrieved object be of the wrong class - clearly if the key changed then all bets are off. You could put X with key "a" then change "a" and put Y with key "a". Subsequent gets with key "a" would then get Y.Severus
yes that was my first answer but was told that had not happenedScan
@Scan - I suspect that the interviewer was looking more at your approach to solving the problem than for a correct answer to the question. I would suggest they would have said "no - not that" to whatever you suggested.Severus
P
0

Well maybe because C1 is an abstract class, and the get function also returns on object(of a subclass of C1 of course) which was casted to C1 before returning?

Phil answered 21/4, 2013 at 5:1 Comment(1)
Well upcasting does not throw any exception in Java.!Saltpeter

© 2022 - 2024 — McMap. All rights reserved.