解决一个诡异的java空指针问题的案例

文章详细描述了在研究Java类加载器时遇到的一个问题,即自定义类加载器尝试加载类时出现空指针异常。问题的关键在于尝试加载Object.class时,由于从classpath无法读取到其字节码,但通过父类加载器(AppClassLoader)可以成功加载。添加条件判断避免了空指针异常的发生,因为Object.class已经由JVM加载。这展示了类加载器机制的复杂性和双亲委派模型的重要性。
摘要由CSDN通过智能技术生成

最近在看java类加载器的资料,于是写了一个自定义类加载器测试一下,结果就悲剧了,直接报空指针!

 跟着报错指引看代码37行是什么东东?

 就是一个inputStream, 然后看看它的定义:

 这玩意就是从classpath读取class字节码文件,然后转换成IO流,并没有什么问题啊。

打个断点看看,发现inputStream是有值的:

 这他妈是什么问题呢,一时间觉得很迷茫,不知从何入手。

而且最诡异的是一旦加上下面一行return的代码,空指针报错就没了!

if (clazz !=null) return clazz; 

 这是不是很诡异,看来只能重新再次debug了。

再次调试发现inputStream仍然是有值的,但是走完这段代码以后发现该代码又会再来一遍:

 此时的类名变成了Object.class!但是之前的读取的类名应该是ClassData.class, 如下所示:

 看来这就是问题的关键了! 可以看到此时的inputStream真的是空值null  

正是因为从classpath读取不到java.lang.Object的字节码文件,导致读取IO时生成了null,然后在创建字节数组时就引发了空指针异常

那为什么加了if判断的return代码就不报空指针错误了呢?

那是因为此时clazz是有值的:class java.lang.Object, 为什么有值呢?因为Object.class虽然从classPath的物理路径读取不到字节码文件,但是通过super.load(name)的父类方法(累加载器应该是AppClassLoader),jvm能够从内存中读取到java.lang.Object.class的字节码文件!

 

然后它会根据下面的if判断直接return clazz,而不会执行后面创建字节数组的代码,从而避免了空指针的发生。

 以上分析完毕,可以看到关于类加载器的知识,里面的水还是很深的,需要仔细分析才能挖掘到深层次的东西

然后附上完整代码:

public class ClassLoaderTest2 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader myClassLoader = new ClassLoader() {
            // 自定义类加载器
            @Override // 重写loadClass方法
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                Class<?> clazz = null;
                // 下面的代码getSystemClassLoader使用了$AppClassLoader来加载,这样就仍然保持了双亲委派机制
//                ClassLoader loader = getSystemClassLoader();
//                try {
//                    clazz = loader.loadClass(name);
//                } catch (ClassNotFoundException e) {
//                    e.printStackTrace();
//                }
//
//                if (clazz != null) {
//                    return clazz;
//                }

                // 读取classPath下编译生成的class字节码文件,然后转化成byte字节流
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if ( inputStream == null) {
                    clazz = super.loadClass(name);
                }

                // 当name入参为Object时,直接return clazz,跳过后面代码的执行,避免产生空指针问题。
              //  if (clazz !=null) return clazz;

                // 下面的代码实际打破了双亲委托机制,使得自定义classLoader生效,从而跟AppClassLoader发生冲突
                // 导致同名Class的实例对象转换异常,因为类加载器不一样了
                try {
                    byte[] bytes = new byte[inputStream.available()];
                    inputStream.read(bytes);
                    // defineClass方法将byte字节流转换成jvm能识别的Class对象
                    clazz = defineClass(name, bytes, 0, bytes.length);

                } catch (IOException exception) {
                    throw new ClassNotFoundException(name);
                }
                return clazz;
            }
        };


        // 通过自定义类加载器myClassLoader来加载类ClassData并完成实例化
        Object clazz = myClassLoader.loadClass("com.mybest.ClassLoaderStudy.ClassData").newInstance();
        System.out.println("clazz self is ======>>>" + clazz.getClass());
        System.out.println("clazz's classLoader is " + clazz.getClass().getClassLoader());
        // 判断通过自定义类加载器myClassLoader生成的clazz是否还是ClassData类型
        // 从这里可以看到如果类加载器不同,即便Class类型相同,JVM仍然认为是两个不同的类
        System.out.println("clazz instanceof ClassData==> " + (clazz instanceof ClassData));
        System.out.println("ClassData's classLoader is " + ClassData.class.getClassLoader());
        // 所以这里会发生ClassCastException,因为类加载器不同:一个是自定义的类加载器myClassLoader,
        // 一个是系统类加载器:AppClassLoader 
        ClassData clazz2 = (ClassData) clazz;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值