1. 程式人生 > 其它 >Java數字型別隱式轉換

Java數字型別隱式轉換

隱式轉換關係

精度丟失

上圖中虛線表示轉換過程中存在精度丟失問題,因為與其它資料型別的十進位制直接轉換為二進位制不同,float、double有其獨特的資料結構,如下所示:

型別 符號位 指數 尾數 長度
float 1 8 23 32
double 1 11 52 64

float舉例分析

以float為例進行分析,例如:12.15623,執行以下步驟:

  1. 對整數位轉化為二進位制:除二取餘,倒序排列,即整數依次除二直到商為0或1的時候結束,然後將餘數倒序寫出,不足位高位補0
12/2    0   低位
6/2     0
3/2     1
1/2     1   高位
12 = 0000 1100B
  1. 對小數位轉換為二進位制:乘二取整,順序排列,即小數位*2,取計算結果的整數位排列,直到小數部分為0,不足位進行低位補0
0.15623*2 = 0.31246   0
0.31246*2 = 0.62492   0
0.62492*2 = 1.24984   1
0.24984*2 = 0.49968   0
0.49968*2 = 0.99936   0
0.99936*2 = 1.99872   1
0.99872*2 = 1.99744   1
0.99744*2 = 1.99488   1 
...
0.15623 = 0.00100111111111101011000001110100101001110111001

