自动拆箱(unboxing)&自动装箱(boxing)
@author 李东秀|| qq:1028659927
本文主要为自己理解所做的学习笔记,如有不对的地方,
望各位看官不吝指出,代码运行环境:Ubuntu 14.04,jdk1.7版本
在jdk 1.5之前,如果你想要定义一个value为100的Integer对象,则需要如下定义:
Integer i=new Integer (100);
但有了自动拆装箱之后就可以直接把基本类型赋值给Integer对象。
int intNum1=100;//普通常量
Integer intNum2=intNum1;//自动装箱
int intNum3=intNum2;//自动拆箱
Integer intNum4=100;//自动装箱
上面的代码中,intNum2为一个Integer类型的实例,intNum1为Java中的基础数据类型,
将intNum1赋值给intNum2便是自动装箱;而将intNum2赋值给intNum3则是自动拆箱。
Java为我们提供了八种基本数据类型: boolean byte char shrot int long float double ,所生成的变量相当于常量;
对应基本类型包装类:Boolean Byte Character Short Integer Long Float Double。
自动拆箱和自动装箱定义:
自动装箱是将一个java定义的基本数据类型赋值给相应封装类的变量。
拆箱与装箱是相反的操作,自动拆箱则是将一个封装类的变量赋值给相应基本数据类型的变量。
自动封箱和自动装箱的原理
自动封箱和自动拆箱是java自动帮我们完成的,通过调试可以看到执行过程,如果调试过程中无法进入源码中的函数,可能因为设置的是JRE运行环境,替换成jdk就可以了,【Window】-【Preference】-【java】-【Installed JREs】
int intNum1=100;//普通常量
Integer intNum2=intNum1;//自动装箱
int intNum3=intNum2;//自动拆箱
Integer intNum4=100;//自动装箱
在每一行代码添加断点,进入调试模式,
第一行代码无法进入函数,说明中间没有调用其他的函数,
第二行代码和第四行代码进入了Integer.valueOf(int);
第三行代码进入了函数Integer.intValue();
第二行代码进入如下代码:
public static Integer valueOf(int i) {
//对于Integer类型变量,如果i在IntegerCache.low-IntegerCache.low之间,
//就直接在缓存中取出i的Integer类型对象 ,而不需要生成新的对象。
if (i >= IntegerCache.low && i <= IntegerCache.low
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);//否则在堆上创建新的对象
}
此代码会去判断是否在可缓存的范围内( if (i >= IntegerCache.low && i <= IntegerCache.high)),如果满足缓存条件进入 return IntegerCache.cache[i + (-IntegerCache.low)];否则new 一个新的对象。所以可以看到Integer intNum2=intNum1;并不是每次都会生成新对象。
第三行进入intValue函数直接返回基本类型。
public int intValue() {
return value;
}//其他类型则会调用xxValue()
下面是缓存数据的代码,说明了缓存的范围
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
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() {}
}
注意此处代码:
String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
说明integerCacheHighPropValue的大小是从VM虚拟机中保存的属性取到的,
所以integerCacheHighPropValue 是可以通过vm命令进行设置的。
可以通过调整虚拟机-XX:AutoBoxCacheMax=<size>选项,调整“自动装箱池”的大小 ,
具体设置方法需要设置虚拟机模式,这里不做讲解。
自动封箱&自动拆箱的使用场景:
(1)赋值过程中:
此过程不在举例
(2)函数参数:
public static int subFun(Integer num){
return num;
}
调用此函数就会发生自动拆箱
public static Integer subFun(int num){
return num;
}
上述两种场景在工作过程中最常见,另外常见的自动拆箱和装箱的场景就是集合操作,集合只能接收对象,但我们传入普通类型也是可以的,它内部就完成了自动装箱。
特别需要注意地方:
1、 是否生成新的对象:
/**
* 基本类型的常量池说明(-127-128)
* @author 李东秀(1028659927)
*/
Integer num1 = 100;
Integer num2 = 100;
System.out.println("num1==num2: " + (num1 == num2));// true 两个自动装箱的对象,都是对常量池中对象的引用,所以相同。
Integer num3 = 200;
Integer num4 = 200;
System.out.println("num3>num4: " + (num3 > num4)); // false 将两个对象拆箱,再比较大小 ,类似的操作还有加减乘除取余等
Integer num5 = new Integer(100);
Integer num6 = new Integer(100);
System.out.println("num5==num6: " + (num5 == num6)); // false 都会生成新的对象,无论与谁比较都是false
int intNum1 = 100;
System.out.println("num1==int1: " + (num1 == intNum1));// true Integer缓存对象拆箱后与int比较 ,已经不是对象地址的比较
int intNum2 = 200;
System.out.println("num3==intNum2: " + (num3 == intNum2));// true Integer对象拆箱后与int比较
所以new对象的方式一定会产生新对象,但自动装箱的方式则不一定产生新对象,对于Integer对象和基本对象进行运算操作或者逻辑操作,则会首先进行拆箱,之后进行值大小的比较。非new方式生成的Integer这里用缓存解释,深层的原因是jvm方法区的常量池的存在,缓存的对象大小-128-127,基本类型中Byte缓存大小为(-128-127),Long缓存大小为()等:
java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用常量池,也即对象不负责创建和管理大于127的这些类的对象。
其它基本数据类型对应的包装类型的自动装箱池大小(可以在各个类型的缓存类中找到范围):
- Boolean :全部缓存
- Byte :全部缓存
- Character : <=127缓存
- Short : (-128,127)缓存
- Long : (-128,127)缓存
- Float : (没有缓存)
- Double : (没有缓存)
//5种整形的包装类Byte,Short,Integer,Long,Character的对象,
//在值小于127时可以使用常量池
Integer intdemo1=127;
Integer intdemo2=127;
System.out.println(intdemo1==intdemo2); //输出true
//值大于127时,不会从常量池中取对象
Integer intdemo3=128;
Integer intdemo4=128;
Byte bdemo1=12;
Byte bdemo2=12;
System.out.println(bdemo1==bdemo2); //输出true
Character chdemo1='a';//a=97
Character chdemo2='a';
System.out.println(chdemo1==chdemo2);//true
//.......整形包装类后面就不在实验
System.out.println(intdemo3==intdemo4); //输出false
//Boolean类也实现了常量池技术
Boolean booldemo1=true;
Boolean booldemo2=true;
System.out.println(booldemo1==booldemo2); //输出true
//浮点类型的包装类没有实现常量池技术
Double ddemo1=1.0;
Double ddemo2=1.0;
System.out.println(ddemo1==ddemo2); //输出false
Java利用内存中存储拘留字符串也为String变量提供了缓存特性。
Integer I1 = 20;
Integer I2 = 20;
Integer I3 = 0;
Integer I4 = 129;
Integer I5 = 129;
Integer I6 = new Integer("20");
Integer I7 = new Integer("20");
Integer I8 = new Integer("0");
// ==
System.out.println(I1 == I2);// 因为小于127所以都是引用的常量池汇总对象,相同
System.out.println(I1 == I2 + I3);// 拆箱,不存在新对象的产生
System.out.println(I4 == I5);// 无法存入常量池,会生成新的对象
System.out.println(I4 == I5 + I3);// 拆箱比较普通值
System.out.println(I1 == I6);// 生成新对象
System.out.println(I6 == I7);// 生成新对象
System.out.println(I6 == I7 + I8);//
System.out.println("================");
// equals比较值是否相同
System.out.println(I1.equals(I2));
System.out.println(I1.equals(I2 + I3));
System.out.println(I4.equals(I5));
System.out.println(I4.equals(I5 + I3));
System.out.println(I6.equals(I7));
System.out.println(I6.equals(I7 + I8));
/**
* String 类型常量池
*/
System.out.println("================");
String str1 = "ab";
String str2 = "ab";
String str3 = "cd";
String str6 = "abcd";
String str4 = "a";
String str5 = "bcd";
System.out.println(str1 == str2);// 证明常量池存在没有生成新的对象
System.out.println(str6 == str1 + str2);// 没有拆箱过程所以不存在常量池的常量直接比较
System.out.println(str6 == "abcd");//
// 涉及到变量(不全是常量)的相加,所以会生成新的对象,其内部实现是先new一个StringBuilder
System.out.println(str2 + str3 == str4 + str5);// 变量的改变,涉及到新建变量
System.out.println(str6.equals(str2 + str3));
// 下面重新定义,因为常量池中已存在abcd
String str10 = new String("AB");
String str11 = new String("AB");
System.out.println("================");
System.out.println(str10 == str11);
String str7 = "AB";
System.out.println(str10 == str7);
String str8 = "CD";
String str9 = "ABCD";
System.out.println(str9 == str7 + str8);
System.out.println(str9 == str10 + str11);
String str12 = new String("CD");// 生成新的对象,CD 放入常量池
String str13 = new String("ABCD");
System.out.println(str9 == str13);
System.out.println(str13 == str11 + str12);
System.out.println(str13 == "ABCD");
String str14 = new String("ABC") + new String("DEF");// 形成的ABCDEF不会被放入常量池
str14.intern();// 添加进常量池中(缓存)
String str15 = "ABCDEF";
System.out.println(str14 == str15);
String str16 = new String("AAA");// AAA会被放入常量池中,由于是new产生的对象多以str16不和任何对象相同
String str17 = "AAA";
System.out.println(str16 == str17);
结果:true true false true false false true
================
true true true true true true
================
true false true false true
================
False false false false false false false true false
除了八种基本类型还有java提供的一种比较特殊的类型String(复合)。Java也为它提供了缓存机制,源代码中所有相同字面值的字符串常量只可能建立唯一 一个拘留字符串对象。 实际上JVM是通过一个记录了拘留字符串引用的内部数据结构来维持这一特性的。在Java程序中,可以调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象。
String主要使用方法有两种:如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。
提到此处就必须得提String.intern方法,intern方法的作用如下:直接使用双引号声明出来的String对象会直接存储在常量池中,如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
String s = new String("A");
会创建两个对象一个“A”存储在常量池中,另外一个创建在堆上,由于两个对象所在位置就不相同。所以一定是不相同的。
String s2 = new String("B") + new String("C");//此种方式创建的s2不会被放在常量池中,可以调用intern()方法把s2放入常量池中
Jdk1.7中:
String str14=new String("ABC")+new String("DEF");//此种方式生成的str14不会被放在常量池中
String str15="ABCDEF";
System.out.println(str14==str15);
结果:false
String str14=new String("ABC")+new String("DEF");
str14.intern();
String str15="ABCDEF";
System.out.println(str14==str15);
结果:true
看过jvm运行时内存分配的都知道,Intern方法的使用在Jdk7中和jdk6中稍有不同:
将String常量池从Perm区移动到了Java Heap区,所以调用intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。(这部分内容还正在看,由于工作比较忙,最近刚开始写blog,会在后面逐渐写一系列文章)
2 Java函数重载
重载的定义为:函数参数类型不同或者参数个数不同则说明两函数重载。
但现在存在自动装箱,自动拆箱功能是否导致重载的函数为一个函数。
public static int add(Integer num){
return num;
}
public static int add(int num){
return num;
}
这样定义是可以的,使用时相应参数传进函数也不存在混淆问题,所以重载不会受到影响。
3 定义的对象可以为空,但是当自动拆箱赋值给普通类型时或者和基本类型比较时,则会报错误。
Integer num1=null;
int num2;
num2=num1;
System.out.println(num2);//或者比较也会发生此类错误
代码能过通过编译,但运行会报java.lang.NullPointerException错误。
4 循环过程的重复对象创建
Integer num4=0;
for(int i=0;i<10;i++){
num4=num4+i;
}
这么简单的一段程序,打断点查看循环过程,会发现每次循环都要先拆箱再装箱,装箱过程中会生成新的对象,所以会生成很多无用的中间对象,降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。
总结:
自动拆箱和自动装箱给变成带来了很多便利,但同时也存在许多弊端,java致力于不让编程者担心内存的分配,由垃圾收集器自动完成内存的管理,这不意味着我们可以随意的使用内存而不加以限制。变成过程中尽量避免可能多次发生的拆箱装箱操作,避免不必要对象的创建。(QQ:1028659927,欢迎指导!)