还有什么可以在java中抛出ClassCastException?

tgk*_*rog 59 java caching classcastexception

这是一个面试问题.

采访结束了,但这个问题仍在我的脑海中.

我不能问面试官,因为我没有得到这份工作.

场景:

  • 将类C1的对象放入带有键"a"的缓存中

后来的代码:

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

此代码抛出ClassCastException.

原因是什么?

我说是因为别人另一个对象放在同一把钥匙上,所以把它覆盖了.我被告知不,想到其他可能性.

我说也许在这个节点上没有定义类C1的jar(不确定这是否会导致类强制转换或ClassNotFoundException,但我现在正在抓住任何领导.然后我说可能是错误的类版本?他们说所有节点都有相同的C1类罐子.

编辑/添加询问获取是否抛出ClassCast但被告知没有.在那之后我告诉他我解决这个问题的行动是放入一个模拟动作的测试jsp,并在异常之后放置更好的日志记录(堆栈跟踪).这是问题的第二部分(如果在生产中发生这种情况,你会做什么以及如果做什么)

有没有其他人对缓存获取为什么会导致演员问题有任何想法?

Kep*_*pil 52

一个原因可能是插入对象的代码部分使用的是与检索它的代码不同的类加载器.
类的实例不能转换为由不同类加载器加载的同一个类.

对编辑的回应:

如果在生产中发生这种情况你会怎么做?

当读取和插入模块各自包含相同的罐子时,通常会发生这种情况C1.
由于大多数容器首先尝试父类加载器,然后是本地类加载器(父第一策略),因此问题的常见解决方案是将类加载到插入和读取模块的最近公共父级中.
如果将包含C1类的模块移动到父模块,则强制两个子模块从父模块中获取类,从而删除任何类加载器差异.

  • 这绝对是正确的答案.只需添加一个:如果缓存作为远程EJB访问,则对象将被序列化和反序列化,因此不会发生类强制转换异常(但如果您具有不兼容的类版本,则可能会出现序列化异常).因此,对OP的更深层答案实际上是关于Java EE应用程序的体系结构,特别是您不能直接调用其他Java EE容器中的对象.听起来像面试官违反规则并被它咬了. (4认同)
  • 这个确切的场景让我陷入了Tomcat的困境.Tomcat使用Hierarchical类加载器,如果您有跨越层次结构边界的类依赖项,除非将所有类移动到高级公共级别,否则基本上不能进行转换 (2认同)
  • @tgkprog:不确定您是否同意远程EJB /序列化.但是,如果您在JVM(或相同或不同的服务器)之间共享缓存,则对象肯定是通过序列化传递的,如果JAR是相同的文件或副本则无关紧要.如果你在Tomcat中加载比EAR(系统类路径?)更高级别的JAR,那么你可以使它工作,但是i)你不能再动态重新加载共享JAR而ii)你可能会得到非常奇怪的行为重新加载,迁移或更新WAR/EAR时,尤其是 对于您认为已卸载的类的实例. (2认同)

Ed *_*ese 31

ClassCastException,如果同一个类是由多个不同的类加载器和正在被它们之间共享的类的实例加载可以发生.

请考虑以下示例层次结构.

SystemClassloader <--- AppClassloader <--+--- Classloader1
                                         |
                                         +--- Classloader2
Run Code Online (Sandbox Code Playgroud)

我认为通常以下情况都是正确的,但可以编写自定义类加载器而偏离此.

  • 可以在任何类加载器上下文中访问由SystemClassloader加载的类的实例.
  • 可以在任何类加载器上下文中访问由AppClassloader加载的类的实例.
  • Classloader2无法访问由Classloader1加载的类的实例.
  • Classloader1无法访问由Classloader2加载的类的实例.

如上所述,出现这种情况的常见情况是Web应用程序部署,其中一般来说AppClassloader非常类似于appserver中配置的类路径,然后Classloader1和Classloader2代表单独部署的Web应用程序的类路径.

如果多个Web应用程序部署相同的JAR /类,那么ClassCastException如果Web应用程序有任何共享对象(如缓存或共享会话)的机制,则可能会发生这种情况.

可能发生这种情况的另一种类似情况是,如果Web应用程序加载了类,并且这些类的实例存储在用户会话或缓存中.如果重新部署了Web应用程序,则这些类将由新的类加载器重新加载,并尝试从会话或缓存中访问对象将引发此异常.

在Production中避免此问题的一种方法是将JAR在类加载器层次结构中向上移动.因此,不是在每个Web应用程序中包含相同的JAR,而是将它们包含在appserver的类路径中可能会更好.通过这样做,类只加载一次,并且可以被所有Web应用程序访问.

另一种避免这种情况的方法是仅在共享对象的接口上操作.接口然后需要在类加载器层次结构中更高的位置加载,但类本身不需要.从缓存中获取对象的示例将是相同的,但C1该类将被C1实现的接口替换.

下面是一些可以独立运行以重新创建此方案的示例代码.它不是最简洁的,当然可能有更好的方式来说明它,但它确实抛出异常,原因如上所述.

a.jar包中有以下两个类,AMyRunnable.这些由两个独立的类加载器多次加载.

package classloadertest;

public class A {
    private String value;

    public A(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "<A value=\"" + value + "\">";
    }
}
Run Code Online (Sandbox Code Playgroud)

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());
    }
}
Run Code Online (Sandbox Code Playgroud)

独立于上面的类运行以下程序.它不能与上述类共享类路径,以确保它们是从JAR文件加载的.

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

运行此命令时,将显示以下输出:

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)
Run Code Online (Sandbox Code Playgroud)

我也将源代码放在GitHub上.