最近做CodeReview,发现了一位已经离职同事写的代码,大致如下,令人费解。(其中map是Map<String, Object>)
1 try { 2 3 int count = (Integer) map.get("count"); 4 5 } catch (NullPointerException e) { 6 // do something. 7 }
先不说这样写好不好,引起我兴趣的是,map这个变量,已经在上文判断是否为空指针了,map.get("count")也不会抛出空指针异常。为什么这里还要判断?
经过一番搜索学习,想要理解上面的语句,那么你需要了解以下知识点。
1. null 与 对象的转换
null既不是对象也不是一种类型,它仅是一种特殊的值,你可以将其赋予任何引用类型,你也可以将null转换成任何类型。例如下面的代码,是可以运行的。
关于null的知识点,可以参考 Java中有关Null的9件事
Integer a = (Integer) null; Double b = (Double) null; Boolean c = (Boolean) null;
2.装箱和拆箱
Java为每种基本数据类型都提供了对应的包装器类型,自jdk 1.5之后提供了自动装箱(autoboxing)和拆箱(unboxing).
上面这个就没什么好讲的了。
所谓装箱和拆箱,就是指基本类型和包装器类型的互相转换。
装箱:基本类型->包装器类型;拆箱:包装器类型->基本类型。
1 Integer integer = 1; //装箱 2 int i = integer; //拆箱
3. 装箱和拆箱的实现
对上面的代码进行编译,查看字节码,如图所示:
装箱的时候,使用静态的valueOf()方法;拆箱的时候,使用非静态的intValue()方法。
经过测试,上述的所有类型,装箱都会调用静态的valueOf()方法,而拆箱使用非静态xxxValue()方法。
4.一个小陷阱
来看下面的代码,会是输出什么呢?
1 Integer a = 1; 2 Integer b = 1; 3 Integer c = 200; 4 Integer d = 200; 5 System.out.println(a == b); 6 System.out.println(c == d);
Integer是引用类型,引用类型是要看引用的地址的,很明显这四个都不是同个对象,都打印false.
然而……
这里就涉及到一个缓存的问题。查看Integer的valueOf()代码,可以看到,在某个范围内,会从缓存取值,这样取出来的,就是同一个对象了。
IntegerCache的范围一般是[-128,127]。(是否可以修改,待确认)
经检查
1)Integer, Byte, Long, Short都是从[-128, 127];Character是[0,127];Boolean是FALSE or TRUE.
2)Double和Float没有缓存的概念
3)通过直接创建的对象,不会从缓存中获取。
1 Integer a = new Integer(1); 2 Integer b = new Integer(1); 3 System.out.println(a == b);
打印false;
5. 包装器的符号运算(以Integer为例)
1)==,两边都是Integer,比较内存地址;存在一边是基本类型,比较数值大小
2)+,转换成基本类型后相加
这个就不证明了。 有兴趣按照上面的方式检查(javap -c xxxx)
6. 问题分析
回到最开始的问题,为什么会抛出空指针异常?
1 int count = (Integer) map.get("count");
1)map获得的值,有可能为null.
2)null可以转换成所有类型,于是得到一个声明为Integer类型的变量,该变量实际上指向为空
3)Integer转成int,发生拆箱,调用非静态的intValue()方法,而变量实际上为空,那么就会抛出空指针异常