【Java基础】Integer的自动拆箱和享元模式,这次我学到了
前言
前段时间线上报了一个空指针异常,后来多方排查发现是Integer自动拆箱的问题。所以少侠又把Integer源码及一些比较底层的知识学习了一遍,这里记录一下,也分享给大家。
自动拆箱
先看下出问题的代码,业务逻辑我给省去了。要做的事情很简单,就是从表里查询某用户购买商品一共支付的价格。注意,用户可能不存在,即通过SQL的SUM函数可能为null。
Mapper层代码:
/**
* @author Carson
* @date 20-9-16 下午7:27
*/
@Mapper
public interface UserOrderMapper {
String TABLE_NAME = "cps_user_order_detail d";
@Select("SELECT SUM(price) FROM " + TABLE_NAME + " WHERE d.user_id=#{userId}")
Integer calPrice(@Param("userId") String userId);
}
业务层代码:
public Integer calPrice(String userId) {
int price = userOrderMapper.calPrice(userId);
Double rate = 0.5;
return rate * price;
}
少侠先不说问题,各位在看到这段代码会想到可能的隐患吗?
手动分隔符 +++++++++++++++++++++++++++++++++++++++++
好了,不卖关子了,上述代码极可能出现空指针异常。
事实上线上日志报错就是在业务层代码的int price = userOrderMapper.calPrice(userId);
这一行。报了NullPointerException,当时我的第一感觉是难道userOrderMapper
没有注入进来?但是业务层该Mapper的其它方法都正常运行,所以立即排查这个可能。但是SQL里面会出错么,即使参数为null,SQL也不会报NPE的。
苦思无果,于是把SQL拿出来手动执行了一遍,发现返回值是null,于是立即想到肯定是这个null的返回值调用了什么方法,但是再一看业务层代码,没有调用直接返回了啊。但是再一看返回值,呦吼,是int,想到可能是自动拆箱(即Integer向int类型转换)的问题。具体是啥问题呢。
看官莫急,且来看一下这段代码:
public class Main {
public static void main(String[] args) {
Integer input = null;
int result = input;
System.out.println(result);
}
}
运行完之后进入classes文件夹下执行javap -v Main.class
,查看反编译执行结果,这个就很明了了,如下所示,实际上Integer类型的参数用int接收是通过隐式的调用Integer.intValue()
实现的。这也解释了上面那段业务代码会出现异常。
享元模式
还是先来看一段代码。相信参加过在线笔试的小伙伴很多人都做过下面这道题:
public class Main {
public static void main(String[] args) {
Integer a1 = new Integer(66);
Integer b1 = new Integer(66);
System.out.println(a1 == b1);
Integer a2 = 128;
Integer b2 = 128;
System.out.println(a2 == b2);
Integer a3 = 127;
Integer b3 = 127;
System.out.println(a3 == b3);
}
}
a1==b1
等于false相信大家都没疑问。a2==b2
你会想到,声明了两个变量,在内存中开辟了两个地址空间,答案也是false,也没问题。
然后再一看,什么鬼?刚刚是128,a3和b3变成127,同样的问题问我两遍?这出题人水平不行啊!小伙子,这样想说明你还年轻啊。是的,a3==b3
答案是true。这是为什么呢?
事实上这是Java源码级的优化,Integer为了提高内存的利用率,内部设置了一个缓冲区,附上源码:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
这是什么意思呢,就是在[-128,127]的区间内的整数,在内存中始终只会有一份(当然你通过new创建新对象的方式不算)。并且这个就是大名鼎鼎的享元模式。下次再有面试官问你JDK应用了哪些设计模式,把Integer这里的实现甩给他!
小结
一个小小的Integer就含有很有讲究,虽然读代码、找问题的过程比较苦涩,但是掌握之后还是会觉得充实。做开发最需要的就是技术提升,而阅读源码就是一个绝佳的方式!
我是少侠露飞,爱技术,爱分享。