Java中浮點數的坑

平兄聊Java發表於2021-05-16

基本資料型別



浮點數存在誤差

浮點數有一個需要特別注意的點就是浮點數是有誤差的,比如以下這段程式碼你覺得輸出的什麼結果:

public class Demo {
	public static void main(String[] args) {
		System.out.println(0.1+0.2 == 0.3);//輸出false
	}
}

這段程式碼輸出值是false,之所以是這個結果那是因為浮點數是存在誤差的,也就yi是說0.1在計算機中儲存時不是精確的0.1,而有可能是0.1000000001,或者其他數,而0.2或0.3也是如此,所以0.1+0.2和0.3在計算機中是不相等的。

因為浮點數存在這個特性,所以我們在程式設計中間要儘量避免用浮點數進行比較。

如果非要用浮點數進行比較的話,那可以使用下面這個方法:

public class Demo {
	public static void main(String[] args) {
		float n = (float)1e-6;//表示10的-6次方
		System.out.println(0.1+0.2 - 0.3 < n);//輸出true
	}
}

以上程式碼的輸出值是true,該方法的原理是如果兩個數相差足夠小,小到可以忽略不記的話,這裡的界限設定是10的-6次方,那證明比較的這兩個數可以認為是相等的,此方法只能在所表示的浮點數的小數點後的位數不是很多的時候使用。

接下來我們再來看一種極端的情況,程式碼如下:

public class Demo {
	public static void main(String[] args) {
		System.out.println(0.30000000000000001 == 0.3);//輸出true
	}
}

以上的程式碼輸出true,但其實我們肉眼可以很直觀的看出,這兩個數雖然很接近,但他們絕對不相等,像這種極端的數我們是無法用上面的方法進行比較的,所以還是記住這句話:儘量避免對浮點數進行比較。


BigDecimal類

我們既然知道了浮點數是存在誤差的,所以在資料本身需要準確精度儲存時,我們是一定不會使用float和double的,比如金錢數額的儲存。這時我們通常使用BigDecimal類進行儲存,它是一個可以儲存準確浮點數的類。


  1. BigDecimal類的定義:
   BigDecimal bd = new BigDecimal("123.456");
  1. BigDecimal使用scale()表示小數位數,例如:
   BigDecimal d1 = new BigDecimal("987.65");
   BigDecimal d2 = new BigDecimal("987.6500");
   BigDecimal d3 = new BigDecimal("98765400");
   System.out.println(d1.scale()); // 2,表示兩位小數
   System.out.println(d2.scale()); // 4
   System.out.println(d3.scale()); // 0
  1. BigDecimal中的stripTrailingZeros()方法,可以將BigDecimal格式化為去掉數值末尾0的相等的數:
   BigDecimal d1 = new BigDecimal("123.4500");
   BigDecimal d2 = d1.stripTrailingZeros();
   System.out.println(d1+" "+d1.scale()); // 123.4500  4
   System.out.println(d2+" "+d2.scale()); // 123.45  2,因為去掉了00
   
   BigDecimal d3 = new BigDecimal("1234500");
   BigDecimal d4 = d3.stripTrailingZeros();
   System.out.println(d3+" "+d3.scale()); // 1234500  0
   System.out.println(d4+" "+d4.scale()); // 1.2345E+6  -2

BigDecimalscale()返回負數,例如,-2,表示這個數是個整數,並且末尾有2個0。以上的d4就是如此,去掉0後數值沒變,只是換了一種表示方法。

  1. BigDecimal可以設定它的scale,如果精度比原始值低,那麼按照指定的方法進行四捨五入或者直接截斷:
   import java.math.BigDecimal;
   import java.math.RoundingMode;
   
   public class Demo {
   	public static void main(String[] args) {
   		BigDecimal d1 = new BigDecimal("123.456789");
           BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四捨五入,123.4568
           BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截斷,123.4567
           System.out.println(d2);//123.4568
           System.out.println(d3);//123.4567
   	}
   }
  1. BigDecimal的加、減、乘、除:
   import java.math.BigDecimal;
   
   public class Demo {
   	public static void main(String[] args) {
   		BigDecimal d1 = new BigDecimal("124.44");
           BigDecimal d2 = new BigDecimal("12.2");
           System.out.println(d1.add(d2));//d1+d2   136.64
           System.out.println(d1.subtract(d2));//d1-d2  112.24
           System.out.println(d1.multiply(d2));//d1*d2  1518.168
           System.out.println(d1.divide(d2));//d1/d2   10.2
   	}
   }
  1. BigDecimal在做加、減、乘時,精度不會丟失,但是做除法時,存在無法除盡的情況,這時就必須指定精度以及如何進行截斷:
   import java.math.BigDecimal;
   import java.math.RoundingMode;
   
   public class Demo {
   	public static void main(String[] args) {
   		BigDecimal d1 = new BigDecimal("123.456");
   		BigDecimal d2 = new BigDecimal("23.456789");
   		BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小數並四捨五入
   		BigDecimal d4 = d1.divide(d2); // 報錯:ArithmeticException,因為除不盡
   	}
   }
  1. 可以對BigDecimal做除法的同時求其餘數:
   import java.math.BigDecimal;
   public class Demo {
   	public static void main(String[] args) {
   		BigDecimal n = new BigDecimal("22.444");
           BigDecimal m = new BigDecimal("0.23");
           BigDecimal[] dr = n.divideAndRemainder(m);
           System.out.println(dr[0]); // 97.0
           System.out.println(dr[1]); // 0.134
   	}
   }
  1. 呼叫divideAndRemainder()方法時,返回的陣列包含兩個BigDecimal,第一個是商,第二個是餘數,商總是整數,餘數不會大於餘數,我們可以利用該方法判斷兩個BigDecimal是否是整數倍數:
   BigDecimal n = new BigDecimal("12.34");
   BigDecimal m = new BigDecimal("0.12");
   BigDecimal[] dr = n.divideAndRemainder(m);
   if (dr[1].signum() == 0) {//signum()會基於此BigDecimal返回三個值-1、1、0,分別對應為該數小於0,大於0和等於0
       // n是m的整數倍
   }
  1. 比較兩個BigDecimal的值是否相等時,要注意的是,使用equals()方法不但要求兩個BigDecimal的值相等,還要求它們的scale()相等:
   BigDecimal d1 = new BigDecimal("123.45");
   BigDecimal d2 = new BigDecimal("123.45000");
   System.out.println(d1.equals(d2)); // false,因為scale不同
   System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因為d2去除尾部0後scale變為2,與d1相同

注意:使用compareTo()來比較兩個BigDecimal的值,不要用equals()

  1. 使用compareTo()方法來比較兩數大小,它根據兩個值的大小分別返回-1、1和0,分別表示小於、大於和等於。
    import java.math.BigDecimal;
    public class Demo {
    	public static void main(String[] args) {
    		BigDecimal d1 = new BigDecimal("123.45");
    		BigDecimal d2 = new BigDecimal("123.45000");
    		BigDecimal d3 = new BigDecimal("123.40");
    		System.out.println(d1.compareTo(d2)); // 0
    		System.out.println(d1.compareTo(d3));//  1
    		System.out.println(d3.compareTo(d2));// -1
    	}
    }
  1. 檢視BigDecimal的原始碼,可以發現一個BigDecimal是通過一個BigInteger和一個scale來表示的,即BigInteger表示一個完整的整數,而scale表示小數位數:
    public class BigDecimal extends Number implements Comparable<BigDecimal> {
        private final BigInteger intVal;
        private final int scale;
    }


更多精彩內容敬請關注微信公眾號:【平兄聊Java】

相關文章