Java:基本类型和包装类

前几天同事遇到了一个很奇怪的 NullPointerException 异常,看她郁闷的蛮久都没有解决,也帮忙分析了一下,情况大致如下:

文章配图

看得她一头雾水,在帮其初步分析也没想到是啥问题,因为是批量代码,没有相关文件不好本地测试,让其远程 debug 跟了一下代码,发现还是报空指针异常,最后怀疑是服务器上 jar 包没有更新到,拿下 jar 包反编译后,才恍然大悟(反编译如下图),是自己忽略 Person 类和 Student 类的类型了,一个是 Integer ,一个是 int 基本类型,1.5 之后的 JDK 在编译的时候会有自动拆装箱机制!这里记录一下 Java 中包装类型相关的知识点,做个小总结。

文章配图

基本类型和包装类

Java 中 8 种基本类型对应的包装类型如下表:

基本类型大小包装类型
boolean1 ByteBoolean
char2 ByteCharacter
byte1 ByteByte
short2 ByteShort
int4 ByteInteger
long8 ByteLong
float4 ByteFloat
double8 ByteDouble

自动装箱和拆箱

以 Integer 包装类为例(其他的包装类也是类似),看看其自动拆装箱是如何实现的,写段 代码如下图:

自动装箱和拆箱

由上图可知,自动装箱时,JDK 在编译的时候会调用 Integer.valueOf(int) 这个方法将 int 转为 Integer,而在自动拆箱的时候 Integer 对象会调用 intValue() 方法(Short 类则为 shortValue()),返回 Integer 所持有的 int 成员变量,intValue() 没什么好看的,就是返回了 Integer 对象所持有的 int 成员变量。valueOf(int) 函数倒是值得研究一下,截图如下:

自动装箱和拆箱

可知在 int 值在 -127~ IntegerCache.high(VM 参数 -Djava.lang.Integer.IntegerCache.high=*** 可以设置 high 的值)之间时,将会返回一个Integer 内部类 IntegerCache 里 cache 数组缓存下来的一个 Integer 对象,不是在这个范围外,才会返回一个新的 Integer 对象。IntegerCache 内部类如下图:

自动装箱和拆箱

IntegerCache 这个内部类,有个静态 Integer 类型 cache 数组,静态代码块会给 cache 数组初始化值,缓存符合要求的 Integer 值。这里也就引发了一个初级面试中经常会遇到的问题,如下图:

自动装箱和拆箱

首先 == 在 Java 里,基本类型比较的是值是否相等,而引用型类型则是比较内存地址是否相同, Integer 是个引用类型,默认情况下,-128到127 范围内的 int 值在自动装箱时调用 valueOf(int)方法返回的都是 cache 里缓存好的对象,所以不管是 i ,还是 i2 引用,其实都是指向缓存数组 cache 早已经定义好的 Integer 对象,所以 i == i2 是 true ;而 128 不在 -128到127 的范围内,所以 valueOf(int) 返回的是 new Integer(int),返回一个新的 Integer 对象,所以 i3 == i4 也就是 false 了。

还有另外一种情况,代码截图如下:

自动装箱和拆箱

这里的 i3 引用其实就是和自动装箱无关了,它就是用 Integer 的构造方法生成了一个新的 Integer 对象,和 cache 数组缓存的又是不同的对象,i2 == i3 也就等于 false 了。

注:除 Integer 包装类外,Byte、Short、Long、Character(char)包装类都是有类似于 Integer 的缓存机制的,Float、Double 由于是浮点类型的,JDK 中并未实现这种缓存机制。至于 Boolean 类型的的话又是一个例外,翻看它的实现大致就可以知道啥原理了,其实现如下:

自动装箱和拆箱

自动装箱流程

性能影响

自动拆装箱虽然方便,但在高频场景下会带来性能开销:

  • 装箱:每次装箱都会调用 valueOf(),若超出缓存范围还会创建新对象,增加 GC 压力。
  • 拆箱:每次拆箱都会调用 intValue() 等方法,虽然开销小,但在循环中大量调用仍不可忽视。
1
2
3
4
5
6
7
8
9
10
11
// 反面示例:循环中频繁装箱
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // 每次循环都触发自动装箱和拆箱
}

// 优化:使用基本类型
long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}

在集合操作中也要特别注意:ArrayList<Integer> 的 get() 返回的是包装类型,大量取值操作会产生拆箱开销。

建议:在性能敏感的场景(如大循环、密集计算)中,优先使用基本类型;仅在需要使用泛型或集合时才使用包装类型。

包装类常用方法

其中包装类中拥有共同的方法如下:

  • 带有基本值参数并创建包装类对象的构造函数。如利用Integer包装类创建对象,Integer obj=new Integer(145);
  • 带有字符串参数并创建包装类对象的构造函数.如:new Integer("-45.36");
  • 可生成对象基本值的typeValue方法,如:obj.intValue();
  • 将字符串转换为基本值的parseType方法,如:Integer.parseInt(args[0]);
  • 生成哈希表代码的hashCode方法,如:obj.hashCode();
  • 对同一个类的两个对象进行比较的equals()方法,如:obj1.equals(obj2);
  • 生成字符串表示法的toString()方法,如:obj.toString().