歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Java浮點數計算精度損失底層原理與解決方案

Java浮點數計算精度損失底層原理與解決方案

日期:2017/3/1 9:05:33   编辑:Linux編程

  浮點數會有精度損失這個在上大學的時候就已經被告知,但是至今完全沒有想明白其中的原由,老師講的時候也是一筆帶過的,自己也沒有好好琢磨。終於在工作的時候碰到了,於是google了一番。

問題:

  對兩個double類型的值進行運算,有時會出現結果值異常的問題。比如:

1     System.out.println(19.99+20);
2     System.out.println(1.0-0.66);
3     System.out.println(0.033*100);
4     System.out.println(12.3/100);

輸出:

39.989999999999995
0.33999999999999997
3.3000000000000003
0.12300000000000001

  Java中的簡單浮點數類型float和double不能夠精確運算。這個問題其實不是JAVA的bug,因為計算機本身是二進制的,而浮點數實際上只是個近似值,所以從二進制轉化為十進制浮點數時,精度容易丟失,導致精度下降。

關於精度損失的原理可以很簡單的講,首先一個正整數在計算機中表示使用01010形式表示的,浮點數也不例外。

  比如11,11除以2等於5余1

       5除以2等於2余1

       2除以2等於1余0

       1除以2等於0余1

  所以11二進制表示為:1011.

  double類型占8個字節,64位,第1位為符號位,後面11位是指數部分,剩余部分是有效數字。

  正整數除以2肯定會有個盡頭的,之後二進制還原成十進制只需要乘以2即可。

  舉個例子:0.99用的有效數字部分,

       0.99 * 2 = 1+0.98 --> 1

       0.98 * 2 = 1+0.96 --> 1

       0.96 * 2 = 1+0.92 -- >1

       0.92 * 2 = 1+0.84 -- >1

      ...............

  這樣周而復始是沒法有盡頭的,而double有效數字有限,所以必定會有損失,所以二進制無法准確表示0.99,就像十進制無法准確表示1/3一樣。

解決辦法:

  在《Effective Java》中提到一個原則,那就是float和double只能用來作科學計算或者是工程計算,但在商業計算中我們要用java.math.BigDecimal,通過使用BigDecimal類可以解決上述問題,首先需要注意的是,直接使用字符串來構造BigDecimal是絕對沒有精度損失的,如果用double或者把double轉化成string來構造BigDecimal依然會有精度損失,所以我覺得這種解決方法就是在使用中就把浮點數用string來表示存放,涉及到運算直接用string構造double,否則肯定會有精度損失。

《Effective Java中文版 第2版》.(Joshua Bloch)(高清pdf)+英文版+源代碼 下載見 http://www.linuxidc.com/Linux/2016-11/137370.htm

1. 相加

 1 /**
 2  * 相加
 3  * @param double1
 4  * @param double2
 5  * @return
 6  */
 7 public static double add(String doubleValA, String doubleValB) {  
 8     BigDecimal a2 = new BigDecimal(doubleValA);  
 9     BigDecimal b2 = new BigDecimal(doubleValB);  
10     return a2.add(b2).doubleValue();  
11 }

2. 相減

 1 /**
 2  * 相減
 3  * @param double1
 4  * @param double2
 5  * @return
 6  */
 7 public static double sub(String doubleValA, String doubleValB) {  
 8     BigDecimal a2 = new BigDecimal(doubleValA);  
 9     BigDecimal b2 = new BigDecimal(doubleValB);  
10     return a2.subtract(b2).doubleValue();
11 }

3. 相乘

 1 /**
 2  * 相乘
 3  * @param double1
 4  * @param double2
 5  * @return
 6  */
 7 public static double mul(String doubleValA, String doubleValB) {  
 8     BigDecimal a2 = new BigDecimal(doubleValA);  
 9     BigDecimal b2 = new BigDecimal(doubleValB);  
10     return a2.multiply(b2).doubleValue();
11 }

4. 相除

 1 /**
 2  * 相除
 3  * @param double1
 4  * @param double2
 5  * @param scale 除不盡時指定精度
 6  * @return
 7  */
 8 public static double div(String doubleValA, String doubleValB, int scale) {  
 9     BigDecimal a2 = new BigDecimal(doubleValA);  
10     BigDecimal b2 = new BigDecimal(doubleValB);
11     return a2.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();  
12 }

5. 主函數調用

1 public static void main(String[] args) {
2     String doubleValA = "3.14159267";
3     String doubleValB = "2.358";
4     System.out.println("add:" + add(doubleValA, doubleValB));
5     System.out.println("sub:" + sub(doubleValA, doubleValB));
6     System.out.println("mul:" + mul(doubleValA, doubleValB));
7     System.out.println("div:" + div(doubleValA, doubleValB, 8));
8 }

結果展示如下所示:

add:5.49959267
sub:0.78359267
mul:7.40787551586
div:1.33231241

所以最好的方法是完全拋棄double,用string和java.math.BigDecimal。

  java遵照IEEE制定的浮點數表示法來進行float,double運算。這種結構是一種科學計數法,用符號、指數和尾數來表示,底數定為2——即把一個浮點數表示為尾數乘以2的指數次方再添上符號。具體底層如何存儲以及如何進行運行請繼續關注我的博客,後續我會將詳情總結好的。

Copyright © Linux教程網 All Rights Reserved