Java包装类、拆箱和装箱详解

简述

虽然Java语言是面向对象编程语言,但其中的八种基本数据类型并不支持面向对象编程,基本类型的数据不具有 “对象”的特性---不携带属性、没有方法可调用。java

Java为每种基本数据类型分别设计了对于的类,称之为包装类。基本数据类型及对应的包装类面试

输入图片说明

每一个包装类的对象能够封装一个相应的基本类型的数据,并提供了其它一些有用的方法。包装类对象一经建立,其内容(所封装的基本类型数据值)不可改变。编程

基本类型和对应的包装类能够相互装换:缓存

  • 由基本类型向对应的包装类转换称为装箱,例如把 int 包装成 Integer 类的对象;
  • 包装类向对应的基本类型转换称为拆箱,例如把 Integer 类的对象从新简化为 int。

什么是自动装箱和拆箱

自动装箱就是Java自动将原始类型值转换成对应的对象,好比将int的变量转换成Integer对象,这个过程叫作装箱,反之将Integer对象转换成int类型值,这个过程叫作拆箱。由于这里的装箱和拆箱是自动进行的非人为转换,因此就称做为自动装箱和拆箱。原始类型byte,short,char,int,long,float,double和boolean对应的封装类为Byte,Short,Character,Integer,Long,Float,Double,Boolean。app

自动装箱拆箱要点

  • 自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器经过调用相似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
  • 自动装箱是将boolean值转换成Boolean对象,byte值转换成Byte对象,char转换成Character对象,float值转换成Float对象,int转换成Integer,long转换成Long,short转换成Short,自动拆箱则是相反的操做。

什么时候发生自动装箱和拆箱

自动装箱和拆箱在Java中很常见,好比咱们有一个方法,接受一个对象类型的参数,若是咱们传递一个原始类型值,那么Java会自动讲这个原始类型值转换成与之对应的对象。最经典的一个场景就是当咱们向ArrayList这样的容器中增长原始类型数据时或者是建立一个参数化的类,好比下面的ThreadLocal。编程语言

ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(1); //autoboxing - primitive to object
intList.add(2); //autoboxing
 
ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>();
intLocal.set(4); //autoboxing
 
int number = intList.get(0); // unboxing
int local = intLocal.get(); // unboxing in Java

举例说明

赋值时

这是最多见的一种状况,在Java 1.5之前咱们须要手动地进行转换才行,而如今全部的转换都是由编译器来完成。性能

//before autoboxing
Integer iObject = Integer.valueOf(3);
Int iPrimitive = iObject.intValue()
 
//after java5
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion

方法调用时

这是另外一个经常使用的状况,当咱们在方法调用时,咱们能够传入原始数据值或者对象,一样编译器会帮咱们进行转换。ui

public static Integer show(Integer iParam){
   System.out.println("autoboxing example - method invocation i: " + iParam);
   return iParam;
}
 
//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer

show方法接受Integer对象做为参数,当调用show(3)时,会将int值转换成对应的Integer对象,这就是所谓的自动装箱,show方法返回Integer对象,而int result = show(3);中result为int类型,因此这时候发生自动拆箱操做,将show方法的返回的Integer对象转换成int值。设计

自动装箱的弊端

自动装箱有一个问题,那就是在一个循环中进行自动装箱操做的状况,以下面的例子就会建立多余的对象,影响程序的性能。code

Integer sum = 0;
 for(int i=1000; i<5000; i++){
   sum+=i;
}

上面的代码sum+=i能够当作sum = sum + i,可是+这个操做符不适用于Integer对象,首先sum进行自动拆箱操做,进行数值相加操做,最后发生自动装箱操做转换成Integer对象。其内部变化以下

sum = sum.intValue() + i;
Integer sum = new Integer(result);

因为咱们这里声明的sum为Integer类型,在上面的循环中会建立将近4000个无用的Integer对象,在这样庞大的循环中,会下降程序的性能而且加剧了垃圾回收的工做量。所以在咱们编程时,须要注意到这一点,正确地声明变量类型,避免由于自动装箱引发的性能问题。

重载与自动装箱

