深入理解Java中的包裝類與自動拆裝箱

深入理解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類型的變量

通俗點來說,就是基本類型和他的包裝類可以互相賦值了,在賦值的時候,編譯器自動的進行了包裝/拆箱工作,但是不僅僅是賦值的時候會發生自動拆裝箱,請看下一個問題

什么時候會發生自動拆裝箱

  1. 賦值
    上邊大家已經看到了,不說啦
  2. 方法調用傳入參數的時候
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()方法,其他基本類型類推

有哪些問題得注意?

  1. 性能問題

首先在堆內存中創建對象的消耗肯定是要比使用棧內存要多的,同時在自動拆裝箱的時候,也有一定的性能消耗,如果在數據量比較大,或者是循環的情況下,頻繁的拆裝箱并且生成包裝類的時候,對性能的影響就是一星半點了,所以不是特殊的需求,例如上述被集合持有的情況,還是使用基本類型而不是包裝類

在循環的時候

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>,在避免了大量自動拆裝箱的同時,還降低的內存消耗。這里就點到為止,具體的數據結構的問題我們就不深入了

  1. 重載與自動裝箱

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

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

至于為什么,我們就不深挖下去了,小伙伴可以自己去看源碼,這里要說的是,包裝類都有相應的緩存機制,來降低一般情況下的資源消耗,但是每個包裝類的機制肯定是不一樣的,大家自己去探索

  1. == 和 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比較

  1. 警惕NullPointerException

我們在使用基本類型的時候,在聲明的時候即使我們沒有對變量進行賦值,編譯器也會自動的為其賦予初始值,比如int值就是0,boolean就是flase,所以我們在使用基本類型的時候,是不會出現NullPointerException的,但在使用包裝類的時候,我們就要注意這個問題了,不能因為有自動拆裝箱這個語法糖,就忘記了包裝類和基本類型的區別,將其同等對待了,如果你沒有在使用包裝類的時候通過顯式、或是通過自動裝箱機制為其賦值,在你取出值、或是通過自動拆箱使用該值的時候,就會發生NullPointerException,這個是大家要注意的

總結

在Java中,使用基本類型還是最節省資源的選擇,雖然基礎類型影響了Java的"面向對象性",,但是犧牲換來了性能,所以在非必要的時候,所以我們應該盡量避免使用包裝類,并且在使用的時候,要清楚自動拆裝箱機制,規避使用的誤區和風險

某些內容和栗子參考于網絡,沒有找到原po,感謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 自動裝箱和拆箱從Java1.5開始引入,目的是將原始類型值轉自動地轉換成對應的對象。自動裝箱與拆箱的機制可以讓我們...
    GB_speak閱讀 624評論 0 4
  • 自動裝箱和拆箱從Java 1.5開始引入,目的是將原始類型值轉自動地轉換成對應的對象。自動裝箱與拆箱的機制可以讓我...
    codersm閱讀 423評論 0 0
  • 基本數據類型的包裝類 包裝類基本知識 Java是面向對象的語言,但不是“純面向對象”,基本數據類型就不是對象。但是...
    全棧JAVA筆記閱讀 564評論 0 1
  • 幸運者:余俊娟 地點:湖北省武漢市 時間:2017.7.20,周四 1,我怎么如此幸運,早上起來,啟動了早晨的程序...
    余俊娟閱讀 280評論 0 0
  • 發現了很牛,裝逼的圖片美美機器 覺得發表我的成果 果然顯得高逼格 于是就多發幾張 太好玩了,我愛你
    旭小東閱讀 164評論 0 0