本文參考了如下兩篇文章:
https://my.oschina.net/joymufeng/blog/139952
http://www.cnblogs.com/lwbqqyumidi/p/3700164.html
Java中,經常可以遇到類型轉換的場景,從變量的定義到復制、數值變量的計算到方法的參數傳遞、基類與派生類間的造型等,隨處可見類型轉換的身影。Java中的類型轉換在Java編碼中具有重要的作用。
首先,來了解下數據類型的基本理解:數據是用來描述數據的種類,包括其值和基於其值基礎上的可進行的操作集合。
Java中數據類型主要分為兩大類:基本數據類型和引用數據類型。
基本數據類型共有8種,分別是:布爾型boolean, 字符型char和數值型byte/short/int/long/float/double。由於字符型char所表示的單個字符與Ascii碼中相應整形對應,因此,有時也將其划分到數值型中。引用類型具體可分為:數組、類和接口。因此java中類型的轉化分為基本數據類型的轉換和引用數據類型的轉換,本文將針對基本數據類型的轉換進行總結.
1.基本數據類型的類型轉換
數據類型 | 所占字節 |
boolean | 未定 |
byte | 1字節 |
char | 2字節 |
short | 2字節 |
int | 4字節 |
long | 8字節 |
float | 4字節 |
double | 8字節 |
從上表可以看出java中各種數據類型所占空間的大小. 在java中整數的默認數據類型是int, 例如數字4, 小數的默認數字類型是double, 例如3.12. 當float a = 3.12時會報錯, 因為3.12的默認數據類型是double, 我們需要使用如下的賦值方法:
第一種方法在3.12后面加了一個F, 告訴編譯器這是一個float的數. 第二種方法對3.12進行了強制的類型轉換. 接下來我們仔細分析一下java中的類型轉換問題.
基本數據類型中,布爾類型boolean占有一個字節,由於其本身所代碼的特殊含義,boolean類型與其他基本類型不能進行類型的轉換(既不能進行自動類型的提升,也不能強制類型轉換), 否則,將編譯出錯。
a. 基本數據類型中類型的自動提升
數值類型在內存中直接存儲其本身的值,對於不同的數值類型,內存中會分配相應的大小去存儲。如:byte類型的變量占用8位,int類型變量占用32位等。相應的,不同的數值類型會有與其存儲空間相匹配的取值范圍。具體如下所示:
圖中依次表示了各數值類型的字節數和相應的取值范圍。在Java中,整數類型(byte/short/int/long)中,對於未聲明數據類型的整形,其默認類型為int型。在浮點類型(float/double)中,對於未聲明數據類型的浮點型,默認為double型。
看下面的例子
是不是有點奇怪?按照上面的思路去理解,將一個int型的1000賦給一個byte型的變量a,提示出錯,但是最后一句:將一個int型的3賦給一個byte型的變量c,居然編譯正確,這是為什么呢?
原因在於:jvm在編譯過程中,對於默認為int類型的數值時,當賦給一個比int型數值范圍小的數值類型變量(在此統一稱為數值類型k,k可以是byte/char/short類型),會進行判斷,如果此int型數值超過數值類型k,那么會直接編譯出錯。因為你將一個超過了范圍的數值賦給類型為k的變量,k裝不下嘛,你有沒有進行強制類型轉換,當然報錯了。但是如果此int型數值尚在數值類型k范圍內,jvm會自定進行一次隱式類型轉換,將此int型數值轉換成類型k。如圖中的虛線箭頭。這一點有點特別,需要稍微注意下。
另外在IDEA中, 類型的判斷會在寫程序時由編輯器幫你做判斷, 而不需要到編譯的時候由編譯器來做判斷, 這也是IDEA的一個優點.
在其他情況下,當將一個數值范圍小的類型賦給一個數值范圍大的數值型變量,jvm在編譯過程中俊將此數值的類型進行了自動提升。在數值類型的自動類型提升過程中,數值精度至少不應該降低(整型保持不變,float->double精度將變高)。
如上:定義long類型的a變量時,將編譯出錯,原因在於11111111111默認是int類型,同時int類型的數值范圍是-2^31 ~ 2^31-1,因此,11111111111已經超過此范圍內的最大值,故而其自身已經編譯出錯,更談不上賦值給long型變量a了。
此時,若想正確賦值,改變11111111111自身默認的類型即可,直接改成11111111111L即可將其自身類型定義為long型。此時再賦值編譯正確。
將值為10的int型變量 z 賦值給long型變量q,按照上文所述,此時直接發生了自動類型提升, 編譯正確。
接下來,還有一個地方需要注意的是:char型其本身是unsigned型,同時具有兩個字節,其數值范圍是0 ~ 2^16-1,因為,這直接導致byte型不能自動類型提升到char,char和short直接也不會發生自動類型提升(因為負數的問題),同時,byte當然可以直接提升到short型。
b. 隱式類型轉換
上面的例子中既有隱式類型轉換, 也有強制類型轉換, 那么什么是隱式類型轉換呢?
隱式轉換也叫作自動類型轉換, 由系統自動完成.
從存儲范圍小的類型到存儲范圍大的類型.
byte ->short(char)->int->long->float->double
c. 顯示類型轉換
顯示類型轉換也叫作強制類型轉換, 是從存儲范圍大的類型到存儲范圍小的類型.
當我們需要將數值范圍較大的數值類型賦給數值范圍較小的數值類型變量時,由於此時可能會丟失精度(1講到的從int到k型的隱式轉換除外),因此,需要人為進行轉換。我們稱之為強制類型轉換。
double→float→long→int→short(char)→byte
byte a =3;編譯正確在1中已經進行了解釋。接下來將一個值為3的int型變量b賦值給byte型變量c,發生編譯錯誤。這兩種寫法之間有什么區別呢?
區別在於前者3是直接量,編譯期間可以直接進行判定,后者b為一變量,需要到運行期間才能確定,也就是說,編譯期間為以防萬一,當然不可能編譯通過了。此時,需要進行強制類型轉換。
強制類型轉換所帶來的結果是可能會丟失精度,如果此數值尚在范圍較小的類型數值范圍內,對於整型變量精度不變,但如果超出范圍較小的類型數值范圍內,很可能出現一些意外情況。
上面的例子中輸出值是 -23.
為什么結果是-23?需要從最根本的二進制存儲考慮。
233的二進制表示為:24位0 + 11101001,byte型只有8位,於是從高位開始舍棄,截斷后剩下:11101001,由於二進制最高位1表示負數,0表示正數,其相應的負數為-23。
d. 進行數學運算時的數據類型自動提升與可能需要的強制類型轉換
當進行數學運算時,數據類型會自動發生提升到運算符左右之較大者,以此類推。當將最后的運算結果賦值給指定的數值類型時,可能需要進行強制類型轉換。例如:
a+b會自動提升為int, 因此在給c賦值的時候要強制轉換成byte.
2.類型轉換中的符號擴展Sign Extension
有沒有想過這么一個問題, 當把一個byte的負數轉換為int時, 它的值是正數還是負數呢? 當把一個int強制轉為為byte, 我們能否確定轉換后數字的符號呢? 要理解這兩點, 我們首先要明白計算機中數的表示, 和java中類型轉換時進行的操作.
a. 計算機中數的表示
計算機中的數都是以補碼的形式存儲的, 最高位是符號位. 正數的補碼是它本身, 而負數的補碼是原碼按位取反后加1. 這樣我們就很清楚java中這些數據類型的范圍是怎么得到的.
例如: byte的范圍是-128 ~ 127. 為什么會有-128呢? 其實-128的二進制表示是 10000000, 這個補碼形式是不是很奇怪呢? 我們找不到一個數可以對應這樣的補碼, 其實這是-0的原碼, 那-0的補碼呢? 按位取反加1試試看, 是不是又變為00000000呢? 所以這個多出來的-0就用來表示-128了.
有了上面的表示, 我們就要問: 如何在類型擴展的時候保持數字的符號和值不變呢?
b. java中的符號擴展
1) 什么是符號擴展
符號擴展(Sign Extension)用於在數值類型轉換時擴展二進制位的長度,以保證轉換后的數值和原數值的符號(正或負)和大小相同,一般用於較窄的類型(如byte)向較寬的類型(如int)轉換。擴展二進制位長度指的是,在原數值的二進制位左邊補齊若干個符號位(0表示正,1表示負)。
舉例來說,如果用6個bit表示十進制數10,二進制碼為"00 1010",如果將它進行符號擴展為16bits長度,結果是"0000 0000 0000 1010",即在左邊補上10個0(因為10是正數,符號為0),符號擴展前后數值的大小和符號都保持不變;如果用10bits表示十進制數-15,使用“2的補碼”編碼后,二進制碼為"11 1111 0001",如果將它進行符號擴展為16bits,結果是"1111 1111 1111 0001",即在左邊補上6個1(因為-15是負數,符號為1),符號擴展前后數值的大小和符號都保持不變。
2) java中數值類型轉換的規則
這個規則是《Java解惑》總結的:如果最初的數值類型是有符號的,那么就執行符號擴展;如果是char類型,那么不管它要被轉換成什么類型,都執行零擴展。還有另外一條規則也需要記住,如果目標類型的長度小於源類型的長度,則直接截取目標類型的長度。例如將int型轉換成byte型,直接截取int型的右邊8位。
所以java在進行類型擴展時候會根據原始數據類型, 來執行符號擴展還是零擴展. 數值類型轉數值類型的符號擴展不會改變值的符號和大小.
c. 解析“多重轉型”問題
一個連續三次類型轉換的表達式如下:
1. int(32位) -> byte(8位)
-1是int型的字面量,根據“2的補碼”編碼規則,編碼結果為0xffffffff,即32位全部置1.轉換成byte類型時,直接截取最后8位,所以byte結果為0xff,對應的十進制值是-1.
2. byte(8位) -> char(16位)
由於byte是有符號類型,所以在轉換成char型(16位)時需要進行符號擴展,即在0xff左邊連續補上8個1(1是0xff的符號位),結果是0xffff。由於char是無符號類型,所以0xffff表示的十進制數是65535。
3. char(16位) -> int(32位)
由於char是無符號類型,轉換成int型時進行零擴展,即在0xffff左邊連續補上16個0,結果是0x0000ffff,對應的十進制數是65535。
d. 幾個轉型的例子
在進行類型轉換時,一定要了解表達式的含義,不能光靠感覺。最好的方法是將你的意圖明確表達出來。
在將一個char型數值c轉型為一個寬度更寬的類型時,並且不希望有符號擴展,可以如下編碼:
上文曾提到過,0xffff是int型字面量,所以在進行&操作之前,編譯器會自動將c轉型成int型,即在c的二進制編碼前添加16個0,然后再和0xffff進行&操作,所表達的意圖是強制將前16置0,后16位保持不變。雖然這個操作不是必須的,但是明確表達了不進行符號擴展的意圖。
如果需要符號擴展,則可以如下編碼:
首先將c轉換成short類型,它和char是 等寬度的,並且是有符號類型,再將short類型轉換成int類型時,會自動進行符號擴展,即如果short為負數,則在左邊補上16個1,否則補上16個0.
如果在將一個byte數值b轉型為一個char時,並且不希望有符號擴展,那么必須使用一個位掩碼來限制它:
(b & 0xff)的結果是32位的int類型,前24被強制置0,后8位保持不變,然后轉換成char型時,直接截取后16位。這樣不管b是正數還是負數,轉換成char時,都相當於是在左邊補上8個0,即進行零擴展而不是符號擴展。
如果需要符號擴展,則編碼如下:
此時為了明確表達需要符號擴展的意圖,注釋是必須的。
e.總結
實際上在數值類型轉換時,只有當遇到負數時才會出現問題,根本原因就是Java中的負數不是采用直觀的方式進行編碼,而是采用“2的補碼”方式,這樣的好處是加法和減法操作可以同時使用加法電路完成,但是在開發時卻會遇到很多奇怪的問題,例如(byte)128的結果是-128,即一個大的正數,截斷后卻變成了負數。3.2節中引用了一些轉型規則,應用這些規則可以很容地解決常見的轉型問題。
參考引用
1. 阮一峰-關於2的補碼
http://www.ruanyifeng.com/blog/2009/08/twos_complement.html
2. wikipedia-Sign extension
http://en.wikipedia.org/wiki/Sign_extension
3. Joshua Bloch, 陳昊鵬譯 - 《Java解惑》