Java的自动装箱与拆箱(Autoboxing and unboxing)
一、什么是自动装箱拆箱
很简单,下面两句代码就可以看到装箱和拆箱过程
//自动装箱 Integer total = 99; //自动拆箱 int totalprim = total;
简单一点说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
下面我们来看看需要装箱拆箱的类型有哪些:
这个过程是自动执行的,那么我们需要看看它的执行过程:
package com.dxz.base; public class Main { public static void main(String[] args) { // 自动装箱 Integer total = 99; // 自定拆箱 int totalprim = total; } }
反编译class文件之后得到如下内容:
D:\gitspace\Test\bin\com\dxz\base>javap -c Main.class Compiled from "Main.java" public class com.dxz.base.Main { public com.dxz.base.Main(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 99 2: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: aload_1 7: invokevirtual #22 // Method java/lang/Integer.intValue:()I 10: istore_2 11: return } D:\gitspace\Test\bin\com\dxz\base>
Integer total = 99;
执行上面那句代码的时候,系统为我们执行了:
Integer total = Integer.valueOf(99);
int totalprim = total;
执行上面那句代码的时候,系统为我们执行了:
int totalprim = total.intValue();
二、Integer源码分析
我们现在就以Integer为例,来分析一下它的源码:
2.1、首先来看看Integer.valueOf函数
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
Integer内部的做了一个缓存,cache数组,缓存了-128到127共256Integer对象,每次创建Integer对象的时候,如果值在指定范围内【-128,127】,就会从缓存中取,否则就会重新new一个。
IntegerCache是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 = 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() {} }
从上面的static方法中,可知:
- low的值是-128;
- high的默认值是127,也可以通过java.lang.Integer.IntegerCache.high的参数指定(通过 JVM 的启动参数 -XX:AutoBoxCacheMax=size 修改)
- 遍历给指定范围的数据赋值
2.2、Integer的构造函数
首先我们来看看Integer的构造函数:
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值,然后进行初始化。
所以我们这里可以总结一点:装箱的过程会创建对应的对象,这个会消耗内存,所以装箱的过程会增加内存的消耗,影响性能。
三、相关问题
上面我们看到在Integer的构造函数中,它分两种情况:
1、i >= 128 || i < -128 =====> new Integer(i)
2、i < 128 && i >= -128 =====> IntegerCache.cache[i + (-IntegerCache.low)]
SMALL_VALUES本来已经被创建好,也就是说在i >= 128 || i < -128是会创建不同的对象,在i < 128 && i >= -128会根据i的值返回已经创建好的指定的对象。
说这些可能还不是很明白,下面我们来举个例子吧:
package com.dxz.base; public class Main { public static void main(String[] args) { Integer i1 = 100; Integer i2 = 100; Integer i3 = 200; Integer i4 = 200; System.out.println(i1 == i2); // true System.out.println(i3 == i4); // false } }
代码的后面,我们可以看到它们的执行结果是不一样的,为什么,在看看我们上面的说明。
1、i1和i2会进行自动装箱,执行了valueOf函数,它们的值在(-128,128]这个范围内,它们会拿到缓存数组里面的同一个对象IntegerCache[228],它们引用到了同一个Integer对象,所以它们肯定是相等的。
2、i3和i4也会进行自动装箱,执行了valueOf函数,它们的值大于128,所以会执行new Integer(200),也就是说它们会分别创建两个不同的对象,所以它们肯定不等。
下面我们来看看另外一个例子:
package com.dxz.base; public class Main { public static void main(String[] args) { Double i1 = 100.0; Double i2 = 100.0; Double i3 = 200.0; Double i4 = 200.0; System.out.println(i1 == i2); // false System.out.println(i3 == i4); // false } }
看看上面的执行结果,跟Integer不一样,这样也不必奇怪,因为它们的valueOf实现不一样,结果肯定不一样,那为什么它们不统一一下呢?
这个很好理解,因为对于Integer,在(-128,128]之间只有固定的256个值,所以为了避免多次创建对象,我们事先就创建好一个大小为256的Integer数组IntegerCache,所以如果值在这个范围内,就可以直接返回我们事先创建好的对象就可以了。
但是对于Double类型来说,我们就不能这样做,因为它在这个范围内个数是无限的。
总结一句就是:在某个范围内的整型数值的个数是有限的,而浮点数却不是。
所以在Double里面的做法很直接,就是直接创建一个对象,所以每次创建的对象都不一样。
public static Double valueOf(double d) { return new Double(d); }
下面我们进行一个归类:
Integer派别:Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。
Double派别:Double、Float的valueOf方法的实现是类似的。每次都返回不同的对象。
下面对Integer派别进行一个总结,如下图:
下面我们来看看另外一种情况:
1 public class Main { 2 public static void main(String[] args) { 3 4 Boolean i1 = false; 5 Boolean i2 = false; 6 Boolean i3 = true; 7 Boolean i4 = true; 8 9 System.out.println(i1==i2);//true 10 System.out.println(i3==i4);//true 11 } 12 }
可以看到返回的都是true,也就是它们执行valueOf返回的都是相同的对象。
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
可以看到它并没有创建对象,因为在内部已经提前创建好两个对象,因为它只有两种情况,这样也是为了避免重复创建太多的对象。
public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false);
上面把几种情况都介绍到了,下面来进一步讨论其他情况。
1 Integer num1 = 400; 2 int num2 = 400; 3 System.out.println(num1 == num2); //true
说明num1 == num2进行了拆箱操作
1 Integer num1 = 100; 2 int num2 = 100; 3 System.out.println(num1.equals(num2)); //true
我们先来看看equals源码:
1 @Override 2 public boolean equals(Object o) { 3 return (o instanceof Integer) && (((Integer) o).value == value); 4 }
我们指定equal比较的是内容本身,并且我们也可以看到equal的参数是一个Object对象,我们传入的是一个int类型,所以首先会进行装箱,然后比较,之所以返回true,是由于它比较的是对象里面的value值。
1 Integer num1 = 100; 2 int num2 = 100; 3 Long num3 = 200l; 4 System.out.println(num1 + num2); //200 5 System.out.println(num3 == (num1 + num2)); //true 6 System.out.println(num3.equals(num1 + num2)); //false
1、当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,对基础数据类型进行运算。
2、对于num3.equals(num1 + num2)为false的原因很简单,我们还是根据代码实现来说明:
1 @Override 2 public boolean equals(Object o) { 3 return (o instanceof Long) && (((Long) o).value == value); 4 }
它必须满足两个条件才为true:
1、类型相同
2、内容相同
上面返回false的原因就是类型不同。
1 Integer num1 = 100; 2 Ingeger num2 = 200; 3 Long num3 = 300l; 4 System.out.println(num3 == (num1 + num2)); //true
我们来反编译一些这个class文件:javap -c StringTest
可以看到运算的时候首先对num3进行拆箱(执行num3的longValue得到基础类型为long的值300),然后对num1和mum2进行拆箱(分别执行了num1和num2的intValue得到基础类型为int的值100和200),然后进行相关的基础运算。
我们来对基础类型进行一个测试:
1 int num1 = 100; 2 int num2 = 200; 3 long mum3 = 300; 4 System.out.println(num3 == (num1 + num2)); //true
就说明了为什么最上面会返回true.
所以,当 “==”运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。
陷阱1:
1 Integer integer100=null; 2 int int100=integer100;
这两行代码是完全合法的,完全能够通过编译的,但是在运行时,就会抛出空指针异常。其中,integer100为Integer类型的对象,它当然可以指向null。但在第二行时,就会对integer100进行拆箱,也就是对一个null对象执行intValue()方法,当然会抛出空指针异常。所以,有拆箱操作时一定要特别注意封装类对象是否为null。
总结:
1、需要知道什么时候会引发装箱和拆箱
2、装箱操作会创建对象,频繁的装箱操作会消耗许多内存,影响性能,所以可以避免装箱的时候应该尽量避免。
3、equals(Object o) 因为原equals方法中的参数类型是封装类型,所传入的参数类型(a)是原始数据类型,所以会自动对其装箱,反之,会对其进行拆箱
4、当两种不同类型用==比较时,包装器类的需要拆箱, 当同种类型用==比较时,会自动拆箱或者装箱
转载:https://www.cnblogs.com/wang-yaz/p/8516151.html