java中精確計算,double與BigDecimal的取捨
相信java程式設計師都知道double是一種不能用作精確計算的型別,因為它會有精度損失,而要想規避精度損失,大家都會想到BigDecimal,這是JDK提供的類,確實能解決精度問題,但是它並不是完美的,它有如下三個缺點:
- 慢
- 亂
- 也不是那麼準確
一、慢有多慢?
所有人都知道,BigDecimal作為物件,new出來是有成本的,肯定比基本資料型別會慢一點,但具體慢到什麼程度呢?我寫了一段求加和的程式碼如下:
double sum = 0.0; long a = System.currentTimeMillis(); for (int i = 0; i < times; i++) { double d = Math.random(); sum += d; } long b = System.currentTimeMillis(); System.out.println(b - a); System.out.println(sum);
Double sum = 0.0; long a = System.currentTimeMillis(); for (int i = 0; i < times; i++) { Double d = Math.random(); sum += d; } long b = System.currentTimeMillis(); System.out.println(b - a); System.out.println(sum);
BigDecimal sum = new BigDecimal("0"); long a = System.currentTimeMillis(); for (int i = 0; i < times; i++) { BigDecimal d = new BigDecimal(Math.random()); sum = sum.add(d); } long b = System.currentTimeMillis(); System.out.println(b - a); System.out.println(sum);
這三段程式碼分別是用基本資料型別、包裝資料型別和BigDecimal來計算一個和,我的電腦上分別執行這三段程式碼,當times是10000的時候,前兩個是1ms執行時間,而第三個是30ms,當times是100000000的時候,前兩個是差不多2秒(但是結果顯然不正確),而第三個是差不多40秒,這是20~30倍的執行效率差距,所以BigDecimal絕不能濫用 。至於為什麼包裝類的建立幾乎沒有感覺到呢?自動拆裝箱不需要時間嗎?這個就涉及到一些底層的優化策略了,在此不做深究,先了解一下結論,不用太糾結基本型別和包裝型別。
二、亂又怎樣?
亂,在某些人看來,並不能稱為問題,但我是有些不能接受的,比如一個簡單的物理公式:距離=速度時間+(加速度 時間的平方)/2(即:S=vt+1/2at^2),用double來寫就跟公式本身很像,而用BigDecimal,即使是這樣簡單的公式,也讓人看得雲裡霧裡,不知所云,程式碼如下:
double s; double v = 2323.346852; double a = 102.1523684; double t = 20.004; s = v * t + (a * Math.pow(t, 2)) / 2; System.out.println(s); // 66914.87711409896
BigDecimal s; BigDecimal v = new BigDecimal(2323.346852); BigDecimal a = new BigDecimal(102.1523684); BigDecimal t = new BigDecimal(20.004); // 寫這個可得留神,稍微一個不注意,錯個括號,那可就差大了, // 有的人可能想要提取變數,可能會好點,但很多時候就像這個例子, // 最複雜的部分a*t^2/2,提取出來的變數叫什麼名能夠見名知意?可能效果跟不提取也沒啥區別。 s = v.multiply(t).add(a.multiply(t.pow(2)).divide(new BigDecimal(2))); System.out.println(s); // 66914.8771140989556179216886351698979346...........還沒完
三、JDK的類能不準?
關於BigDecimal計算不準確的問題,專案中已經多次遇到了,分為兩方面,一方面是BigDecimal的用法不對,本文中上面所列舉的所有關於BigDecimal的程式碼,用法都是錯的;另一方面是即使用對了結果也未必是你想要的。
先說第一個用法的問題,很多程式設計師在使用BigDecimal時會用BigDecimal(double val)這個構造方法,JDK的文件中說得非常明白“The results of this constructor can be somewhat unpredictable”,“The String constructor, on the other hand, is perfectly predictable”(double的構造方法是垃圾,String的構造方法是完美的),而很少有人在被這個構造方法坑掉之前查閱這個文件,我們用String構造重新執行一下算距離的公式
BigDecimal s; BigDecimal v = new BigDecimal("2323.346852"); BigDecimal a = new BigDecimal("102.1523684"); BigDecimal t = new BigDecimal("20.004"); s = v.multiply(t).add(a.multiply(t.pow(2)).divide(new BigDecimal("2"))); System.out.println(s); // 66914.8771140989472(完結)
可以看出,用double構造方法來計算,精度幾乎沒有比double提升多少,但開銷多了30倍。可見對知識的一知半解有的時候比完全不懂更差。
有的時候,現實是很殘酷的,我們用了正確的方法使用了BigDecimal而結果依然可能是不盡如人意的,比如下面這個場景:
部門 | 人員 | 考勤(分鐘) |
---|---|---|
1 | Jack | 3073.32 |
1 | Robin | 3073.32 |
1 | 小白 | 3073.32 |
2 | Robin | 3073.32 |
2 | 小白 | 3073.32 |
2 | Jack | 3073.32 |
假設某個公司有三個人,兩個部門,而這三個人來回在這兩個部門之間調動,現在要統計每個人的總考勤,每個部門的總考勤,還有整個公司的總考勤,並且是要以小時為單位,保留兩位小數,這個需求一看就很明確,每個人的考勤總和、每個部門的考勤總和、公司的總考勤應該是相等的,但不幸的是,經過計算可能會得到下面的結果:
部門 | 考勤(分鐘) | 考勤(小時) | 人員 | 考勤(分鐘) | 考勤(小時) |
---|---|---|---|---|---|
1 | 9219.96 | 153.67 | Jack | 6146.64 | 102.44 |
2 | 9219.96 | 153.67 | Robin | 6146.64 | 102.44 |
- | - | - | 小白 | 6146.64 | 102.44 |
合計 | - | 307.34 | 合計 | - | 307.32 |
顯然,每個人的考勤總和、每個部門的考勤總和已經不相等了,而這跟你用double還是BigDecimal無關。更令人絕望的是整個公司的合計是18439.92分鐘,保留小數後是307.33小時,三個本應該相等的數,全都不相等。
可能有的朋友已經看出了端倪,這不是一個技術問題,這已經是一個業務了,如果這個專案的需求就是這樣的,那作為開發人員必須據理力爭,痛陳這個不合理性,不要再做任何技術嘗試,不管是用double還是BigDecimal都是在浪費時間。