夯實Java基礎(chǔ)系列:包裝類型

1. 基本數(shù)據(jù)類型

Java基本數(shù)據(jù)類型(也稱原生數(shù)據(jù)類型,primitive type)一共有8種。

1.1 特性

1.1.1 高性能

原生數(shù)據(jù)類型的聲明方式一般如下:

int a = 3;

這里的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數(shù)據(jù),由于大小可知,生存期可知(這些字面值定義在某個程序塊里面,程序塊退出后,字段值就消失了),出于追求速度的原因,就存在于棧中,存在棧中的數(shù)據(jù)擁有較高的存取速率,所以原生數(shù)據(jù)類型比引用類型性能更高一些。

1.1.2 可共享

另外,棧有一個很重要的特殊性,就是存在棧中的數(shù)據(jù)可以共享。比如: 我們同時定義:

int a=3; int b=3;

編譯器先處理int a = 3;首先它會在棧中創(chuàng)建一個變量為a的引用,然后查找有沒有字面值為3的地址,沒找到,就開辟一個存放3這個字面值的地址,然后將a指向3的地址。接著處理int b = 3;在創(chuàng)建完b這個引用變量后,由于在棧中已經(jīng)有3這個字面值,便將b直接指向3的地址。這樣,就出現(xiàn)了a與b同時均指向3的情況。
定義完a與b的值后,再令a = 4;那么,b不會等于4,還是等于3。在編譯器內(nèi)部,遇到時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開辟地址存放4的值;如果已經(jīng)有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。

1.2 整型

Java提供了4種整型類型,與其他語言不同的是,Java中整型的取值范圍與宿主機器無關(guān),具有跨平臺性,無論什么平臺,int都是4個字節(jié),long都是8個字節(jié)。

類型 存儲需求 取值范圍
int 4字節(jié) -2147483648?2147483647
short 2字節(jié) -32768?32768
long 8字節(jié) -9223372036854775808??9223372036854775807
byte 1字節(jié) -128?127

1.3 浮點型

Java提供了2種浮點類型

類型 存儲需求 有效整數(shù)位
float 4字節(jié) 6~7位(比int能表示的范圍?。?/td>
double 8字節(jié) 15位(比int能表示的范圍大,比long?。?/td>

默認(rèn)浮點類型采用的是double,在浮點數(shù)后加f表示float類型。

注意,當(dāng)需要得到精確的計算結(jié)果時,不要使用浮點型。主要是因為浮點數(shù)采用二進(jìn)制系統(tǒng)表示,而二進(jìn)制中無法精確地表示分?jǐn)?shù)1/10。應(yīng)使用BigDecimal或者轉(zhuǎn)換為整型進(jìn)行計算。

1.4 字符型

char類型用來表示單個字符,事實上,有些字符需要2個char才能表示。char表示的是一個碼元。
與字符串不同,char類型的字面量是用單引號括起來的。

'A' //這是一個char
"A" // 這是一個String對象

1.5 布爾類型

Java用boolean來定義布爾類型,只有兩個取值:true和false.

1.6 基本數(shù)據(jù)類型之間的轉(zhuǎn)換

當(dāng)使用兩個不同類型的數(shù)值進(jìn)行計算時,先要將兩個操作數(shù)轉(zhuǎn)換成同一類型,規(guī)則如下:

  • 如果兩個操作數(shù)中有一個是double類型,另一個操作數(shù)就會轉(zhuǎn)換為double類型
  • 否則,如果其中一個是float類型,另一個操作數(shù)將會轉(zhuǎn)換成float類型
  • 否則,如果其中一個操作數(shù)是long類型,另一個操作數(shù)就會轉(zhuǎn)換為long類型
  • 否則,兩個操作數(shù)都將是int類型(char, byte, short與int運算,都要先轉(zhuǎn)換為int)。

可以簡單地理解為,轉(zhuǎn)換的優(yōu)先級是: double > float > long > int?

而這種轉(zhuǎn)換實際上會導(dǎo)致數(shù)據(jù)失真。
比如一個較大的int數(shù)值與float數(shù)值進(jìn)行運算時,按規(guī)則會將int轉(zhuǎn)換為float,但我們知道float能表示的有效整數(shù)位僅為6到7位,如果int數(shù)值大于7位,就會失真。
如下圖所示:實線表示數(shù)值轉(zhuǎn)換不會失真的情況,虛線表示數(shù)值轉(zhuǎn)換可能會失真的情況。


image.png

當(dāng)然,也可以直接進(jìn)行強制類型轉(zhuǎn)換,比如

double a = 100.50
int b = (int)a; // b == 100

強制類型轉(zhuǎn)換,可能會導(dǎo)致數(shù)據(jù)失真。

2. 包裝類型

2.1 為什么需要包裝類

大多數(shù)情況下,我們使用Java基本數(shù)值類型進(jìn)行數(shù)值運算。那為什么還需要包裝類型呢?
一般來說,以下三種情況,必須使用包裝類型

  • 作為泛型的參數(shù)類型時,Java規(guī)定泛型的參數(shù)類型必須是引用類型
Collection<int> numbers;//不合法,編譯失敗
Collection<Integer> numbers; //合法
  • 觸發(fā)反射方法時。被觸發(fā)的反射方法中的參數(shù)必須定義為包裝類型,因為Java反射的時候會把基礎(chǔ)數(shù)據(jù)類型獲取的數(shù)據(jù)類型都變成包裝類,當(dāng)你需要調(diào)用的那個方法卻不是包裝類而是基礎(chǔ)數(shù)據(jù)類型,就會報找不到方法的異常,NoSuchMethodException。
  • 想要使用一些包裝類的特性時,比如得到類中的常量值,如Integer.MAX_VALUE,或者比如調(diào)用包裝類的一些方法進(jìn)行便利的計算時,比如調(diào)用Integer的valueOf方法將一個字符串轉(zhuǎn)換為一個整型數(shù)值。

2.2 自動拆裝箱

自動裝箱是Java編譯器自發(fā)地將原生類型轉(zhuǎn)換為包裝類型。比如,將一個int類型轉(zhuǎn)換為Integer。 自動拆箱則是反向。那么什么情況下會觸發(fā)這種自動拆裝箱呢?

2.2.1 觸發(fā)自動拆裝箱的情況

  • 以下情況,會觸發(fā)自動裝箱
    (1)將一個基本數(shù)值類型傳遞給一個接收參數(shù)為相應(yīng)包裝類型的方法時。
    比如下面的consume方法,它接收的是Integer類型的參數(shù)
public void consume(Integer value){}

當(dāng)將一個int值傳給這個方法,編譯器并不會報錯,因為編譯器自動做了裝箱操作。

int param = 100;
consume(param);

編譯器會將上述代碼編譯成類似以下形式:

int param = 100;
consume(Integer.valueOf(param));//自動裝箱

(2)將一個基本數(shù)值類型直接賦值給相應(yīng)包裝類型時。

Integer number = 100;
  • 以下情況,會觸發(fā)自動拆箱

(1)將一個包裝類型傳遞給一個接收參數(shù)為相應(yīng)基本數(shù)值類型的方法時

public void sum(int value){} //方法定義為接收int類型
sum(new Integer(100);//調(diào)用時傳入的是對應(yīng)的Integer類型

(2)對包裝類型執(zhí)行算術(shù)運算時

Integer number = new Integer(100);
number++;

(3)直接將包裝類型賦值給一個基本數(shù)值類型時

Integer number = new Integer(100);
int num = number;

之所以不會報錯,是因為編譯器自動做了相應(yīng)的拆裝箱操作。而裝箱過程是通過調(diào)用包裝器的valueOf方法實現(xiàn)的,而拆箱過程是通過調(diào)用包裝器的 xxxValue方法實現(xiàn)的。(xxx代表對應(yīng)的基本數(shù)據(jù)類型)。

2.2.2 整型包裝類的緩存機制

先來看下面的程序片斷

public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        
        System.out.println(a == b);
        
        Integer c = 300;
        Integer d = 300;
        
        System.out.println(c == d);
        
    }

輸出結(jié)果是:

true
false

為什么是這個結(jié)果呢?
我們知道,將int數(shù)值直接賦值給Integer類型,會觸發(fā)自動裝箱,也就是實際運行時,Integer a = 100會轉(zhuǎn)換為Integer a = Integer.valueOf(100);來執(zhí)行。那么,讓我們直接來查看一下Integer類的valueOf方法的實現(xiàn)源碼:

 public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

