Java的自动装箱与拆箱原理探讨
什么是自动装箱拆箱
自动装箱拆箱 Autoboxing and unboxing
装箱就是自动将基本数据类型转换为包装器类型;
拆箱就是自动将包装器类型转换为基本数据类型。
装箱拆箱过程是自动执行的。
//自动装箱
Integer total = 99;
//自动拆箱
int totalprim = total;
需要装箱拆箱的类型有哪些?
通过源代码理解执行过程
执行代码
package com.yueny.study.jdk.autoboxing;
/**
* @author fengyang <deep_blue_yang@126.com>
* @date 2020/9/30 10:54 上午
* @title: IntegerAutobox
* @projectName algorithm
* @description:
*/
public class IntegerAutobox {
public static void main(String[] args) {
//自动装箱
Integer totalInteger = 99;
//自定拆箱
int totalIntegerprim = totalInteger;
}
}
编译得到class文件后,反编译class文件
javap -c IntegerAutobox.class
反编译class内容如下:
Compiled from "IntegerAutobox.java"
public class com.yueny.study.jdk.autoboxing.IntegerAutobox {
public com.yueny.study.jdk.autoboxing.IntegerAutobox();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 99
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: aload_1
7: invokevirtual #3 // Method java/lang/Integer.intValue:()I
10: istore_2
11: return
}
代码 | 系统执行 | |
---|---|---|
Integer totalInteger = 99; | Integer totalInteger = Integer.valueOf(99); | 自动装箱 |
int totalIntegerprim = totalInteger; | int totalIntegerprim = totalInteger.intValue(); | 自动拆箱 |
Integer 的装拆箱
装箱
1、首先看Integer.valueOf函数
public static Integer valueOf(String s, int radix) throws NumberFormatException {
return Integer.valueOf(parseInt(s,radix));
}
public static Integer valueOf(int i) {
if (i >=-128 && i <= 127)
return IntegerCache.cache[i + (128)];
return new Integer(i);
}
它会首先判断 i
的大小:如果 i
大于等于-128并且小于等于127,则执行 IntegerCache.cache[i + 128]。否则,就创建一个Integer对象。
2、再看Integer的构造函数
/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;
public Integer(int value) {
this.value = value;
}
public Integer(String s) throws NumberFormatException {
this.value = parseInt(s, 10);
}
构造函数里面定义了一个value变量,创建一个Integer对象,就会给这个变量初始化。第二个传入的是一个String变量,它会先把它转换成一个int值,然后进行初始化。
3、再看 IntegerCache.cache[i + 128]
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 =
sun.misc.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() {}
}
在jdk1.8中,IntegerCache 为 Integer 的内部私有类。
IntegerCache中有3个final类型的变量:
low:-128(一个字节能够表示的最小值);
high:127(一个字节能够表示的最大值),JVM中设置的属性值(通过-XX:AutoBoxCacheMax=设置)二者取大值,再和Integer.MAX_VALUE取小值;
cache:在静态块中初始化为由区间[low, high]上的所有整数组成的升序数组。
经过一系列的运算,static final Integer cache[] 相当于 static final Integer[] SMALL_VALUES = new Integer[256];
它是一个静态的Integer数组对象,也就是说最终valueOf返回的都是一个Integer对象。
因此,可以初步得到一个简单的结论:因为装箱的过程会创建对应的对象,因此会额外的消耗内存,所以装箱的过程会增加内存的消耗,进而对性能会存在影响(可能影响比较微量)。
拆箱
4、intValue函数
public int intValue() {
return value;
}
拆箱很简单,直接返回value值。
Integer 装拆箱总结
- 在 i >= 128 || i < -128时, 会创建不同的新对象, 分配不同的地址和内存空间。
- 在 -128 <= i < 128 会根据i的值返回已经创建好的指定的对象(IntegerCache 缓存)。
举例
code
private static void testInteger(){
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
Assert.isTrue(i1 == i2, "如果此处报错说明返回了 false。");
System.out.println(i1 == i2); //true
Assert.isTrue(i3!=i4, "如果此处报错说明返回了 false");
System.out.println(i3==i4); //false
}
输出结果:
true
false
说明:
- i1和i2会进行自动装箱,执行了valueOf函数,它们的值在(-128,128]这个范围内,它们会拿IntegerCache里面的同一个对象,引用到了同一个Integer对象,所以它们肯定是相等的。
- i3和i4也会进行自动装箱,执行了valueOf函数,它们的值大于128,所以会执行new Integer(200),也就是说它们会分别创建两个不同的对象,所以它们肯定不等。
基本类型装拆箱汇总
其实对于Java的基本类型装拆箱而言,都是大同小异的。
装箱调用的都是 valueOf
方法;拆箱调用的均是 xxxValue
方法。
我们可以总结归纳为如下几种情形:
自动装拆箱调用方法
基本类型 | 常规类型 | 自动装箱调用方法 | 自动拆箱调用方法 |
---|---|---|---|
Integer | int | Integer.valueOf | .intValue() |
Short | short | Short.valueOf | .shortValue() |
Byte | byte | Byte.valueOf | .byteValue() |
Character | char | Character.valueOf | .charValue() |
Long | long | Long.valueOf | .longValue() |
Double | double | Double.valueOf | .doubleValue() |
Float | float | Float.valueOf | .floatValue() |
各类型的相同值范围
类型 | 相同对象范围(缓存区间) | 不同对象范围(非缓存区间) | 缓存区间是否可变 | 代码段 |
---|---|---|---|---|
Integer | [-128, 127] | i >= 128 或者 i < -128 | 上限可设置 | -- |
Short | [-128, 127] | s >= 128 或者 s < -128 | 不可变 | -- |
Character | [0, 127] | c >= 128 | 不可变 | if (c <= 127) return CharacterCache.cache[(int)c]; |
Long | [-128, 127] | l >= 128 或者 l < -128 | 不可变 | -- |
Byte | [-128, 127] | b >= 128 或者 b < -128 | 不可变 | -- |
Double | -- | ALL | -- | -- |
Float | -- | ALL | -- | -- |
Boolean | {true, false} | -- | 不可变 | -- |
分门别类
Integer派别:Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的:存在缓存区间的设置。
Double派别:Double、Float的valueOf方法的实现是类似的:不存在缓存区间,每次都返回不同的对象。
Boolean: Boolean没有缓存的概念,所有的返回值全部为固定值 TRUE, FALSE。
Boolean 装拆箱代码片
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code true}.
*/
public static final Boolean TRUE = new Boolean(true);
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
public boolean booleanValue() {
return value;
}
回首望月
通过上面的分析,我们可以得到几点注意事项。
- 当用“Boolean bool = true”的自动装箱方式定义变量的时候,这两个变量其实指向的都是Boolean类型的缓存池中的那个值为 TRUE 的对象,所以用它们当做同步锁,其实是用的同一把锁,必然会出现竞争等待。
- 当我们使用自动装箱机制初始化变量的时候,就相当于告诉JAVA这里
需要一个对象
,而不是告诉JAVA这里需要一个新的对象
。当我们需要一个新的对象的时候,那就自己new出来。 - 因为基本类型对应的包装器都是不可变类,它们的缓存区间一旦初始化,里面的值就无法再改变,所以
在JVM运行过程中,所有的基本类型包装器的缓存池都是不变的
。 - 当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,对
基础数据类型
进行运算(也就是值的运算)。- 当进行==、+、-、*、/运算符的两个操作数
都是包装器类型
的引用,则是比较指向的是否是同一个对象
;而如果其中有一个是基本类型时则比较的是数值(即会触发自动拆箱的过程)。
- 当进行==、+、-、*、/运算符的两个操作数
Integer num1 = 400;
int num2 = 400;
System.out.println(num1 == num2); //true
Integer num1 = 100;
int num2 = 100;
Long num3 = 200l;
System.out.println(num1 + num2); //200
System.out.println(num3 == (num1 + num2)); //true
System.out.println(num3.equals(num1 + num2)); //false
- .equals 运算为 false 原因:必须满足两个条件才为true:
- 1、类型相同
- 2、内容相同
上面返回false的原因就是类型不同。
@Override
public boolean equals(Object o) {
return (o instanceof Long) && (((Long) o).value == value);
}