Java 類型轉換精度問題


基本數據類型占用內存大小

最近項目中修復了一個關於類型轉換精度丟失的問題,以前對於類型轉換會丟失精度只知其然,不知其所以然,這次了解了下相關原理,也分享給大家。先來回顧一下 Java 的基本數據類型中整型與浮點型及其所占用的內存大小:

整型:

  • int:4 字節 32 位
  • long:8 字節 64 位

浮點型:

  • float:4 字節 32 位
  • double:8 字節 64 位

Java 運算時,當兩個不同類型的數進行基本運算符操作時,低精度會自動向高精度轉換,字節短的會自動向字節長的轉換。

《Java 核心技術》一書中這么歸納到:

如果兩個操作數其中有一個是 double 類型,另一個操作就會轉換為 double 類型。
否則,如果其中一個操作數是 float 類型,另一個將會轉換為 float 類型。
否則,如果其中一個操作數是 long 類型,另一個會轉換為 long 類型。
否則,兩個操作數都轉換為 int 類型。

需要注意 Java 自動轉換類型可能會帶來精度的丟失,附上一張不會丟失精度的合法類型轉換說明圖:

合法轉換

圖中實現箭頭類型轉換代表不會丟失精度,虛線箭頭類型轉換可能會丟失精度。

基本數據類型表示范圍

精度和數據類型可表示的數值大小范圍息息相關,計算機中所有數值歸根到底都是使用二進制 0、1 來組成,因此一個數據類型所占用的內存大小越大,就意味着可用的二進制位數越多,當然可表示的范圍就越大。回顧一下幾個常見的參與運算的基本數據類型的取值范圍:

int

二進制位數:32
最小值:Integer.MIN_VALUE= -2147483648 (-2 的 31 次方)
最大值:Integer.MAX_VALUE= 2147483647 (2 的 31 次方 -1)

long

二進制位數:64
最小值:Long.MIN_VALUE=-9223372036854775808 (-2 的 63 次方)
最大值:Long.MAX_VALUE=9223372036854775807 (2 的 63 次方 -1)

float

二進制位數:32
最小值:Float.MIN_VALUE=1.4E-45 (2 的 -149 次方)
最大值:Float.MAX_VALUE=3.4028235E38 (2 的 128 次方 -1)

double

二進制位數:64
最小值:Double.MIN_VALUE=4.9E-324 (2 的 -1074 次方)
最大值:Double.MAX_VALUE=1.7976931348623157E308 (2 的 1024 次方 -1)

當 long 類型的數大於 Integer.MAX_VALUE 時,long 強制轉換 int,就會出現丟失精度。轉換過程是將 long 類型數值的二進制數從低位到高位截取 32 位,再將 32 位二進制數轉為 int。

long l3 = 24696061952L; //10111000000000000000000000000000000
int c3 = (int)l3; //-1073741824
System.out.println(Integer.toBinaryString(c3)); //1000000000000000000000000000000

上面的例子中,long 類型截取 32 位后轉為 int,最高位作為符號位,1 代表負數,強轉后的 int 值為 -1073741824
類似這種不合理的強制轉換丟失的已經不僅僅是精度了。

不知道有沒有人注意到,long 類型的二進制位數是 64,float 類型的二進制位數是 32,但是 float 類型可表示范圍卻遠遠大於 long 類型。更不用提一樣是 32 位的 int 了,float 到底啥家庭啊?謎底就在內存結構中。

浮點類型數值的內存結構

與整形類型的內存結構不同,float 在內存中是這樣的:

SEEE EEEE EMMM MMMM MMMM MMMM MMMM MMMM

  • S:最高位 S 代表符號位
  • E:后面 8 位 E 代表指數域,二進制中就是 2 的 n 次方,采用移位存儲(127+指數)的二進制方式。
  • M:剩下的 23 位 M 代表小數域。規定小數點前的數必須為 1,因此只記錄小數點后的數。(從左往右,低位補零)

以 7.8125 為例,整數十進制轉二進制,除 2 取余,逆序排列,求得 7 二進制為 111。小數十進制轉二進制,乘 2 取整,順序排列,求得 0.8125 二進制為:0.1101,組合起來是 111.1101

根據規范,小數點前的數只保留 1,因此將 111.1101 小數點左移兩位得 1.111101 * 2^2

符號位 0,指數位為 2+127=129,即二進制 10000001,小數域為 111101。因此 float 數 7.8125 在內存中存儲的格式為:0 10000001 111101 低位補零補齊到 32 位,得:0100 0000 1111 1010 0000 0000 0000 0000

可以使用 Java 提供的 API 驗證一下:

int i = Float.floatToIntBits(7.8125F); //得到 7.8125F 底層數據(十進制)
Integer.toBinaryString(i); //得到指定 int 值的二進制數
//輸出 1000000111110100000000000000000
//補上最高位符號位 0,結果與上面計算的一樣。

通過對浮點類型數值內存結構的了解,我們知道了 float 雖然可用於存儲數值的位數沒有 long 型多,但是 float 通過使用指數進行降維打擊,可表示范圍蹭蹭蹭往上漲。

double 的內存結構同理,只不過 double 二進制位數更多,總共 64 位分別分配給:符號位 1 位,指數位 11 位,小數位 52 位。

需要注意的是,雖然 float 因為有指數的概念,可表示范圍變大了,但是其用於存儲小數的位數卻只有 23 位。這就意味着當一個整型類型數值的二進制位大於 24 位時,類型轉換到 float 就會帶來精度丟失了。

整型轉換浮點型的精度丟失問題

看到上圖中的int 轉 float、long 轉 float 都是虛線表示,代表運算時自動類型轉換可能會出現精度丟失的問題。經過上面對浮點型數據內存結構的學習,我們應該不難理解,float 能表示的數的大小靠指數位,但是表示的數的精度需要靠小數位。而 float 的小數位只有 23 位,而 int 是 32 位。

舉個例子:int 值 16777217,二進制數 1 0000 0000 0000 0000 0000 0001,除去最高位符號位后,需要 25 位表示。

順帶提一下,計算某個數值除了符號位外需要多少位二進制位可以表示,除了挨個去數二進制數外,還可以直接計算 log2 的值:

int i = 16777217;
double num = Math.log(i) / Math.log(2.0);
//num = 24.000000085991324,即需要 25 位二進制位表示

int 轉 float,轉換過程是先將 int 的數值由十進制轉為二進制,再通過對二進制數左移小數點直到個位為 1,變為:1. 0000 0000 0000 0000 0000 0001 * 2 ^ 24,轉換后的數小數點后有 24 位,對 float 來說只能舍棄掉無法表示的位數,只保留 23 位小數位,指數位 24 + 127 = 151,二進制為 10010111,因此轉換后的 float 二進制數為 110010111 + 23個0,float 值為 1.6777216E7,已經丟失了精度。

同理,int 轉 double,由於 double 有 52 位小數位,因此足以 hold 住 int 的精度,而 long 需要 64 位表示精度,因此 long 轉 double 也可能出現精度丟失。另外需要注意的是,單位秒的時間戳,也需要 31 位來表示,用 int 表示是夠的,但是轉 float 也一樣會丟失精度。

以上就是對 Java 類型轉換精度問題的分析,希望對你有幫助 😛


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM