Java中的小數運算與精度損失

wunsiang發表於2020-04-30

float、double型別的問題

我們都知道,計算機是使用二進位制儲存資料的。而平常生活中,大多數情況下我們都是使用的十進位制,因此計算機顯示給我們看的內容大多數也是十進位制的,這就使得很多時候資料需要在二進位制與十進位制之間進行轉換。對於整數來說,兩種進位制可以做到一一對應。而對於小數來講就不是這樣的啦。

我們先來看看十進位制小數轉二進位制小數的方法

對小數點以後的數乘以2,會得到一個結果,取結果的整數部分(不是1就是0),然後再用小數部分再乘以2,再取結果的整數部分……以此類推,直到小數部分為0或者位數已經夠了。順序取每次運算得到的整數部分,即為轉換後的小數部分。

演示: 
0.125 ×2=0.25 .......................0
0.25×2=0.5.............................0
0.5×2=1.0................................1
即 0.125的二進位制表示為小數部分為0.001

其實我們可以看出,這種方法實質上就是用1/2,1/4,8/1...來組合加出我們要轉換的資料值,但顯然不是所有的數都能夠組合出來的。如0.1。

 0.1×2=0.2 .....................0

 0.2×2=0.4 ......................0

 0.4×2=0.8 .....................0

 0.8×2=1.6.......................1

 0.6×2=1.2.......................1

 0.2×2=0.4.......................0
 .....

從上述計算過程我們可以看出,這是個無限小數,所以在這種情況下我們的float、double只能捨去一些位。

那為什麼我們在直接給float賦值在輸出時沒有看到精度損失而在運算時卻會出現呢?

確實是這樣,如下

float a = 0.2f;
System.out.println(a);
//輸出0.2

對於上述情況我只是查了資料,好像是因為編譯器會進行優化,當我們儲存的資料特別接近的時候,編譯器會很貼心的返回我們想看到的數值(即二進位制浮點數並不能準確的表示0.1這個十進位制小數,它使用了0.100000001490116119384765625來代替0.1。),至於到了運算中,就會出現精度損失較大從而看到了真相。如果這塊說的不對歡迎小夥伴們在評論區指正!

解決方法

BigDecimal 原理

我們一般會使用
BigDecimal 來避免出現精度丟失問題,至於為什麼BigDecimal 可以避免,而float或double不行,我們在此不詳細討論,簡單來說就是BigDecimal 通過藉助整數來表示小數的方式,因為對於整數而言,二進位制和十進位制是完全一一對應的,用整數來表示小數,再記錄下小數的位數,就可以完美的解決該問題。

BigDecimal 用法

java.math.BinInteger 類和 java.math.BigDecimal 類都是Java提供的用於高精度計算的類.其中 BigInteger 類是針對大整數的處理類,而 BigDecimal 類則是針對大小數的處理類.

BigDecimal構造方法

BigDecimal BigDecimal(double d); //不允許使用
BigDecimal BigDecimal(String s); //常用,推薦使用
static BigDecimal valueOf(double d); //常用,推薦使用
  1. double 引數的構造方法,不允許使用!!!!因為它不能精確的得到相應的值;
  2. String 構造方法是完全可預知的: 寫入 new BigDecimal("0.1") 將建立一個 BigDecimal,它正好等於預期的0.1; 因此,通常建議優先使用 String 構造方法;
  3. 靜態方法 valueOf(double val) 內部實現,仍是將 double 型別轉為 String 型別; 這通常是將 double(或float)轉化為 BigDecimal 的首選方法;

測試

System.out.println(new BigDecimal(0.1));
System.out.println(BigDecimal.valueOf(0.1));
\\輸出*****************************************
0.1000000000000000055511151231257827021181583404541015625
0.1

BigDecimal常用操作

我們通過一個工具類原始碼來體會BigDecimal的常規用法

package com.util;
 
import java.math.BigDecimal;
 
/**
 * 提供精確的浮點數運算(包括加、減、乘、除、四捨五入)工具類
 */
public class ArithUtil {
 
	// 除法運算預設精度
	private static final int DEF_DIV_SCALE = 10;
 
	private ArithUtil() {
 
	}
 
	/**
	 * 精確加法
	 */
	public static double add(double value1, double value2) {
		BigDecimal b1 = BigDecimal.valueOf(value1);
		BigDecimal b2 = BigDecimal.valueOf(value2);
		return b1.add(b2).doubleValue();
	}
 
	/**
	 * 精確減法
	 */
	public static double sub(double value1, double value2) {
		BigDecimal b1 = BigDecimal.valueOf(value1);
		BigDecimal b2 = BigDecimal.valueOf(value2);
		return b1.subtract(b2).doubleValue();
	}
 
	/**
	 * 精確乘法
	 */
	public static double mul(double value1, double value2) {
		BigDecimal b1 = BigDecimal.valueOf(value1);
		BigDecimal b2 = BigDecimal.valueOf(value2);
		return b1.multiply(b2).doubleValue();
	}
 
	/**
	 * 精確除法 使用預設精度
	 */
	public static double div(double value1, double value2) throws IllegalAccessException {
		return div(value1, value2, DEF_DIV_SCALE);
	}
 
	/**
	 * 精確除法
	 * @param scale 精度
	 */
	public static double div(double value1, double value2, int scale) throws IllegalAccessException {
		if(scale < 0) {
			throw new IllegalAccessException("精確度不能小於0");
		}
		BigDecimal b1 = BigDecimal.valueOf(value1);
		BigDecimal b2 = BigDecimal.valueOf(value2);
		// return b1.divide(b2, scale).doubleValue();
		return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
	}
 
	/**
	 * 四捨五入
	 * @param scale 小數點後保留幾位
	 */
	public static double round(double v, int scale) throws IllegalAccessException {
		return div(v, 1, scale);
	}
	
	/**
	 * 比較大小
	 */
	public static boolean equalTo(BigDecimal b1, BigDecimal b2) {
		if(b1 == null || b2 == null) {
			return false;
		}
		return 0 == b1.compareTo(b2);
	}
}

相關文章