当重载赶上自动装箱时,状况会比较有些复杂,可能会让人产生有些困惑。在1.5以前,value(int)和value(Integer)是彻底不相同的方法,开发者不会由于传入是int仍是Integer调用哪一个方法困惑,可是因为自动装箱和拆箱的引入,处理重载方法时稍微有点复杂。一个典型的例子就是ArrayList的remove方法,它有remove(index)和remove(Object)两种重载,咱们可能会有一点小小的困惑,其实这种困惑是能够验证并解开的,经过下面的例子咱们能够看到,当出现这种状况时,不会发生自动装箱操做。

public void test(int num){
    System.out.println("method with primitive argument");
 
}
 
public void test(Integer num){
    System.out.println("method with wrapper argument");
 
}
 
//calling overloaded method
AutoboxingTest autoTest = new AutoboxingTest();
int value = 3;
autoTest.test(value); //no autoboxing 
Integer iValue = value;
autoTest.test(iValue); //no autoboxing
 
Output:
method with primitive argument
method with wrapper argument

要注意的事项

自动装箱和拆箱可使代码变得简洁,可是其也存在一些问题和极端状况下的问题,如下几点须要咱们增强注意。

对象相等比较

这是一个比较容易出错的地方,”==“能够用于原始值进行比较,也能够用于对象进行比较,当用于对象与对象之间比较时,比较的不是对象表明的值,而是检查两个对象是不是同一对象,这个比较过程当中没有自动装箱发生。进行对象值比较不该该使用”==“,而应该使用对象对应的equals方法。看一个能说明问题的例子。

public class AutoboxingTest {
 
    public static void main(String args[]) {
 
        // Example 1: == comparison pure primitive – no autoboxing
        int i1 = 1;
        int i2 = 1;
        System.out.println("i1==i2 : " + (i1 == i2)); // true
 
        // Example 2: equality operator mixing object and primitive
        Integer num1 = 1; // autoboxing
        int num2 = 1;
        System.out.println("num1 == num2 : " + (num1 == num2)); // true
 
        // Example 3: special case - arises due to autoboxing in Java
        Integer obj1 = 1; // autoboxing will call Integer.valueOf()
        Integer obj2 = 1; // same call to Integer.valueOf() will return same
                            // cached Object
 
        System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true
 
        // Example 4: equality operator - pure object comparison
        Integer one = new Integer(1); // no autoboxing
        Integer anotherOne = new Integer(1);
        System.out.println("one == anotherOne : " + (one == anotherOne)); // false
 
    }
 
}
 
Output:
i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false

值得注意的是第三个小例子,这是一种极端状况。obj1和obj2的初始化都发生了自动装箱操做。可是处于节省内存的考虑,JVM会缓存-128到127的Integer对象。由于obj1和obj2其实是同一个对象。因此使用”==“比较返回true。

生成无用对象增长GC压力

由于自动装箱会隐式地建立对象,像前面提到的那样,若是在一个循环体中,会建立无用的中间对象,这样会增长GC压力,拉低程序的性能。因此在写循环时必定要注意代码,避免引入没必要要的自动装箱操做。

三.面试中相关的问题

虽然大多数人对装箱和拆箱的概念都清楚,可是在面试和笔试中遇到了与装箱和拆箱的问题却不必定会答得上来。下面列举一些常见的与装箱/拆箱有关的面试题。

1.下面这段代码的输出结果是什么?

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);
    }
}

也许有些朋友会说都会输出false,或者也有朋友会说都会输出true。可是事实上输出结果是:

true
false

为何会出现这样的结果?输出结果代表i1和i2指向的是同一个对象,而i3和i4指向的是不一样的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:

public static Integer valueOf(int i) {
        if(i >= -128 && i <= IntegerCache.high)
            return IntegerCache.cache[i + 128];
        else
            return new Integer(i);
    }

而其中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() {}
    }

从这2段代码能够看出,在经过valueOf方法建立Integer对象的时候,若是数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;不然建立一个新的Integer对象。

上面的代码中i1和i2的数值为100,所以会直接从cache中取已经存在的对象,因此i1和i2指向的是同一个对象,而i3和i4则是分别指向不一样的对象。

2.下面这段代码的输出结果是什么?

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);
    }
}

也许有的朋友会认为跟上面一道题目的输出结果相同,可是事实上却不是。实际输出结果为:

false
false

至于具体为何,读者能够去查看Double类的valueOf的实现。

在这里只解释一下为何Double类的valueOf方法会采用与Integer类的valueOf方法不一样的实现。很简单:在某个范围内的整型数值的个数是有限的,而浮点数却不是。

注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是相似的。

Double、Float的valueOf方法的实现是相似的。

3.下面这段代码输出结果是什么:

public class Main {
    public static void main(String[] args) {
 
        Boolean i1 = false;
        Boolean i2 = false;
        Boolean i3 = true;
        Boolean i4 = true;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

输出结果是:

true
true

至于为何是这个结果,一样地,看了Boolean类的源码也会一目了然。下面是Boolean的valueOf方法的具体实现:

public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

而其中的 TRUE 和FALSE又是什么呢?在Boolean中定义了2个静态成员属性:

public static final Boolean TRUE = new Boolean(true);
 
    /** 
     * The <code>Boolean</code> object corresponding to the primitive 
     * value <code>false</code>. 
     */
    public static final Boolean FALSE = new Boolean(false);

至此,你们应该明白了为什么上面输出的结果都是true了。

4.谈谈Integer i = new Integer(xxx)和Integer i =xxx;这两种方式的区别。

固然,这个题目属于比较宽泛类型的。可是要点必定要答上,我总结一下主要有如下这两点区别:

1)第一种方式不会触发自动装箱的过程;而第二种方式会触发;

2)在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在通常性状况下要优于第一种状况(注意这并非绝对的)。

5.下面程序的输出结果是什么?

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;
  
// 会自动拆箱(会调用intValue方法)
System.out.println(c==d);
// 会自动拆箱后再自动装箱
System.out.println(e==f);
// 虽然“==”比较的是引用的是不是同一对象,但这里有算术运算,若是该引用为包装器类型则会致使自动拆箱
System.out.println(c==(a+b));
// equals 比较的是引用的对象的内容(值)是否相等,但这里有算术运算,若是该引用为包装器类型则会导
 // 致自动拆箱,再自动装箱
// a+b触发自动拆箱获得值后,再自动装箱与c比较
System.out.println(c.equals(a+b));
// 首先a+b触发自动拆箱后值为int型,因此比较的是值是否相等
System.out.println(g==(a+b));
// 首先a+b触发自动拆箱后值为int型,自动装箱后为Integer型,而后g为Long型
System.out.println(g.equals(a+b));
// 首先a+h触发自动拆箱后值为long型,由于int型的a会自动转型为long型的g而后自动装箱后为Long型,
 // 而g也为Long型
System.out.println(g.equals(a+h));
  
}

先别看输出结果,读者本身想一下这段代码的输出结果是什么。这里面须要注意的是:当 “==”运算符的两个操做数都是 包装器类型的引用,则是比较指向的是不是同一个对象,而若是其中有一个操做数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。另外,对于包装器类型,equals方法并不会进行类型转换。明白了这2点以后,上面的输出结果便一目了然:

true
false
true
true
true
false
true

第一个和第二个输出结果没有什么疑问。第三句因为 a+b包含了算术运算,所以会触发自动拆箱过程(会调用intValue方法),所以它们比较的是数值是否相等。而对于c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,获得了加法运算后的数值以后,便调用Integer.valueOf方法,再进行equals比较。同理对于后面的也是这样,不过要注意倒数第二个和最后一个输出的结果(若是数值是int类型的,装箱过程调用的是Integer.valueOf;若是是long类型的,装箱调用的Long.valueOf方法)。

System.out.println(g==(a+b));的分析:a+b 拆箱,(a+b) 会在装箱。Long 是装箱对象,此时会指向同一个地址。这次的值是小于128的。

注意

这里面须要注意的是:当 “==”运算符的两个操做数都是包装器类型的引用,则是比较指向的是不是同一个对象,而若是其中有一个操做数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)另外,对于包装器类型,equals方法并不会进行类型转换。

相关文章
相关标签/搜索