Java自動拆裝箱(Autoboxing and unboxing)學習
在學習併發的過程中,用“Boolean bool = true”的自動裝箱方式初始化了兩個物件鎖去鎖兩塊程式碼,結果執行的時候出現了競爭等待,除錯了一下發現兩個鎖變數指向的是同一個物件,由此可見我對自動拆裝箱的機制想的太簡單了,查了一下,發現這個機制還挺細節,那就記錄一下:
本文主要有以下幾個方面:
- 什麼是自動拆裝箱
- 拆裝箱的實現
- 拆裝箱發生的場景
- 關於String
- 回首望月
嘗試的第一篇部落格,不當之處,求輕噴!
一. 什麼是自動拆箱與裝箱
我們都知道,Java定義了8種基本型別和與之對應的8中包裝器,其中6種資料型別,1種字元型別以及1種布林型別:
在Java5之前,定義生成一個Integer包裝器型別的物件,只能通過以下方式:
1 Integer i = new Integer(0);
Java5支援了基本型別和對應的包裝型別之前的自動轉換機制,即自動拆箱(包裝器型別轉換成基本型別)與裝箱(基本型別封裝成包裝器型別)。於是,就有了以下兩行程式碼:
1 Integer i = 0; //自動裝箱 2 int j = i; //自動拆箱
二. 自動拆裝箱的實現(int-Integer為例)
我們將下面自動拆裝箱的程式碼反編譯一下,拆裝箱的動作就一目瞭然。
1 public class MainTest { 2 public static void main(String[] args) { 3 Integer i = 0; 4 int j = i; 5 } 6 }
編譯後:
通過反編譯的結果看,在"Integer i = 0"自動裝箱的過程中,呼叫了Integer.valueOf(int i)方法;在"int j = i;"的自動裝箱的過程中,呼叫了Integer.intValue()方法。
其中,拆箱方法Integer.intValue()方法很簡單:
1 /** 2 * Returns the value of this {@code Integer} as an 3 * {@code int}. 4 */ 5 public int intValue() { 6 return value; 7 }
只是返回了當前物件的value值,沒什麼好說的。
但是裝箱方法Integer.valueOf(int i)就有細節了,一起看下:
1 /** 2 * Returns an {@code Integer} instance representing the specified 3 * {@code int} value. If a new {@code Integer} instance is not 4 * required, this method should generally be used in preference to 5 * the constructor {@link #Integer(int)}, as this method is likely 6 * to yield significantly better space and time performance by 7 * caching frequently requested values. 8 * 9 * This method will always cache values in the range -128 to 127, 10 * inclusive, and may cache other values outside of this range. 11 * 12 * @param i an {@code int} value. 13 * @return an {@code Integer} instance representing {@code i}. 14 * @since 1.5 15 */ 16 public static Integer valueOf(int i) { 17 if (i >= IntegerCache.low && i <= IntegerCache.high) 18 return IntegerCache.cache[i + (-IntegerCache.low)]; 19 return new Integer(i); 20 }
這邊的原始碼比預想的多了一個細節操作,值落在[IntegerCache.low, IntegerCache.high]區間上時,是直接從一個Integer型別的快取陣列IntegerCache.cache中取一個物件返回出去,值不在這個區間時才new一個新物件返回。
看一下IntegerCache的實現,它是Integer類的一個私有靜態內部類:
1 /** 2 * Cache to support the object identity semantics of autoboxing for values between 3 * -128 and 127 (inclusive) as required by JLS. 4 * 5 * The cache is initialized on first usage. The size of the cache 6 * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option. 7 * During VM initialization, java.lang.Integer.IntegerCache.high property 8 * may be set and saved in the private system properties in the 9 * sun.misc.VM class. 10 */ 11 12 private static class IntegerCache { 13 static final int low = -128; 14 static final int high; 15 static final Integer cache[]; 16 17 static { 18 // high value may be configured by property 19 int h = 127; 20 String integerCacheHighPropValue = 21 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); 22 if (integerCacheHighPropValue != null) { 23 try { 24 int i = parseInt(integerCacheHighPropValue); 25 i = Math.max(i, 127); 26 // Maximum array size is Integer.MAX_VALUE 27 h = Math.min(i, Integer.MAX_VALUE - (-low) -1); 28 } catch( NumberFormatException nfe) { 29 // If the property cannot be parsed into an int, ignore it. 30 } 31 } 32 high = h; 33 34 cache = new Integer[(high - low) + 1]; 35 int j = low; 36 for(int k = 0; k < cache.length; k++) 37 cache[k] = new Integer(j++); 38 39 // range [-128, 127] must be interned (JLS7 5.1.7) 40 assert IntegerCache.high >= 127; 41 } 42 43 private IntegerCache() {} 44 }
IntegerCache中有3個final型別的變數:
low:-128(一個位元組能夠表示的最小值);
high:127(一個位元組能夠表示的最大值),JVM中設定的屬性值(通過-XX:AutoBoxCacheMax=設定)二者取大值,再和Integer.MAX_VALUE取小值;
cache:在靜態塊中初始化為由區間[low, high]上的所有整陣列成的升序陣列。
綜上,Java在虛擬機器堆記憶體中維持了一個快取池,在裝箱的過程中,如果發現目標包裝器物件在快取池中已經存在,就直接取快取池中的,否則就新建一個物件。
測試一下:
1 public static void main(String[] args) { 2 Integer i = 127; 3 Integer j = 127; 4 System.out.println(System.identityHashCode(i)); //本地輸出i的地址:1173230247 5 System.out.println(System.identityHashCode(j)); //本地輸出j的地址:1173230247 6 7 Integer m = 128; 8 Integer n = 128; 9 System.out.println(System.identityHashCode(m)); //本地輸出m的地址:856419764 10 System.out.println(System.identityHashCode(n)); //本地輸出n的地址:621009875 11 }
由測試結果來看,值為127時,兩次裝箱返回的是同一個物件,值為128時,兩次裝箱返回的是不同的物件。
因為小數的區間取值無限,所以float->Float,double->Double兩種型別裝箱機制沒有快取機制,其他5中基本型別的封裝機制也是類似int->Integer的裝箱套路,不過快取的邊界不可改變:
基本型別 | 包裝器型別 | 快取區間 | 快取是否可變 |
---|---|---|---|
byte | Byte | [-128, 127] | 不可變 |
short | Short | [-128, 127] | 不可變 |
int | Integer | [-128, 127] | 上限可設定 |
long | Long | [-128, 127] | 不可變 |
float | Float | -- | -- |
double | Double | -- | -- |
char | Character | [0, 127] | 不可變 |
boolean | Boolean | {true, false} | 不可變 |
因為基本型別對應的包裝器都是不可變類,多以他們的快取區間一旦初始化,裡面的值就無法再改變,所以在JVM執行過程中,所有的基本型別包裝器的快取池都是不變的。
三. 拆裝箱發生的場景
1.定義變數和方法引數傳遞:
這裡的拆裝箱是指開發者通過編寫程式碼控制的拆裝箱,比較明顯:
1 public static void main(String[] args) { 2 Integer i = 0; //裝箱 3 int j = i; //拆箱 4 aa(i); //拆箱,傳值時發生了:int fi = i; 5 bb(j); //裝箱,傳值時發生了:Integer fi = j; 6 } 7 private static void aa(int fi){ 8 } 9 private static void bb(Integer fi){ 10 }
2.運算時拆箱
我們都知道,當一個變數的定義型別不是基本型別,其實變數的值是物件的在虛擬機器中的地址,當用初始化後的包裝器型別變數進行運算時,會發生什麼呢?
1.“+,-,*,/ ...”等運算時拆箱
當用包裝器型別的資料進行運算時,JAVA會先執行拆箱操作,然後進行運算。
1 public class MainTest { 2 public static void main(String[] args) { 3 Integer i = 127; 4 Integer j = 127; 5 i = i + j; 6 } 7 }
將上面一段程式碼反編譯:
發現,除了在分別原始碼的3,4行進行了裝箱操作後,在執行add操作之前,有兩次拆箱操作,add之後,又把結果裝箱賦值給變數i。
2.“==”判等運算
“==”運算比較特殊:
A == B
- 當A,B都是基本型別時,直接進行比較兩個變數的值是否相等
- 當A,B都是包裝器型別時,比較兩個變數指向的物件所在的地址是否相等
- 當A,B中有一個是基本型別時,會將另一個包裝器型別拆箱成基本型別,然後再進行基本型別的判等比較
測試如下:
1 public static void main(String[] args) { 2 int m = 128; 3 int n = 128; 4 Integer i = 128; 5 Integer j = 128; 6 System.out.println(m == n); //輸出:true 7 System.out.println(m == i); //輸出:true 8 System.out.println(i == j); //輸出:false 9 }
前文已經說了,JVM沒有設定Integer型別的快取上限的時候,128不在快取池內,所以兩次封裝後的物件是不同的物件。在此基礎上:
- 第6行輸出true:如果比較的是裝箱後的物件地址,結果肯定是false,實際結果是true,說明比較的是基本型別的值,沒有發生自動拆裝箱動作
- 第7行輸出true:如果比較的是裝箱後的物件地址,結果肯定是false,實際結果是true,說明比較的是基本型別的值,那麼包裝器型別的變數肯定進行了自動拆箱動作
- 第8行輸出false:如果比較的是拆箱後的基本型別的值,結果肯定是true,實際結果是false,說明比較的是物件的地址,沒有發生自動拆裝箱動作
看一下反編譯的結果:
對應原始碼中除了第4、5行出現了自動裝箱動作,就只有在第7行發生了自動拆箱動作。
四. 關於String型別
String型別沒有對應的基本型別,所以沒有自動拆裝箱的機制,之所以在這裡提一下,是因為String的初始化過程和自動裝箱的過程很像。
1 public static void main(String[] args) { 2 String s1 = "hello"; 3 String s2 = "hello"; 4 String s3 = new String("hello"); 5 String s4 = new String("hello"); 6 System.out.println(System.identityHashCode(s1)); //輸出s1地址:1173230247 7 System.out.println(System.identityHashCode(s2)); //輸出s2地址:1173230247 8 System.out.println(System.identityHashCode(s3)); //輸出s3地址:856419764 9 System.out.println(System.identityHashCode(s4)); //輸出s5地址:621009875 10 }
從上面的輸出結果可以看出,兩個直接用字串賦值的變數s1,s2指向的是同一個物件,而new String()生成物件賦值的變數s3,s4則是不同的物件。
其實,JVM中存在一個字串快取池,當直接使用字串初始化變數的時候,JAVA會先到字串快取池中檢視有沒有相同值的String物件,如果有,直接返回快取池中的物件;如果沒有,就new出一個新的物件存入快取池,再返回這個物件。而String的不可變性質則能保證在物件共享的過程中不會出現執行緒安全問題。
與基本型別的快取池相比,String型別的快取池在執行時是動態變化的。
五. 回首望月
回到最開始我碰到的問題,當我用“Boolean bool = true”的自動裝箱方式定義變數的時候,這兩個變數其實指向的都是Boolean型別的快取池中的那個值為true的物件,所以用他們當做同步鎖,其實是用的同一把鎖,自然會出現競爭等待。
經驗:當我們使用自動裝箱機制初始化變數的時候,就相當於告訴JAVA這裡需要一個物件,而不是告訴JAVA這裡需要一個新的物件。當我們需要一個新的物件的時候,為了保險起見,自己new一個出來,比如鎖。
來源:https://www.cnblogs.com/ShepherdInJVM/p/9873307.html