小白學Java:包裝類
學習了許久的Java,我們知道Java是一種面向對象的語言,萬物皆對象。但是我們之前在說到Java基本數據類型的時候,由於處理對象需要額外的系統開銷,於是出於對性能的考慮,基本數據類型並不做為對象使用。
既然是面向對象的,在Java中許多方法需要把對象作為參數,但是基本類型變量身上沒有任何方法和屬性,於是Java提供了一個簡單的方法,就是為每一個基本數據類型類型都配套提供一個包裝類型,我們便可以在兩者之間來回反復地橫跳。
包裝類的繼承關系
先看一波包裝類型的繼承圖:
數值類型都直接繼承於父類Number類,非數值類型Character和Boolean直接繼承於Object類。
除此之外,包裝類型的名字也非常好記,除了int->Integer
和char->Character
兩個比較特殊之外,其他都是基本數據類型的首字母改為大寫即可,如:byte->Byte
。
通過查看官方文檔,我們可以發現,數值類型繼承的Number類其實是一個抽象類,那么可想而知,該類中的抽象方法已經在這幾個數值類型中得到實現,看一波:
很明顯,除了最后一個serialVersionUID(這個以后再總結),其他的方法在數值型包裝類中都存在,可以通過這些方法將對象“轉換”為基本類型的數值。
創建包裝類實例
我們再來看看包裝類型的構造器,我們再查看所有包裝類之后,發現:
- 所有的包裝類型都不存在無參構造器。
- 所有包裝類的實例都是不可變的。
- 一旦創建對象后,它們的內部值就不能進行改變。
在JDK1.5之前,我們可以這樣把基本數據類型轉換為包裝類對象,這個過程也叫做裝箱,當然反向的過程叫做拆箱:
Integer i1 = new Integer(5);//5
Integer i2 = new Integer("5");//5
- 第一句調用的是傳入int類型參數的構造器,
this.value = value
,一目了然。 - 第二句調用的是傳入String類型參數的構造器,其實又是調用了靜態方法parseInt(String s,int radix):
public Integer(String s) throws NumberFormatException {
this.value = parseInt(s, 10);
}
深究一下,parse(String s,int radix)中的radix其實代表着進制信息,而我們的構造器默認讓radix為10,代表着輸出字符串s在十進制下的數,所以除了數字0-9之外,字符串中不能有其他的玩意兒,否則會拋出NumberFormatException的異常。
自動裝箱與拆箱
我們在上面說過,基本數據類型和包裝類型之間的轉換涉及到裝箱與拆箱的操作,為了簡化代碼,在JDK1.5之后,Java允許基本類型和包裝類型之間可以自動轉換。
自動裝箱
將基本類型直接賦值給對應的引用類型,編譯器在底層自動調用對應的valueOf方法。
就像下面這樣:
int i = 5;
Integer in = i;
我們利用debug調試工具設上斷點,發現在執行Integer in = i;
時,將會自動調用下面的方法:
繼續深究其底層實現,我們發現IntegerCache其實是Integer包裝類的一個內部類,我們進入IntegerCache一探究竟:
我們會發現所有的整數類型的(包括Character)包裝類里都有類似的玩意兒,所以大致運行的規則應該大致相同,在這里就總結幾點不太一樣的:
- 只有Integer包裝類才可以更改緩存大小。
- Character容量只有128。
浮點數類型包裝類並不存在緩存機制,是因為在一定的范圍內,該類型的數值並不是有限的。
看到這,我們大致就可以得出結論,整數數值類型在自動裝箱的時候會進行判斷數值的范圍,如果正好在緩存區,那么就不必創建新的對象,它們將會指向同一地址。Java中另一個例子就是我們說的字符串常量池。
所以下面很火的幾條語句,結果就很明顯了:
int num = 100;
Integer i1 = num;
Integer i2 = num;
System.out.println(i1==i2);//true
//num改為200,結果為false
Integer i1 = 100;
Integer i2 = new Integer(100);
System.out.println(i1 == i2);//false
自動拆箱
將引用類型字節賦值給對應的基本類型,編譯器在底層自動調用對應的xxxvalue方法(如intValue)。
Integer in = 5;
int i = in;
自動拆箱相對來說就稍微簡單一點了,我們還是利用debug工具,發現上面的代碼將會自動調用下面的方法
包裝類型的比較
"=="比較
int num = 100;
Integer i1 = num;
Integer i2 = num;
//都是包裝器類型的引用時,比較是否指向同一對象。
System.out.println(i1==i2);//true
Integer i1 = 128;
int i2 = 128;
//如果包含算數運算符,則底層自動拆箱,即比較數值。
System.out.println(i1 == i2);//true
Integer i3 = 1;
Integer i4 = 129;
System.out.println(i4 == i1+i3);//true
equals比較
equals比較的是同一包裝類型,即比較兩者數值是否相等
Integer i1 = 5;
Integer i2 = 5;
Integer i3 = 10;
//同一包裝類型,比較數值是否相等
System.out.println(i1.equals(i2));//true
System.out.println(i3.equals(i1+i2));//true
Long l1 = 5L;
Long l2 = 10L;
//Long與Integer比較,不是同一類型,false
System.out.println(l1.equals(i1));//false
//先自動拆箱,i1先轉為int,l轉為long,int自動類型提升轉為long,最后相等
System.out.println(l2.equals(l1+i1));//true
自動裝箱與拆箱引發的弊端
自動裝箱弊端
Integer sum = 0;
for(int i = 500;i<5000;i++){
//先自動拆箱,而后自動裝箱
sum+=i;
}
在拆箱裝箱操作之后,由於sum數值超過緩存范圍,所以會new出4500個毫無用處的實例對象,大大影響了程序的性能。所以在循環語句之前,務必聲明正確的變量類型。
自動拆箱引起的空指針
private static Integer sum;
public static void setSum(Integer num,boolean flag){
sum = (flag)?num:-1;
}
上面的代碼,當num傳入為null時,即會引發空指針異常,因為包裝類在進行算術運算時(上述是三目運算),如果數據類型不一致,將會先自動拆箱轉換成基本類型進行運算,而null如果調用了intValue()方法就會形成空指針。
改進方案:
public static void setSum(Integer num,boolean flag){
//這樣類型一致,便不會自動拆箱了
sum = (flag)?num:Integer.valueOf(-1);
}
參考鏈接: