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)換可能會失真的情況。

當(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;
}
參考資料: