包裝類型
Java語言是一個面向對象的語言,但是Java中的基本數據類型卻是不面向對象的,這在實際使用時存在很多的不便,為了解決這個不足,在設計類時為每個基本數據類型設計了一個對應的類進行代表,這樣八個和基本數據類型對應的類統稱為包裝類(Wrapper Class)。
包裝類均位於java.lang包,包裝類和基本數據類型的對應關系如下表所示
在這八個類名中,除了Integer和Character類以后,其它六個類的類名和基本數據類型一致,只是類名的第一個字母大寫即可。
為什么需要包裝類
很多人會有疑問,既然Java中為了提高效率,提供了八種基本數據類型,為什么還要提供包裝類呢?
這個問題,其實前面已經有了答案,因為Java是一種面向對象語言,很多地方都需要使用對象而不是基本數據類型。比如,在集合類中,我們是無法將int 、double等類型放進去的。因為集合的容器要求元素是Object類型。
為了讓基本類型也具有對象的特征,就出現了包裝類型,它相當於將基本類型“包裝起來”,使得它具有了對象的性質,並且為其添加了屬性和方法,豐富了基本類型的操作。
3、拆箱與裝箱
在Java SE5之前,要進行裝箱,可以通過以下代碼:
Integer i = new Integer(10);
4、自動拆箱與自動裝箱
在Java SE5中,為了減少開發人員的工作,Java提供了自動拆箱與自動裝箱功能。
自動裝箱: 就是將基本數據類型自動轉換成對應的包裝類。
自動拆箱:就是將包裝類自動轉換成對應的基本數據類型。
Integer i =10; //自動裝箱
int b= i; //自動拆箱
Integer i=10 可以替代 Integer i = new Integer(10);,這就是因為Java幫我們提供了自動裝箱的功能,不需要開發者手動去new一個Integer對象。
5、自動裝箱與自動拆箱的實現原理
既然Java提供了自動拆裝箱的能力,那么,我們就來看一下,到底是什么原理,Java是如何實現的自動拆裝箱功能。
我們有以下自動拆裝箱的代碼:
public static void main(String[]args){
Integer integer=1; //裝箱
int i=integer; //拆箱
}
對以上代碼進行反編譯后可以得到以下代碼:
public static void main(String[]args){
Integer integer=Integer.valueOf(1);
int i=integer.intValue();
}
從上面反編譯后的代碼可以看出,int的自動裝箱都是通過Integer.valueOf()方法來實現的,Integer的自動拆箱都是通過integer.intValue來實現的。如果讀者感興趣,可以試着將八種類型都反編譯一遍 ,你會發現以下規律:
自動裝箱都是通過包裝類的valueOf()方法來實現的.自動拆箱都是通過包裝類對象的xxxValue()來實現的。
6、哪些地方會自動拆裝箱
我們了解過原理之后,在來看一下,什么情況下,Java會幫我們進行自動拆裝箱。前面提到的變量的初始化和賦值的場景就不介紹了,那是最簡單的也最容易理解的。
我們主要來看一下,那些可能被忽略的場景。
場景一、將基本數據類型放入集合類
我們知道,Java中的集合類只能接收對象類型,那么以下代碼為什么會不報錯呢?
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i ++){
li.add(i);
}
將上面代碼進行反編譯,可以得到以下代碼:
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
li.add(Integer.valueOf(i));
}
以上,我們可以得出結論,當我們把基本數據類型放入集合類中的時候,會進行自動裝箱。
場景二、包裝類型和基本類型的大小比較
有沒有人想過,當我們對Integer對象與基本類型進行大小比較的時候,實際上比較的是什么內容呢?看以下代碼:
Integer a=1;
System.out.println(a==1?"等於":"不等於");
Boolean bool=false;
System.out.println(bool?"真":"假");
對以上代碼進行反編譯,得到以下代碼:
Integer a=1;
System.out.println(a.intValue()==1?"等於":"不等於");
Boolean bool=false;
System.out.println(bool.booleanValue?"真":"假");
可以看到,包裝類與基本數據類型進行比較運算,是先將包裝類進行拆箱成基本數據類型,然后進行比較的。
場景三、包裝類型的運算
有沒有人想過,當我們對Integer對象進行四則運算的時候,是如何進行的呢?看以下代碼:
Integer i = 10;
Integer j = 20;
System.out.println(i+j);
反編譯后代碼如下:
Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(20);
System.out.println(i.intValue() + j.intValue());
我們發現,兩個包裝類型之間的運算,會被自動拆箱成基本類型進行。
場景四、三目運算符的使用
這是很多人不知道的一個場景,作者也是一次線上的血淋淋的Bug發生后才了解到的一種案例。看一個簡單的三目運算符的代碼:
boolean flag = true;
Integer i = 0;
int j = 1;
int k = flag ? i : j;
很多人不知道,其實在int k = flag ? i : j;這一行,會發生自動拆箱。反編譯后代碼如下:
boolean flag = true;
Integer i = Integer.valueOf(0);
int j = 1;
int k = flag ? i.intValue() : j;
這其實是三目運算符的語法規范:當第二,第三位操作數分別為基本類型和對象時,其中的對象就會拆箱為基本類型進行操作。
因為例子中,flag ? i : j;片段中,第二段的i是一個包裝類型的對象,而第三段的j是一個基本類型,所以會對包裝類進行自動拆箱。如果這個時候i的值為null,那么久會發生NPE。(自動拆箱導致空指針異常)
場景五、函數參數與返回值
這個比較容易理解,直接上代碼了:
//自動拆箱
public int getNum1(Integer num) {
return num;
}
//自動裝箱
public Integer getNum2(int num) {
return num;
}
7、自動拆裝箱與緩存
Java SE的自動拆裝箱還提供了一個和緩存有關的功能,我們先來看以下代碼,猜測一下輸出結果:
public static void main(String... strings) {
Integer integer1 = 3;
Integer integer2 = 3;
if (integer1 == integer2)
System.out.println("integer1 == integer2");
else
System.out.println("integer1 != integer2");
Integer integer3 = 300;
Integer integer4 = 300;
if (integer3 == integer4)
System.out.println("integer3 == integer4");
else
System.out.println("integer3 != integer4");
}
我們普遍認為上面的兩個判斷的結果都是false。雖然比較的值是相等的,但是由於比較的是對象,而對象的引用不一樣,所以會認為兩個if判斷都是false的。
在Java中,==比較的是對象應用,而equals比較的是值。
所以,在這個例子中,不同的對象有不同的引用,所以在進行比較的時候都將返回false。奇怪的是,這里兩個類似的if條件判斷返回不同的布爾值。
上面這段代碼真正的輸出結果:
integer1 == integer2
integer3 != integer4
原因就和Integer中的緩存機制有關。在Java 5中,在Integer的操作上引入了一個新功能來節省內存和提高性能。整型對象通過使用相同的對象引用實現了緩存和重用。
適用於整數值區間-128 至 +127。
只適用於自動裝箱。使用構造函數創建對象不適用。
具體的代碼實現可以閱讀Java中整型的緩存機制一文,這里不再闡述。
我們只需要知道,當需要進行自動裝箱時,如果數字在-128至127之間時,會直接使用緩存中的對象,而不是重新創建一個對象。
其中的javadoc詳細的說明了緩存支持-128到127之間的自動裝箱過程。最大值127可以通過-XX:AutoBoxCacheMax=size修改。
實際上這個功能在Java 5中引入的時候,范圍是固定的-128 至 +127。后來在Java 6中,可以通過java.lang.Integer.IntegerCache.high設置最大值。
這使我們可以根據應用程序的實際情況靈活地調整來提高性能。到底是什么原因選擇這個-128到127范圍呢?因為這個范圍的數字是最被廣泛使用的。 在程序中,第一次使用Integer的時候也需要一定的額外時間來初始化這個緩存。
在Boxing Conversion部分的Java語言規范(JLS)規定如下:
如果一個變量p的值是:
-128至127之間的整數(§3.10.1)
true 和 false的布爾值 (§3.10.3)
‘\u0000’至 ‘\u007f’之間的字符(§3.10.4)
范圍內的時,將p包裝成a和b兩個對象時,可以直接使用a==b判斷a和b的值是否相等。
8、自動拆裝箱帶來的問題
當然,自動拆裝箱是一個很好的功能,大大節省了開發人員的精力,不再需要關心到底什么時候需要拆裝箱。但是,他也會引入一些問題。
包裝對象的數值比較,不能簡單的使用==,雖然-128到127之間的數字可以,但是這個范圍之外還是需要使用equals比較。
前面提到,有些場景會進行自動拆裝箱,同時也說過,由於自動拆箱,如果包裝類對象為null,那么自動拆箱時就有可能拋出NPE。
如果一個for循環中有大量拆裝箱操作,會浪費很多資源。
常見筆試題:
Integer i1 =59;
int i2 = 59;
Integer i3 = Integer.valueOf(59);
Integer i4 = new Integer(59);
System.out.println(i1 == i2); true:包裝類和基本類型比較時,包裝類自動拆箱為基本類型
System.out.println(i1 == i3); true:數值59 在-128到127之間,上文所說的緩存,因此為true,若數值不在-128到127之間則為false
System.out.println(i1 == i4); false:引用類型比較地址值,地址值不同
System.out.println(i2 == i3); true:i1的源碼是i3,i2和i3比較結果和i2與i1比較結果相同,包裝類和基本類型比較時自動拆箱
System.out.println(i2 == i4); true:包裝類和基本類型比較時自動拆箱
System.out.println(i3 == i4);同i1 == i4