存在無限迴圈或者小數位過多的情況,例如:float尾數位23bit表示,這也是浮點數無法精確表示的原因之一

  1. 12.15623的完整二進位制為:1100.00100111...,可通過線上進位制轉換驗證
  2. 根據IEEE 754標準,其實際表示形式為:$ v = (-1)^{sign} \times M \times 2^E $
  • M:尾數,M = 1 + f,其中$ 0 \le f < 1 \(,f的二進位制表示為\) 0.f_{n-1}...f_1f_0 \(,因此M的二進位制可以看做是\) 1.f_{n-1}...f_1f_0 \(,可通過對12.15623的二進位制進行位移使尾數M的範圍保持在\) 1 \le M <2 $,向右移3位變為:1.10000100111...。實際就是轉化為二進位制科學計數,由於尾數最高位總是為1,所以高位直接隱去
    ,12.15623的尾數即為10000100111111111101011,float用23位表示,double用52位表示,所以才有了精度的不同
  • E:指數,E = 位移數(右移取整數,反之取負數,也可以理解為二進位制計數法的指數) + 127,因此,12.15623的指數E = 3 + 127 = 130,轉化為二進位制為:10000010。float指數位有8位,但實際指數需要減去127,也就是$ 2^7 $ = 128,所以float的最大範圍是$ -2^{128} \sim 2^{128} $
  • 根據float的儲存結構:1位符號位+8位指數位+23位尾數位,12.15623實際儲存為:
    \(\color{#4285f2}{0}\color{#ea4335}{1000001}\quad\color{#ea4335}{0}\color{#34a853}{1000010}\quad\color{#34a853}{01111111}\quad\color{#34a853}{11101011}\)

結果驗證

反向推導

\(\color{#4285f2}{0}\color{#ea4335}{1000001}\quad\color{#ea4335}{0}\color{#34a853}{1000010}\quad\color{#34a853}{01111111}\quad\color{#34a853}{11101011}\)

  • 尾數M高位為1,補1後 = 1.1000010 01111111 11101011
  • 指數E = 1000 0010B = 130,位移數 = 130 -127 = 3
  • 實際尾數M左移3位即為實際二進位制: 1100.0010 01111111 11101011
  • 二進位制轉換為十進位制,過程如下:
    \(12+(1/8+1/64+1/128+1/256+1/512+1/1024+1/2048+1/4096+1/8192+1/16384+1/32768+1/131072+1/524288+1/1048576)\)
    \(value = 12+0.15622997283935547 = 12.15622997283935547\)

公式推導

\(value = (-1)^{sign} \times (1+\sum_{i=1}^{23} b_{23-i}2^{-i})\times2^{e-127}\)

求和展開後如下:

\(value = (-1)^{sign} \times (1+b_{22}2^{-1}+b_{21}2^{-2}+...+b_{0}2^{-23}) \times2^{e-127}\)

其中\(b_n2^{n-23}\),當尾數位為0時結果都是0,可直接忽略。因此,尾數位求和只取尾數位等於1的即可。\(\color{#4285f2}{0}\color{#ea4335}{1000001}\quad\color{#ea4335}{0}\color{#34a853}{1000010}\quad\color{#34a853}{01111111}\quad\color{#34a853}{11101011}\)代入公式為:

\(value = (-1)^0 \times (1+2^{-1}+2^{-6}+2^{-9\sim-18}+2^{-20}+2^{-22}+2^{-23}) \times2^{130-127}\)

\(value = 1 \times (1+1/2+1/64+1/512+1/1024+1/2048+1/4096+1/8192+1/16384+1/32768+1/65536+1/131072+1/262144+1/1048576+1/4194304+1/8388608) \times2^3\)

\(value = 1 \times 1.5195287466049194 \times 8 = 12.156229972839355\)

精度的計算

還是以float舉例,尾數一共23位,加上隱藏位1,實際有24位,所能表示的取值範圍\([0,2^{24}-1]\),最大值為16777215,轉化為二進位制科學計數法為\(1.1111111 11111111 1111111 * 2^{23}\)比這個大的數小數位已經是24位了,儲存時超出的位數就被捨棄掉了。因此,可以得出以下結論:

  • 小於16777215的整數都可以精確表示
  • 小於16777215的非整數可能不能精確表示,因為小數轉化為二進位制位數不能保證,且可能存在無限迴圈的情況。如果是是16777215以內的整數乘以\(2^{-n}\)即可精確表示。例如:(16777214*1/2048=8191.9990234375,其二進位制為\(1.1111111111111111111111*2^{12}\),儲存不會出現精度丟失,但是java中列印為:8191.999,預設顯示8位)
  • 超出16777215的整數可能不能精確表示,即存在可以精確表示的。如果剛好是\(2^n\),或者是16777215以內的整數乘以\(2^n\),這樣超出的位數都是0,例如:33554432其二進位制科學計數法為\(1.0000000 00000000 00000000*2^{25}\)
  • 超出16777215的非整數不能精確表示,小數位肯定被丟棄了

最終結論:精度是由尾數的位數來決定的,浮點數在記憶體中是按科學計數法來儲存的,其整數部分始終是1,由於它是不變的,故不能對精度造成影響。數學領域的精度一般指有效位數,即十進位制位數。因此,結論如下:

  • float:2^23 = 8388608,一共七位,最多能有7位有效數字,能絕對保證前6位,第7位可能存在舍入的情況,即float的精度為6~7位有效數字
  • double:2^52 = 4503599627370496,一共16位,同理,double的精度為15~16位

疑問

印象中float的整數位+小數位一共8位,例如:

- (float)10/3=3.3333333
- (float)1/3=0.33333334

再看以下程式碼:

System.out.println(0.100000024f);
System.out.println(0.100000025f);
System.out.println(0.100000026f);
System.out.println(0.100000027f);
System.out.println(0.100000028f);

輸出結果如下:

0.100000024
0.100000024
0.100000024
0.100000024
0.10000003

0.100000024~0.100000027輸出都是0.100000024,小數位9位,0.100000028就變成0.10000003,小數位8位,為什麼??

  1. 先轉化為對應的二進位制:
0.100000024=0.000110011001100110011010000 00000101011011110000100001011
0.100000025=0.000110011001100110011010000 00100111110010110010000000101
0.100000026=0.000110011001100110011010000 01001010001001110011011111111
0.100000027=0.000110011001100110011010000 01101100100000110100111111001
0.100000028=0.000110011001100110011010000 10001110110111110110011110011
  1. 再二進位制科學計數法:
  • 0.100000024=1.10011001100110011010000*\(2^{-4}\)
  • 0.100000025=1.10011001100110011010000*\(2^{-4}\)
  • 0.100000026=1.10011001100110011010000*\(2^{-4}\)
  • 0.100000027=1.10011001100110011010000*\(2^{-4}\)
  • 0.100000028=1.10011001100110011010001*\(2^{-4}\)

由於尾數位只有23位,丟棄多餘部分,0.100000024~0.100000027的資料結構完全一樣,0.100000028由於第24位是1,捨棄時進位使23位變成了1,這就是差異部分

  1. 代入上文中公式:

0.100000024如下:
\((1+1/2+1/16+1/32+1/256+1/512+1/4096+1/8192+1/65536+1/131072+1/524288)*2^{-4}=0.10000002384185791\)
0.100000028如下:
\((1+1/2+1/16+1/32+1/256+1/512+1/4096+1/8192+1/65536+1/131072+1/524288+1/8388608)*2^{-4}=0.10000003129243851\)

到這一步,可以看出其不同,但為什麼輸出0.100000024和0.10000003呢?

觀察結果如下:

  • 0.10000002384185791比原數小,多取一位進行四捨五入,更接近原值
  • 0.10000003129243851已經比原值大了,預設取8位,丟棄多餘的位數
  • 還是一種是上文提到的8191.9990234375列印輸出為8191.999,可以精確表示,但是預設也只輸出8位

結論:

  • 精度是指有效數字,和小數位的多少沒有必然聯絡,小數位是不定的
  • float預設顯示的小數位並不代表其實際精度