包裝類
Java有八種基本類型,每種基本類型都有一個對應的包裝類。
包裝類是什麼呢?它是一個類,內部有一個實例變量,保存對應的基本類型的值,這個類一般還有一些靜態方法、靜態變量和實例方法,以方便對數據進行操作。
Java中,基本類型和對應的包裝類如下表所示:
基本類型 包裝類 boolean包裝類也都很好記,除了Integer和Character外,其他類名稱與基本類型基本一樣,只是首字母大寫。
包裝類有什麼用呢?Java中很多代碼(比如後續文章介紹的集合類)只能操作對象,為了能操作基本類型,需要使用其對應的包裝類,另外,包裝類提供了很多有用的方法,可以方便對數據的操作。
包裝類的基本使用是比較簡單的,但我們不僅會介紹其基本用法,還會介紹一些平時用的相對較少的功能,同時剖析其實現代碼,內容比較多,我們會分三節來介紹,本節主要介紹各個包裝類的基本用法及其共同點,後兩節我們會進一步介紹高級功能,並剖析實現代碼。
讓我們逐步來介紹。
基本類型和包裝類
我們先來看各個基本類型和其包裝類是如何轉換的,我們直接看代碼:
Boolean
boolean b1 = false; Boolean bObj = Boolean.valueOf(b1); boolean b2 = bObj.booleanValue();
Byte
byte b1 = 123; Byte byteObj = Byte.valueOf(b1); byte b2 = byteObj.byteValue();
Short
short s1 = 12345; Short sObj = Short.valueOf(s1); short s2 = sObj.shortValue();
Integer
int i1 = 12345; Integer iObj = Integer.valueOf(i1); int i2 = iObj.intValue();
Long
long l1 = 12345; Long lObj = Long.valueOf(l1); long l2 = lObj.longValue();
Float
float f1 = 123.45f; Float fObj = Float.valueOf(f1); float f2 = fObj.floatValue();
Double
double d1 = 123.45; Double dObj = Double.valueOf(d1); double d2 = dObj.doubleValue();
Character
char c1 = 'A'; Character cObj = Character.valueOf(c1); char c2 = cObj.charValue();
這些代碼結構是類似的,每種包裝類都有一個靜態方法valueOf(),接受基本類型,返回引用類型,也都有一個實例方法xxxValue()返回對應的基本類型。
將基本類型轉換為包裝類的過程,一般稱為"裝箱",而將包裝類型轉換為基本類型的過程,則稱為"拆箱"。裝箱/拆箱寫起來比較啰嗦,Java 1.5以後引入了自動裝箱和拆箱技術,可以直接將基本類型賦值給引用類型,反之亦可,如下代碼所示:
Integer a = 100; int b = a;
自動裝箱/拆箱是Java編譯器提供的能力,背後,它會替換為調用對應的valueOf()/xxxValue(),比如說,上面的代碼會被Java編譯器替換為:
Integer a = Integer.valueOf(100); int b = a.intValue();
每種包裝類也都有構造方法,可以通過new創建,比如說:
Integer a = new Integer(100); Boolean b = new Boolean(true); Double d = new Double(12.345); Character c = new Character('馬');
那到底應該用靜態的valueOf方法,還是使用new呢?一般建議使用valueOf。new每次都會創建一個新對象,而除了Float和Double外的其他包裝類,都會緩存包裝類對象,減少需要創建對象的次數,節省空間,提升性能,後續我們會分析其具體代碼。
重寫Object方法
所有包裝類都重寫了Object類的如下方法:
boolean equals(Object obj) int hashCode() String toString()
我們逐個來看下。
equals
equals用於判斷當前對象和參數傳入的對象是否相同,Object類的默認實現是比較地址,對於兩個變量,只有這兩個變量指向同一個對象時,equals才返回true,它和比較運算符(==)的結果是一樣的。
但,equals應該反映的是對象間的邏輯相等關系,所以這個默認實現一般是不合適的,子類需要重寫該實現。所有包裝類都重寫了該實現,實際比較用的是其包裝的基本類型值,比如說,對於Long類,其equals方法代碼是:
public boolean equals(Object obj) { if (obj instanceof Long) { return value == ((Long)obj).longValue(); } return false; }
對於Float,其實現代碼為:
public boolean equals(Object obj) { return (obj instanceof Float) && (floatToIntBits(((Float)obj).value) == floatToIntBits(value)); }
Float有一個靜態方法floatToIntBits(),將float的二進制表示看做int。需要注意的是,只有兩個float的二進制表示完全一樣的時候,equals才會返回true。在第5節的時候,我們提到小數計算是不精確的,數學概念上運算結果一樣,但計算機運算結果可能不同,比如說,看下面代碼:
Float f1 = 0.01f; Float f2 = 0.1f*0.1f; System.out.println(f1.equals(f2)); System.out.println(Float.floatToIntBits(f1)); System.out.println(Float.floatToIntBits(f2));
輸出為:
false 1008981770 1008981771
也就是,兩個浮點數不一樣,將二進制看做整數也不一樣,相差為1。
Double的equals方法與Float類似,它有一個靜態方法doubleToLongBits,將double的二進制表示看做long,然後再按long比較。
hashCode
hashCode返回一個對象的哈希值,哈希值是一個int類型的數,由對象中一般不變的屬性映射得來,用於快速對對象進行區分、分組等。一個對象的哈希值不能變,相同對象的哈希值必須一樣。不同對象的哈希值一般應不同,但這不是必須的,可以有不同對象但哈希值相同的情況。
比如說,對於一個班的學生對象,hashCode可以是學生的出生月日,出生日期是不變的,不同學生生日一般不同,分布比較均勻,個別生日相同的也沒關系。
hashCode和equals方法聯系密切,對兩個對象,如果equals方法返回true,則hashCode也必須一樣。反之不要求,equal返回false時,hashCode可以一樣,也可以不一樣,但應該盡量不一樣。hashCode的默認實現一般是將對象的內存地址轉換為整數,子類重寫equals時,也必須重寫hashCode。之所以有這個規定,是因為Java API中很多類依賴於這個行為,尤其是集合中的一些類。
包裝類都重寫了hashCode,根據包裝的基本類型值計算hashCode,對於Byte, Short, Integer, Character,hashCode就是其內部值,代碼為:
public int hashCode() { return (int)value; }
對於Boolean,hashCode代碼為:
public int hashCode() { return value ? 1231 : 1237; }
根據基類類型值返回了兩個不同的數,為什麼選這兩個值呢?它們是質數,即只能被1和自己整除的數,後續我們會講到,質數比較好,但質數很多,為什麼選這兩個呢,這個就不得而知了,大概是因為程序員對它們有特殊的偏好吧。
對於Long,hashCode代碼為:
public int hashCode() { return (int)(value ^ (value >>> 32)); }
是高32位與低32位進行位異或操作。
對於Float,hashCode代碼為:
public int hashCode() { return floatToIntBits(value); }
與equals方法類似,將float的二進制表示看做了int。
對於Double,hashCode代碼為:
public int hashCode() { long bits = doubleToLongBits(value); return (int)(bits ^ (bits >>> 32)); }
與equals類似,將double的二進制表示看做long,然後再按long計算hashCode。
關於equals和hashCode,我們還會在後續的章節中碰到,並進行進一步說明。
toString
每個包裝類也都重寫了toString方法,返回對象的字符串表示,這個一般比較自然,我們就不贅述了。
Comparable
每個包裝類也都實現了Java API中的Comparable接口,Comparable接口代碼如下:
public interface Comparable<T> { public int compareTo(T o); }
<T>是泛型語法,我們後續文章介紹,T表示比較的類型,由實現接口的類傳入。接口只有一個方法compareTo,當前對象與參數對象進行比較,在小於、等於、大於參數時,應分別返回-1,0,1。
各個包裝類的實現基本都是根據基本類型值進行比較,不再贅述。對於Boolean,false小於true。對於Float和Double,存在和equals一樣的問題,0.01和0.1*0.1相比的結果並不為0。
包裝類和String
除了toString方法外,包裝類還有一些其他與String相關的方法。
除了Character外,每個包裝類都有一個靜態的valueOf(String)方法,根據字符串表示返回包裝類對象,如:
Boolean b = Boolean.valueOf("true"); Float f = Float.valueOf("123.45f");
也都有一個靜態的parseXXX(String)方法,根據字符串表示返回基本類型值,如:
boolean b = Boolean.parseBoolean("true"); double d = Double.parseDouble("123.45");
都有一個靜態的toString()方法,根據基本類型值返回字符串表示,如:
System.out.println(Boolean.toString(true)); System.out.println(Double.toString(123.45));
輸出:
true 123.45
對於整數類型,字符串表示除了默認的十進制外,還可以表示為其他進制,如二進制、八進制和十六進制,包裝類有靜態方法進行相互轉換,比如:
System.out.println(Integer.toBinaryString(12345)); //輸出2進制 System.out.println(Integer.toHexString(12345)); //輸出16進制 System.out.println(Integer.parseInt("3039", 16)); //按16進制解析
輸出為:
11000000111001 3039 12345
常用常量
包裝類中除了定義靜態方法和實例方法外,還定義了一些靜態變量。
Boolean類型:
public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false);
所有數值類型都定義了MAX_VALUE和MIN_VALUE,表示能表示的最大/最小值,比如,對Integer:
public static final int MIN_VALUE = 0x80000000; public static final int MAX_VALUE = 0x7fffffff;
Float和Double還定義了一些特殊數值,比如正無窮、負無窮、非數值,如Double類:
public static final double POSITIVE_INFINITY = 1.0 / 0.0; public static final double NEGATIVE_INFINITY = -1.0 / 0.0; public static final double NaN = 0.0d / 0.0;
Number
六種數值類型包裝類有一個共同的父類Number,Number是一個抽象類,它定義了如下方法:
byte byteValue() short shortValue() int intValue() long longValue() float floatValue() double doubleValue()
通過這些方法,包裝類實例可以返回任意的基本數值類型。
不可變性
包裝類都是不可變類,所謂不可變就是,實例對象一旦創建,就沒有辦法修改了。這是通過如下方式強制實現的:
為什麼要定義為不可變類呢?不可變使得程序可以更為簡單安全,因為不用操心數據被意外改寫的可能了,可以安全的共享數據,尤其是在多線程的環境下。關於線程,我們後續文章介紹。
小結
本節介紹了包裝類的基本用法,基本類型與包裝類的相互轉換、自動裝箱/拆箱、重寫的Object方法、Comparable接口、與String的相互轉換、常用常量、Number父類,以及包裝類的不可變性。從日常基本使用來說,除了Character外,其他類介紹的內容基本就夠用了。
但Integer和Long中有一些關於位操作的方法,我們還沒有介紹,Character中的大部分方法我們也都沒介紹,它們的一些實現原理我們也沒討論,讓我們在接下來的兩節中繼續探索。
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。
-----------
更多好評原創文章
計算機程序的思維邏輯 (1) - 數據和變量
計算機程序的思維邏輯 (5) - 小數計算為什麼會出錯?
計算機程序的思維邏輯 (6) - 如何從亂碼中恢復 (上)?
計算機程序的思維邏輯 (8) - char的真正含義
計算機程序的思維邏輯 (12) - 函數調用的基本原理
計算機程序的思維邏輯 (17) - 繼承實現的基本原理
計算機程序的思維邏輯 (18) - 為什麼說繼承是把雙刃劍
計算機程序的思維邏輯 (19) - 接口的本質
計算機程序的思維邏輯 (20) - 為什麼要有抽象類?
計算機程序的思維邏輯 (21) - 內部類的本質
計算機程序的思維邏輯 (23) - 枚舉的本質
計算機程序的思維邏輯 (24) - 異常 (上)
計算機程序的思維邏輯 (25) - 異常 (下)