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節中引用了一些轉型規則,應用這些規則可以很容地解決常見的轉型問題。