可以看到,當(dāng)包裝的int的值在IntegerCache的low和high區(qū)間內(nèi)時,會直接從IntegerCache這個緩存中讀取一個Integer對象,而不是直接創(chuàng)建一個新的Integer對象。只有不在這個緩存區(qū)間內(nèi),才會直接new一個Integer對象。這個緩沖區(qū)間是多少呢?

 private static class IntegerCache {
        static final int low = -128;
//...省略部分代碼
 // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;

可以看出,這個緩存區(qū)間是 -128 到127。所以上例中c和d的值為300,超出了這個區(qū)間,自動裝箱時是直接new出來的對象,==運算符比較兩個不同的對象,自然返回false。

2.2.3 浮點型包裝類沒有緩存機制

除了Integer類,Boolean、Byte、Short、Long和Character也都有各自的緩存區(qū)間,實現(xiàn)緩存機制。
注意,Double和Float類沒有緩存機制,我們可以看一下Double類和Float類的valueOf方法源碼

    //double原生類型自動裝箱成Double
    public static Double valueOf(double d) {
        return new Double(d);
    }

    //float原生類型自動裝箱成Float
    public static Float valueOf(float f) {
        return new Float(f);
    }

之所以使用緩存機制是因為緩存的對象都是經(jīng)常使用到的(如字符、-128至127之間的數(shù)字),防止每次自動裝箱都創(chuàng)建一次對象的實例。
而double、float是浮點型的,沒有特別的熱的數(shù)據(jù)(比如在某個范圍內(nèi)的整型數(shù)值的個數(shù)是有限的,而浮點數(shù)卻是不是有限的),緩存效果沒有其它幾種類型使用效率高。

3. 原生數(shù)據(jù)類型與包裝類型的區(qū)別及注意點

3.1 原生數(shù)據(jù)類型與包裝類型的區(qū)別

原生數(shù)據(jù)類型與包裝類型主要有三個區(qū)別。
(1)原生數(shù)據(jù)類型是值類型,只是用來表示值的,它是存在方法區(qū)中的,相同值的原生數(shù)據(jù)類型共享同一個內(nèi)存空間。而包裝類型是引用類型,一個包裝類型除了表示值,還可以表示一個內(nèi)存空間。換句話說,兩個值相同的包裝類型對象,也許是存在不同的內(nèi)存空間的。
(2)包裝類型可能存在null的情況。一個包裝類型如果只定義,未初始化或賦值,則默認(rèn)就是null的。
(3)原生數(shù)據(jù)類型比包裝類型在時間和空間上擁有更高的性能。

3.2 盡量使用原生數(shù)據(jù)類型,避免使用包裝類型

來看以下程序片斷:

    public static void main(String[] args) {
        Long sum = 0L;
        for(long i = 0;i <Integer.MAX_VALUE;i++) {
            sum += i ;
        }
        System.out.println(sum);
    }

這個程序執(zhí)行起來會非常慢,只因為它寫錯了一個小小的地方:Long sum = 0L;
為什么呢?sum定義為一個Long類型的包裝對象,那么在接下來的for循環(huán)中,當(dāng)執(zhí)行sum +=i;因為i是long類型的,所以會先將sum進(jìn)行自動拆箱,以便于與i進(jìn)行算術(shù)運算,然后將結(jié)果賦予sum時,又得進(jìn)行自動裝箱,當(dāng)超出Long類型的緩存區(qū)間時,就會不斷地在堆內(nèi)創(chuàng)建新的Long對象。所以這個程序非常慢,僅僅只是因為將sum定義為了Long,如果將sum定義為long時,問題就解決了。
所以我們應(yīng)該盡量使用原生數(shù)據(jù)類型,在萬不得已的情況下,不要使用包裝類型。

3.3 包裝類對象未初始化導(dǎo)致NullPointerException問題

我們來看下面的例子:

public class WrapperMess{
  
    static Integer i ;
    public static void main(String[] args){
       if(i==0){
            System.out.println(" i is 0");
       } 
   }
}

程序會不會輸出"i is 0"呢?答案是不會,而且還會拋出NullPointerException。
當(dāng)程序執(zhí)行到 if(i==0)時,因為要將Integer對象i與int值0進(jìn)行比較,會進(jìn)行自動拆箱。而Integer i還未初始化,它現(xiàn)在的值是null,當(dāng)對一個null對象進(jìn)行拆箱操作,即調(diào)用Integer的intValue方法時,就會拋出NullPointerException了。

3.4 使用==與equals方法的注意點

當(dāng)我們想要判斷兩個包裝類型對象的值是否相等時,不要使用==,而應(yīng)該使用equals。
因為==判斷的是對象的地址,我們知道兩個包裝類型對象即使值相等,也可能是存在堆中不同的空間的。
而包裝類都重寫了Object類的equals方法,直接比較所包裝的值。比如Integer的equals方法源碼:

 public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

參考資料:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,644評論 18 399
  • 第一類:邏輯型boolean 第二類:文本型char 第三類:整數(shù)型(byte、short、int、long) c...
    Jasonme閱讀 1,189評論 0 4
  • 流行歌曲里面有很多抒寫愛情的,其中很多都與失戀有關(guān),香雪就特別喜歡聽這樣的歌曲,每每聽到這樣的歌曲我總是嘲笑她多愁...
    醞錦閱讀 889評論 4 14
  • 你們就是白鶴少年? 貴妃如是對白龍與丹龍問到。貴妃問起白龍的身世,丹龍為白龍略作掩飾,白龍卻毫不避諱說出自己被生父...
    沒有蛀牙的奧斯卡閱讀 2,759評論 0 0

友情鏈接更多精彩內(nèi)容