Java自动拆箱空指针异常,救火队员上线

简介: Java自动拆箱空指针异常,救火队员上线

公司搬迁,临时充当装修工,提前两个小时到公司忙着拆卸设备。结果接到客户反映,某部分功能偶尔不能用。于是参与救火,与写这段代码的小伙伴一起排查原因。

最终发现导致业务偶尔不能使用是由Long类型自动拆箱导致空指针异常引起的。下面就带大家分析一下Java中基础类型的包装类在拆箱和装箱过程中都做了什么,为什么会出现空指针异常,以及面试过程中会出现的相关面试题。

问题重现

下面通过一个简单的示例才重现一下异常出现的场景。

public class BoxTest {
    public static void main(String[] args) {
        Map<String,Object> result = httpRequest();
        long userId = (Long) result.get("userId");
    }
    // 模拟一个HTTP请求
    private static Map<String,Object> httpRequest(){
        Map<String,Object> map = new HashMap<>();
        map.put("userId",null);
        return map;
    }
}

基本的场景就是请求一个接口,去接口中取某个值,这个值为Long类型,从Map中取得值之后,进行Long类型的强转。当接口返回的userId为null时,强转这块就抛出空指针异常:

Exception in thread "main" java.lang.NullPointerException
 at com.choupangxia.box.BoxTest.main(BoxTest.java:15)

上面的场景跟下面的代码出现异常效果一样:

public class BoxTest {
    public static long getValue(long value) {
        return value;
    }
    public static void main(String[] args) {
        Long value = null;
        getValue(value);
    }
}

上述代码也是将Long类型进拆箱导致的异常,只不过一个在代码中,一个在参数中。为了分析更简化,我们以第二个为例进行讲解。

原因分析

最初大家可能会疑惑,抛出异常的代码都没有对象的方法调用,怎么会出现空指针呢?

这中间主要涉及到的就是一个自动拆箱操作。是否是拆箱导致的呢?我们来通过字节码看一下。

通过javap -c来查看一下对应的字节码:

public class com.choupangxia.box.BoxTest {
  public com.choupangxia.box.BoxTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static long getValue(long);
    Code:
       0: lload_0
       1: lreturn
  public static void main(java.lang.String[]);
    Code:
       0: aconst_null
       1: astore_1
       2: aload_1
       3: invokevirtual #2                  // Method java/lang/Long.longValue:()J
       6: invokestatic  #3                  // Method getValue:(J)J
       9: pop2
      10: return
}

其中getValue方法调用对应的是main方法中编号3和6的操作。编号3为命令invokevirtual为方法指令。对应的便是value.longValue,value对应的就是声明的Long类型。

也就是说编译器将getValue(value)拆分成了两步,第一步将通过value的longValue方法将其拆箱,然后再将拆箱之后的结果传递给方法。相当于:

long primitive = value.longValue();
test(promitive);

对照最开始的代码,如果value为null的话,那么在调用longValue方法时便会抛出NullPointerException。

所以,本质上来讲,所谓的自动拆箱和装箱只不过是Java提供的语法糖而已。

再次证实

下面用int类型的实例同时证实一下自动拆箱和自动装箱两个操作语法糖底层到底是怎么运行的:

public class IntBoxTest {
    public static void main(String[] args) {
        Integer index = 11;
        int primitive = index;
    }
}

同样查看上面代码的字节码:

public class com.choupangxia.box.IntBoxTest {
  public com.choupangxia.box.IntBoxTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: bipush        11
       2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: aload_1
       7: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
      10: istore_2
      11: return
}

可以看到main方法部分,编号2进行了装箱操作,将原始类型int,装箱成了Integer,调用的方法为Integer.valueOf;而编号7进行了拆箱操作将Integer类型转换成了int类型,调用的方法为Integer.intValue。

自动拆箱装箱的本质

通过上面的分析,我们可以看出所谓的拆箱(unboxing)和装箱(boxing)操作只不过是一个语法糖的功能。编译器在编译操作时,本质上还是会调用对应包装类的不同方法来进行处理。

装箱时通常会调用包装类的valueOf方法,而拆箱时通常会调用包装类的xxxValue()方法,其中xxx为类似boolean/long/int等。

而自动拆箱和装箱的操作主要发生在赋值、比较、算数运算、方法调用等常见。此时,我们就需要主要空指针的问题。

面试题

看一个面试题:请问下面foo1和foo2被调用时如何执行?并简单分析一下。

public void foo1() {
    if ((Integer) null == 1) {
    }
}
public void foo2() {
    if ((Integer) null > 1) {
        System.out.println("abc");
    }
}

很明显在调用两个方法时都会抛出空指针异常。关于抛空指针异常的原因及分析过程,上文已经讲过,大家可以尝试分析一下字节码。

再看一个面试题:下面的语句能正常执行吗?

Integer value1 = (Integer) null;
Double value2 = (Double) null;
Boolean value3 = (Boolean) null;

答案:可以正常执行。在Java中null是一个特殊的值,可以赋值给任何引用类型,也可以转化为任何引用类型。

小结

任何一个小的问题,小的异常,如果深入追踪一下,不仅能够更清楚的明白底层原理,而且还可以在实践的过程中更有把握,更少犯错。

目录
打赏
0
0
0
0
33
分享
相关文章
【YashanDB知识库】yasdb jdbc驱动集成druid连接池,业务(java)日志中有token IDENTIFIER start异常
客户Java日志中出现异常,影响Druid的merge SQL功能(将SQL字面量替换为绑定变量以统计性能),但不影响正常业务流程。原因是Druid在merge SQL时传入null作为dbType,导致无法解析递归查询中的`start`关键字。
【YashanDB知识库】yasdb jdbc驱动集成BeetISQL中间件,业务(java)报autoAssignKey failure异常
在BeetISQL 2.13.8版本中,客户使用batch insert向yashandb表插入数据并尝试获取自动生成的sequence id时,出现类型转换异常。原因是beetlsql在prepareStatement时未指定返回列,导致yashan JDBC驱动返回rowid(字符串),与Java Bean中的数字类型tid不匹配。此问题影响业务流程,使无法正确获取sequence id。解决方法包括:1) 在batchInsert时不返回自动生成的sequence id;2) 升级至BeetISQL 3,其已修正该问题。
【YashanDB知识库】yasdb jdbc驱动集成BeetISQL中间件,业务(java)报autoAssignKey failure异常
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
109 14
【AI程序员】通义灵码 AI 程序员全面上线JAVA使用体验
通过 AI 程序编写一个JAVA后台项目登陆页面
400 42
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
147 5
如何避免 Java 中的 TimeoutException 异常
在Java中,`TimeoutException`通常发生在执行操作超过预设时间时。要避免此异常,可以优化代码逻辑,减少不必要的等待;合理设置超时时间,确保其足够完成正常操作;使用异步处理或线程池管理任务,提高程序响应性。
244 13
|
4月前
|
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
130 1
|
4月前
|
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
68 1
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
345 13
|
5月前
|
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
65 0

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等