深入理解Java中的包装类与自动拆装箱
文章出处:安卓进阶学习指南
作者:麦田哥(Wheat7)
审核者:shixinzhang Struggle
完稿日期:2017.10.30
今儿来和大家聊一聊Java中的自动拆装箱问题,也是我们安卓进阶学习指南的一部分,欢迎大家多多关注,其中的一些问题也是我重新学习得到的,欢迎大家多多讨论
什么是自动拆装箱
自动拆装箱在Java5(就是Java1.5,后边改了命名)时被引入,自动装箱就是Java自动将基础类型值转换成对应的包装类对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。说白了,就是个语法糖
基本类型与引用类型
稍有常识的人都看得出。。。哦,不对,稍有Java基础的同学都应该知道Java的数据类型,大的分类就分为基础类型与引用类类型
基础类型又能分为我们俗称的四类八种,分别为四种整型,byte,short,int,long,他们的区别是所能存储的数据的长度不同,也就是说他们在内存中分配的内存长度不同,两种浮点类型,32位的单精度浮点float,64位双精度浮点数double,1种Unicode编码的字符单元
char,最后就是boolean,真值布尔类型。 接下来是与我们今天主题相关的重点,就是基础类型是存储在栈内存中的,在程序启动的时候就会被初始化,就是你用或不用,他都在那里,在你声明一个基本类型的时候,他就被赋予了默认的初始值,比如int类型,就是0
并且我们再扩展讨论一下这个情况
int a = 1
int b = 1
System.out.printf(a == b) ---- true
因为 == 判断的是内存地址,也就是判断两者是否为同一个对象,基本类型相同的值指向的是同一块内存区域,所以返回的是ture,这也就解释了我们为什么可以用==来判断基本类型,而不能用 == 来判断引用类型,而是要用equals()方法
基本类型并不具对象的性质
2.引用类型又有类,接口,数组三种,为什么叫引用类型,因为我们的引用类型的对象,是存在于堆内存中的,我们所持有的是栈内存中指向相应堆内存的一个引用
这和自动拆装箱有什么关系?请看下边
持有对象&包装类
在有些情况下,我们需要持有一系列的对象,也就是使用我们常用的集合类,在这里不展开说,然而集合类在设计的时候持有的是我们所有类型的单根超类,Object,在将对象装入集合的时候,对象都会被向上转型为Object类,然后取出的时候,又通过参数化类型,也就是我们常用的泛型菱形<>语法,转型为我们装入的原始类型,但是如果我们呢要持有的是基本类型呢?基础类型的并没有父类,所以集合类并不能持有他,那怎么办呢?于是Java为每一个基础类型封装了相应的包装类
基本类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
于是我们可以这样操作了
List<Interger> intList = new ArrayList<>();
intList.add(1);
注意后边一句,这就是我们后边要说的自动装箱,因为add方法需要传入的是List中所持有的参数化类型,也就是int的包装类型Integer,而我们传入的是一个int类型的值,这个int值被编译器自动包装成了Integer值,这就是本文的主题,自动拆装箱,后边我们会展开细说
你应该会想到,数组可以来持有基本类型啊,但是你也知道的是,有些时候我们要持有的数量是不确定的,数组在初始化的时候就必须确定长度,这使我们使用数组来持有基本类型,或是对象都有很大的局限性
集合持有对象是包装类最常见的应用点,当然也有其他地方,我们需要的是Object参数而又需要的是数值,或者是其他基本类型的时候,也会应用到包装类,包装类使基本类型有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作
自动拆装箱
何为自动拆装箱,请看代码
Java5以前
//装箱
Integer integer = new Integer(10);
//拆箱
int i = integer.intValue();
Java5以后
//自动装箱
Integer integer = 10;
//自动拆箱
int i = integer;
在Java5之前,你需要一个Integer类型的对象,你需要像其他对象一样,把他new出来(调用静态方法Integer.valueOf(3)来创建对象内部也是new,别抬杠),拆箱需要调用intValue()方法来取出int值,而在Java5之后,你创建Integer类型的对象,可以直接用int类型赋值,Integer类型的也能赋值给int类型的变量
通俗点来说,就是基本类型和他的包装类可以互相赋值了,在赋值的时候,编译器自动的进行了包装/拆箱工作,但是不仅仅是赋值的时候会发生自动拆装箱,请看下一个问题
什么时候会发生自动拆装箱
- 赋值
上边大家已经看到了,不说啦 - 方法调用传入参数的时候
public void argAutoBoxing(Integer i) {
}
argAutoBoxing(1);
public void argAutoUnBoxing(int i) {
}
argAutoUnBoxing(new Integer(1));
3.被操作符操作的时候
Integer integer = new Integer(1);
int i = interger + 1
自动拆装箱是怎么实现的
一句话,就是编译器帮我们自动调用了拆装箱的方法,以Integer/int为例子,自动装箱就是编译器自动调用了valueOf(int i)方法,自动拆箱自动调用了intValue()方法,其他基本类型类推
有哪些问题得注意?
- 性能问题
首先在堆内存中创建对象的消耗肯定是要比使用栈内存要多的,同时在自动拆装箱的时候,也有一定的性能消耗,如果在数据量比较大,或者是循环的情况下,频繁的拆装箱并且生成包装类的时候,对性能的影响就是一星半点了,所以不是特殊的需求,例如上述被集合持有的情况,还是使用基本类型而不是包装类
在循环的时候
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}
上面的代码sum+=i可以看成sum = sum + i,在sum被+操作符操作的时候,会对sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下
sum = sum.intValue() + i;
Integer sum = new Integer(result);
sum为Integer类型,在上面的循环中会创建4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题
再举一个例子,在Java中的HashMap的性能也受到自动拆装箱的影响
因为HashMap接收的参数类型是HashMap <Object, Object>,所以在增删改查的时候,都会对Key值进行大量的自动拆装箱,为了解决这个问题,Java提供了SparseArray,包括SparseBoolMap, SparseIntMap, SparseLongMap, LongSparseMap,所接受的Key值都是基本类型的值,例如SparseIntMap就是SparseIntMap<int, Object>,在避免了大量自动拆装箱的同时,还降低的内存消耗。这里就点到为止,具体的数据结构的问题我们就不深入了
- 重载与自动装箱
在Java5之前,value(int i)和value(Integer o)是完全不相同的方法,开发者不会因为传入是int还是Integer调用哪个方法困惑,但是由于自动装箱和拆箱的引入,处理重载方法时稍微有点复杂,例如在ArrayList中,有remove(int index)和remove(Object o)两个重载方法,如果集合持有三个Integer类型值为3,1,2的对象,我们调用remove(3), 是调用了remove的哪个重载方法?remove掉的是值为3的对象,还是remove了index为3,值为2的那个对象呢?其实问题就是,参数3是否会被自动打包呢?答案是:不会,在这种情况下,编译器不会进行自动拆装箱,所以调用的是remove(int index),index为3值为2的这个Integer对象会被remove
通过以下例子我们可以验证
public void testAutoBoxing(int i){
System.out.println("primitive argument");
}
public void testAutoBoxing(Integer integer){
System.out.println("wrapper argument");
}
//calling overloaded method
int value = 1;
test(value); //no autoboxing
Integer iValue = value;
test(iValue); //no autoboxing
Output:
primitive argument
wrapper argument
- 缓存值问题
这个问题是面试的常客了
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);
System.out.println(i3==i4);
}
}
Output:
true
false
这是为什呢,让我们来翻一翻源码
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
欸,看来问题就出在IntegerCache类中了,我们再来翻一下IntegerCache的实现类
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象
我们再来看一题
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);
System.out.println(i3==i4);
}
}
Output:
flase
flase
至于为什么,我们就不深挖下去了,小伙伴可以自己去看源码,这里要说的是,包装类都有相应的缓存机制,来降低一般情况下的资源消耗,但是每个包装类的机制肯定是不一样的,大家自己去探索
- == 和 equlas()
大家都应该清楚明了的了解两者的区别,一句话说就是 == 比较的是内存中地址,equlas()对比的为数值,因为基本类型相同的数值指向的同一块内存,所以可以用==来比较,而引用类型则不可以
下边我们也是直观的使用代码来说明在包装类和自动拆装箱时使用==和equlas()的情况
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
System.out.println(g.equals(a+h));
}
}
Output:
true
false
true
true
true
false
true
在包装类的使用和自动拆装箱中,使用==运算符的时候,如果两个操作数都是包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(上边说到的的使用运算符触发了自动拆箱)。另外,对于包装器类型,equals()方法并不会进行类型转换,和我们常见的对String类型使用一样,比较的是对象的值
理解了这个,大家应该就对结果清晰明了了,第一句和第二句是因为上边说过的缓存机制,重点解释一下第三句,a+b包含了算术运算,因此会触发自动拆箱过程,因此它们比较的是数值是否相等。而对于c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较
- 警惕NullPointerException
我们在使用基本类型的时候,在声明的时候即使我们没有对变量进行赋值,编译器也会自动的为其赋予初始值,比如int值就是0,boolean就是flase,所以我们在使用基本类型的时候,是不会出现NullPointerException的,但在使用包装类的时候,我们就要注意这个问题了,不能因为有自动拆装箱这个语法糖,就忘记了包装类和基本类型的区别,将其同等对待了,如果你没有在使用包装类的时候通过显式、或是通过自动装箱机制为其赋值,在你取出值、或是通过自动拆箱使用该值的时候,就会发生NullPointerException,这个是大家要注意的
总结
在Java中,使用基本类型还是最节省资源的选择,虽然基础类型影响了Java的"面向对象性",,但是牺牲换来了性能,所以在非必要的时候,所以我们应该尽量避免使用包装类,并且在使用的时候,要清楚自动拆装箱机制,规避使用的误区和风险
某些内容和栗子参考于网络,没有找到原po,感谢!