1.抛出问题(一次NullPointerException带来的思考)
遇到这样一个场景,代码类似如下:
一个Controller,使用GET方法获取参数id对应的数据
@RequestMapping(value = "/home/show", method = {RequestMethod.GET})
public Result homeShow(@RequestParam(name = "id") Long id) {
List<HomeShow> res = homeShowService.show(id);
if (res != null) {
return Result.success(res);
} else {
return Result.fail(ResultCode.ERROR,"");
}
}
一个Service,homeShowService,对应的接口是这样写的,实现方法省略
public interface CircuitWarnService {
List<HomeShow> show(long id);
}
代码这样写完上线,然后时不时就会报NullPointerException。
抛出问题:Controller接收参数id的类型是Long,而Service接收参数id的类型是long,
如果前端传进来的是id为null,Controller不会报错,调用Service的时候就会报错,
因为从类型 Long 到基本数据类型 long 有个自动拆箱过程,那这个过程中会不会抛出 NullPointerException 呢?
2.原因分析
模拟同样的一个场景
public class AutoUnbox {
public static long test(long value) {
return value;
}
public static void main(String[] args) {
Long value = null;
test(value);
}
}
编译能通过,运行时就会抛NullPointerException;使用 javap -c AutoUnbox 对上述代码class文件进行反汇编,可以看到
public class first.demo.AutoUnbox {
public first.demo.AutoUnbox();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static long test(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 test:(J)J
9: pop2
10: return
}
test(value)对应着反汇编后的
3: invokevirtual #2 // Method java/lang/Long.longValue:()J
6: invokestatic #3 // Method test:(J)J
可以看出确实有个自动拆箱过程「Method java/lang/Long.longValue:()J」,而我们传入的 value 为 null,value.longValue() 会抛出 NullPointerException。
所以对于开始的问题,解决方法在方法调用链路上都使用同样类型,要么使用基础类型long,要么使用引用类型Long
3.自动拆箱与装箱介绍
自动装箱与拆箱是 Java 1.5 引入的新特性,是一种语法糖。
在此之前,我们要创建一个值为 10 的 Integer 对象,只能写作:
Integer value = new Integer(10);
而现在,我们可以更方便地写为:
Integer value = 10;
定义
自动装箱,是指从基本数据类型值到其对应的包装类对象的自动转换。比如 Integer value = 10;,是通过调用 Integer.valueOf 方法实现转换的
自动拆箱,是指从包装类对象到其对应的基本数据类型值的自动转换。比如 int primitive = value;,是通过调用 Integer.intValue 方法实现转换的
基本数据类型 | 包装类型 | 装箱方法 | 拆箱方法 |
---|---|---|---|
boolean | Boolean | Boolean.valueOf(boolean) | Boolean.booleanValue() |
byte | Byte | Byte.valueOf(byte) | Byte.byteValue() |
char | Character | Character.valueOf(char) | Character.charValue() |
short | Short | Short.valueOf(short) | Short.shortValue() |
int | Integer | Integer.valueOf(int) | Integer.intValue() |
long | Long | Long.valueOf(long) | Long.longValue() |
float | Float | Float.valueOf(float) | Float.floatValue() |
double | Double | Double.valueOf(double) | Double.doubleValue() |
发生时机
自动装箱与拆箱主要发生在以下四种时机:
-
1.赋值时
Integer value = 10; // 自动装箱(赋值时)
int primitive = value; // 自动拆箱(方法调用时)
-
2.比较时
Integer value = 1000;
if (value <= 1000) { // 自动拆箱(比较时)
}
-
3.算术运算时
Integer v1 = new Integer(10);
Integer v2 = new Integer(20);
int v3 = 30;
int sum = v1 + v2; // 自动拆箱(算术运算时)
sum = v1 + 30; // 自动拆箱(算术运算时)
-
4.方法调用时
List<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱(方法调用时)
int i = list.get(0); // 自动拆箱(赋值时)
4.相关知识点
比较
除 == 以外,包装类对象与基本数据类型值的比较,包装类对象与包装类对象之间的比较,都是自动拆箱后对基本数据类型值进行比较,所以,要注意这些类型间进行比较时自动拆箱可能引发的 NullPointerException。
== 比较特殊,因为可以用于判断左右是否为同一对象,所以两个包装类对象之间 ==,会用于判断是否为同一对象,而不会进行自动拆箱操作;包装类对象与基本数据类型值之间 ==,会自动拆箱。
示例代码:
Integer v1 = new Integer(10);
Integer v2 = new Integer(20);
if (v1 < v2) { } // 自动拆箱
if (v1 == v2) { } // 不拆箱
if (v1 == 10) { } // 自动拆箱
缓存
Java 为整型值包装类 Byte、Character、Short、Integer、Long 设置了缓存,用于存储一定范围内的值,详细如下:
类型 | 缓存值范围 |
---|---|
Byte | -128 ~ 127 |
Character | 0 ~ 127 |
Short | -128 ~ 127 |
Integer | -128 ~ 127(可配置) |
Long | -128 ~ 127 |
在一些情况下,比如自动装箱时,如果值在缓存值范围内,将不创建新对象,直接从缓存里取出对象返回,比如:
Integer v1 = 10;
Integer v2 = 10;
Integer v3 = new Integer(10);
Integer v4 = 128;
Integer v5 = 128;
Integer v6 = Integer.valueOf(10);
System.out.println(v1 == v2); // true,都是缓存中对象
System.out.println(v1 == v3); // false,v1是缓存中对象,v3强制new对象
System.out.println(v4 == v5); // false,都不是缓存中对象,都重新new
System.out.println(v1 == v6); // true,都是缓存中对象
缓存实现机制:
这里使用了设计模式享元模式:重用现有的同类对象,如果未找到匹配的对象,则创建新对象
以 Short 类实现源码为例:(jdk1.8中源码位置在/java/lang/Short.java)
public final class Short extends Number implements Comparable<Short> {
private static class ShortCache {
private ShortCache(){}
static final Short cache[] = new Short[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Short((short)(i - 128));
}
}
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
// 范围内的直接返回缓存中的对象,避免频繁创建对象
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
}
在第一次调用到 Short.valueOf(short) 方法时,将创建 -128 ~ 127 对应的 256 个对象缓存到堆内存里。
这种设计,在频繁用到这个范围内的值的时候效率较高,可以避免重复创建和回收对象,否则有可能闲置较多对象在内存中。
使用不当的情况
如果使用不当,也有可能带来负面影响,如性能的损耗
Integer sum = 0;
for (int i = 1000; i < 5000; i++) {
// 1. 先对 sum 进行自动拆箱
// 2. 加法
// 3. 自动装箱赋值给 sum,无法命中缓存,会 new Integer(int)
sum = sum + i;
}
在循环过程中会分别调用 4000 次 Integer.intValue() 和 Integer.valueOf(int),并 new 4000 个 Integer 对象,
而这些操作将 sum 的类型改为 int 即可避免,节约运行时间和空间,提升性能。
主要参考: