一些概念
想必大家都知道Java的基礎數據類型有:char、byte、short、int、long、float、double、boolean 這幾種,與C/C++等語言不同的是,Java的基礎數據的位數是固定不變的。
????Java作為一門面向對象的語言,自然少不了對象了,因此基礎數據類型都對應存在一個基本類型封裝器類,它們的封裝器分別為:Character、Byte、Short、Integer、Long、Float、Double、Boolean。
????在JDk1.5之前,在基礎數據類型與其封裝器之間的轉化必須手動進行,其中將基本數據類型轉換為封裝器類型需要調用封裝類的靜態方法T.valueOf(),T代表封裝器類型的名稱,而將封裝器類型轉換為基本數據類型需要調用它的實例方法xValue();x代表基礎數據類型的名稱,這個過程比較繁瑣,故從JDK1.5開始,JDK提供了自動裝箱的特性,不再需要手動進行了。所謂裝箱指:將基本數據類型轉換為對應的封裝器類型,拆箱指:將封裝器類型轉換為對應的基本數據類型。eg:
Integer a = 1;//裝箱,底層實現:Integer a = Integer.valueOf(1);
int b = a; //拆箱,底層實現:int b = a.intValue();
eg:
public class PackageAndUnpackageDemo{
public static void main(String[] args) {
Integer a = 1;//裝箱,底層實現:Integer a = Integer.valueOf(1);
int b = a; //拆箱,底層實現:int b = a.intValue();
}
}
反編譯結果
從圖片中我們可以看到,裝拆箱確實是這樣實現的。
????此外,為了提高封裝器的效率,Java將常用的數據緩存起來放到封裝器對象數組里面,裝箱的時候判斷如果數據在緩存的范圍內則從緩存里面取出對象,否則將new一個出來。
Integer相關的源碼如下:
我們可以看到,當數字的范圍在low與high之間時,直接從cache對象數組中獲取,否則就new一個對象,cache的源碼如下:
Integer的cache數組與其他的有些區別是,Integer可以自定義數組的上限,而其他封裝器的cache是固定的。
封裝器的緩存的大小以及他們對應的基礎數據類型的范圍見下表:
基本類型 | 封裝器 | 字節數 | 最大值 | 最小值 | 緩存范圍 |
---|---|---|---|---|---|
byte | Byte | 1byte | 2^7 - 1 | -2^7 | -128~127 |
short | Short | 2byte | 2^15 - 1 | -2^15 | -128~127 |
char | Character | 2byte | 2^16 - 1 | 0 | 0~127 |
int | Integer | 4byte | 2^31 - 1 | -2^31 | -128~127 |
long | Long | 8byte | 2^63 - 1 | -2^63 | -128~127 |
float | Float | 4byte | 3.4e+38 | 1.4e-45 | 無緩存 |
double | Double | 8byte | 1.8e+308 | 4.9e-324 | 無緩存 |
boolean | Boolean | 1byte/4byte/不明確 | - | - | false、true |
我們注意到Boolean與Byte全部被緩存了,boolean類型沒有給出精確的定義,《Java虛擬機規范》給出了4個字節,和boolean數組1個字節的定義,具體還要看虛擬機實現是否按照規范來,所以1個字節、4個字節都是有可能的,這個在這里不做深究。
看到這里想必您現在應該能夠理解下面的一些現象吧:
public class CacheDemo{
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println(a == b);//true
Integer c = 128;
Integer d = 128;
System.out.println(c == d);//false
Integer e = -128;
Integer f = -128;
System.out.println(e == f);//true
Integer g = -129;
Integer h = -129;
System.out.println(g == h);//false
}
}
由于127與-128都在緩存數組緩存的范圍內,所以他們是直接從緩存數組中取得的對象,故引用相同,而128與-129不在該范圍內,所以是new出來的,故引用不相同。當然得到這個結果的前提條件是我們沒有指定Integer緩存數組的上限,直接使用它的默認上限127。您可以通過-Djava.lang.Integer.IntegerCache.high=128 指定上限為128,那么運行結果就有所不同了。
java CacheDemo
true
false
true
false
java -Djava.lang.Integer.IntegerCache.high=128 CacheDemo
true
true
true
false
到這里需要提醒一下大家的是,“Java中對引用類型而言 == 比較的是引用,而equals()比較的是內容”這句話其實嚴格意義上來講不準確。
我們不妨看一下對象之源Object中關于equals()的實現
public boolean equals(Object obj) {
return (this == obj);
}
OK,Object的equals()方法默認還是使用了 “==”,所以要是自定義的對象沒有Override該方法與hashCode()方法,那么比較的也是引用而不是內容了,當然Java的大部分類都重寫了該方法,但是還有一些類如StringBuffer,StringBuilder沒有重寫該方法,這點需要注意。
注意事項
空指針異常
自動的裝拆箱給我們帶來了很大的方便,大部分情況下我們都將封裝器與基礎數據類型混為一談了,但是二者之間畢竟還是存在著一些差別,在某些場景下要是不注意,自動裝拆箱這個語法糖也會給我們帶來一些意想不到的后果。
????由于引用類型的默認值是null,而不能將null賦值給基本數據類型,在拆箱的過程中可能會導致空指針異常,這個需要我們格外注意要加以判斷:
如下面的情形:
import java.util.List;
import java.util.ArrayList;
public class NullPointInUnpackageDemo{
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>(3);
list.add(0);
list.add(1);
list.add(null);
for (int n:list){
System.out.println("n="+n);
}
}
}
運行結果:
n=0
n=1
Exception in thread "main" java.lang.NullPointerException
at NullPointInUnpackageDemo.main(NullPointInUnpackageDemo.java:10)
當然例子出現空指針異常的情況我們一眼就能夠看出,但是實際開發過程可能由于疏忽或者不那么直接的拆箱行為,將會帶來意想不到的錯誤,這是我們需要格外注意的。
封裝器帶來的模糊性問題
在方法的重載某些情形,封裝器帶來的使用可能會帶來方法調用的模糊性問題,給我們帶來一些意想不到的bug,比如下面的情形:
public class BugInOverloading{
public static void test(Integer num){
System.out.println("Integer num");
}
public static void test(long num){
System.out.println("long num");
}
public static void main(String[] args) {
test(1);
}
}
您能夠一眼看出會調用哪個方法嗎?好吧,結果可能與您想象中的可能有點出入,運行結果是
long num
也就是說它居然沒有調用test(Integer num),反而直接向上轉型為long 調用了test(long num),這不很費解嗎?對于該現象的解釋是:Java為了向下兼容,保證程序的正確性,在方法重載時先不考慮自動裝拆箱,而是遵循最精確匹配的原則,找最匹配的類型,由于沒有int類型的方法可以調用,但是有long類型的方法,那么根據這個原則,int向上轉型為long了,而不是自動裝箱,這點需要注意,(ps:關于方法重載調用的問題可以參考博主另外的一篇文章 )額,時間不早了,就先這樣吧,以上便是Java基本數據類型及其封裝器的一些千絲萬縷的糾葛。
圉于博主的水平,理解可能有所偏差,還望各位大佬不吝指正!