阿里巴巴JAVA开发手册中写道:
关于基本数据类型与包装数据类型的使用标准如下:
- 【强制】所有的 POJO 类属性必须使用包装数据类型。
- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
- 【推荐】所有的局部变量使用基本数据类型。
说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
- 正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
- 反例:比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
测试用例
pojo:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Test {
private Integer id;
private String name;
private Integer age;
private int num;
}
service:
public void selTest(){
Test t = (Test) testMapper.selectById(1);
System.out.println(t.getAge());//Integer
System.out.println(t.getNum());//int
}
数据库:
其余简单代码不再展示。
请求service
测试结果:
这里由于num字段是int类型,所以即使从数据库中查出的数据为Null,但是由于自动拆箱,int类型数据有默认值,所以数据库中的null映射到int类型的变量num上,就变成了0,这是不正确的。
所以pojo中的字段应该都为包装类型。
阿里建议所有 POJO 类属性使用包装类,但这些坑你有注意到吗?
包装类在Java 5
中和泛型一起引入,引入包装类的原因有两点:
- 解决无法创建基本类型泛型集合的问题
- 加入对基本类型为
null
这个语义的支持
并提供boxing
和unboxing
的语法糖,让编译器支持基本类型和包装类的自动转化,减少开发者的工作量。但是经常有同学因为误用包装类导致惨烈的线上问题,在使用包装类的时候务必需要注意以下四点:
- 与基础类截然不同的
==
和equals
语义- 糟糕的性能
- 不易察觉的
NPE
问题- 令人疑惑的
API
设计
1. 相等还是不相等?这是个问题
比如以下代码片段
class Biziclop {
public static void main(String\[\] args) {
System.out.println(new Integer(5) == new Integer(5)); // false
System.out.println(new Integer(500) == new Integer(500)); // false
System.out.println(Integer.valueOf(5) == Integer.valueOf(5)); // true
System.out.println(Integer.valueOf(500) == Integer.valueOf(500)); // false
}
}
第一个和第二个语句返回false
是比较容易理解的,因为对于Java
中的对象调用=
其实是在比较对象在堆上的地址,由于两个对象都是新建的,所以地址肯定不等,返回false
。
比较令人疑惑的是第三个语句,按照我们前面的分析,应该也返回false
才对,但其实Integer.valueOf(5) == Integer.valueOf(5)
比较的结果是true
,这是因为JVM
缓存了-128-127的整数,所以当数值在这个区间的时候,返回的对象都是同一个的。第四个语句因为数值已经不在-128-127的区间范围,所以返回了false
。
上面的这几个例子都是比较经典的例子,大家比较熟悉,一般也比较难掉坑里,但是下面的几个例子就比较有迷惑性了
class Biziclop {
public static void main(String\[\] args) {
List<Long> list = new ArrayList<>();
list.add(Long.valueOf(200));
System.out.println(list.contains(200)); // false
Long temp = 0L;
System.out.println(temp.equals(0)); // false
System.out.orintln(0==0L); // true
}
}
原因在于
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
包装类重写了equals
方法,导致包装类即便是调用equals
方法比较大小,也会和基本类型出现不一致的结果。与基础类截然不同的==
和equals
语义经常会导致代码走到非期望的分支,再配上JVM
对数字独特的缓存策略,极容易出现测试环境和正式环境不一样的运行结果。
2. 糟糕的性能
《Effective Java
》中有如下的例子:
public static void main(String\[\] args) {
Long sum = 0L; // uses Long, not long
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
这段代码的耗时比使用基本类型long
的版本慢6倍(声明变量sum
类型为 Long
的耗时是43秒, 如果声明变量sum
为基本类型long
,则耗时6.8秒)。导致这样的原因是包装类要经过在堆上开辟内存空间,初始化,内存寻址以及数据载入寄存器的过程,性能差也就不足为奇了。因此Joshua Bloch
对开发者的建议是:Avoid creating unnecessary objects.
在经典的JMH workbench
上跑的包装类和基础类性能对比如下图所示:
图片
可以看到与基础类相比,包装类普遍要慢不少。
图片地址:https://www.baeldung.com/java-primitives-vs-objects
3. 不易察觉的NPE
不同于基本类型,作为对象的包装类是可能为null
的,这就意味着一个指向null
的包装类unboxing
的时候会抛出NPE
异常,比如以下代码:
Integer in = null;
...
...
int i = in; // NPE at runtime
这段代码也是比较明显的,但是如果包装类遇到三元运算符,则会出现更复杂的NPE
class Biziclop {
public static void main(String\[\] args) {
Boolean b = true ? returnsNull() : false; // NPE on this line.
System.out.println(b);
}
public static Boolean returnsNull() {
return null;
}
}
这跟Java中三元运算符类型的判定有关系,有一条判定规则是,
如果三元运算符的第二个或者第三个参数是基本类型T,并且另一个是相应的包装类型的话,那么三元运算符的返回类型就是这个基本类型T
所以在上面的代码中,returnsNull的返回值还要进行一次unboxing
,因此抛出了NPE
.
4. 令人疑惑的API
在Long
这个类中,有一个api
是getLong
,其声明如下:
/**
* Determines the {@code long} value of the system property
* with the specified name.
*/
public static Long getLong(String nm) {
return getLong(nm, null);
}
这个api
的作用是获取JVM
中的属性值的,并且转换为Long
类型,比如:
class Biziclop {
public static void main(String\[\] args) {
System.setProperty("22", "22");
System.setProperty("23", "hello world!");
System.out.println(Long.getLong("22")); // 22
System.out.println(Long.getLong("23")); // null
System.out.println(Long.getLong("24")); // null
}
}
这个api
的设计妥妥是一个反例,经常有同学误用,把它当成Long.valueOf
或者是Long.parseLong
,结果返回不符合期望的值。
5. 最佳实践
《阿里巴巴Java
编程手册》对包装类的使用有以下三条建议:
- 所有
POJO
类属性使用包装类 - RPC方法的返回值和参数使用包装类
- 所有的局部变量使用基本数据类型
说明:
POJO
类属性没有初值是提醒使用在需要使用时,必须自己显式的进行赋值,任何NPE
问题,或者入库检查,都有使用者来保证。
正例:数据库的查询结果可能是null
,因为自动拆箱,用基本数据类型接受有NPE
的风险
反例:某业务的交易报表上显示成交额涨跌情况,即x%,x为基本数据类型,调用的HSF
服务,调用不成功时,返回的是默认值,页面展示0%,这是不合理的,应该展示成中划线-,所以包装类的null
值,能够表示额外的信息,如:远程调用失败,异常退出。