[改善Java代碼]不要讓四舍五入虧了一方


建議25: 不要讓四舍五入虧了一方

本建議還是來重溫一個小學數學問題:四舍五入。四舍五入是一種近似精確的計算方法,在Java 5之前,我們一般是通過使用Math.round來獲得指定精度的整數或小數的,這種方法使用非常廣泛,代碼如下:

1 public class Client {  
2      public static void main(String[] args) {  
3           System.out.println("10.5近似值:" + Math.round(10.5));  
4           System.out.println("-10.5近似值:"+ Math.round(-10.5));  
5      }  
6 } 

運行結果:

10.5近似值:11
-10.5近似值:-10

這是四舍五入的經典案例,也是初級面試官很樂意選擇的考題,絕對值相同的兩個數字,近似值為什么就不同了呢?這是由Math.round采用的舍入規則所決定的(采用的是正無窮方向舍入規則,后面會講解)。我們知道四舍五入是有誤差的:其誤差值是舍入位的一半。我們以舍入運用最頻繁的銀行利息計算為例來闡述該問題。

我們知道銀行的盈利渠道主要是利息差,從儲戶手里收攏資金,然后放貸出去,其間的利息差額便是所獲得的利潤。對一個銀行來說,對付給儲戶的利息的計算非常頻繁,人民銀行規定每個季度末月的20日為銀行結息日,一年有4次的結息日。

場景介紹完畢,我們回過頭來看四舍五入,小於5的數字被舍去,大於等於5的數字進位后舍去,由於所有位上的數字都是自然計算出來的,按照概率計算可知,被舍入的數字均勻分布在0到9之間,下面以10筆存款利息計算作為模型,以銀行家的身份來思考這個算法:

四舍。舍棄的數值:0.000、0.001、0.002、0.003、0.004,因為是舍棄的,對銀行家來說,就不用付款給儲戶了,那每舍棄一個數字就會賺取相應的金額:0.000、0.001、0.002、0.003、0.004。

五入。進位的數值:0.005、0.006、0.007、0.008、0.009,因為是進位,對銀行家來說,每進一位就會多付款給儲戶,也就是虧損了,那虧損部分就是其對應的10進制補數:0.005、0.004、0.003、0.002、0.001。

因為舍棄和進位的數字是在0到9之間均勻分布的,所以對於銀行家來說,每10筆存款的利息因采用四舍五入而獲得的盈利是:

0.000 + 0.001 + 0.002 + 0.003 + 0.004 - 0.005 - 0.004 - 0.003 - 0.002 - 0.001 = -0.005 

也就是說,每10筆的利息計算中就損失0.005元,即每筆利息計算損失0.0005元,這對一家有5千萬儲戶的銀行來說(對國內的銀行來說,5千萬是個很小的數字),每年僅僅因為四舍五入的誤差而損失的金額是:

1 public class Client {  
2      public static void main(String[] args) {  
3           //銀行賬戶數量,5千萬  
4           int accountNum =5000*10000;  
5           //按照人行的規定,每個季度末月的20日為銀行結息日  
6           double cost = 0.0005 * accountNum * 4 ;  
7           System.out.println("銀行每年損失的金額:" + cost);  
8      }  
9 } 

輸出的結果是:“銀行每年損失的金額:100000.0”。即,每年因為一個算法誤差就損失了10萬元,事實上以上的假設條件都是非常保守的,實際情況可能損失得更多。那各位可能要說了,銀行還要放貸呀,放出去這筆計算誤差不就抵消掉了嗎?不會抵銷,銀行的貸款數量是非常有限的,其數量級根本沒有辦法和存款相比。

這個算法誤差是由美國銀行家發現的(那可是私人銀行,錢是自己的,白白損失了可不行),並且對此提出了一個修正算法,叫做銀行家舍入(Banker's Round)的近似算法,其規則如下:

舍去位的數值小於5時,直接舍去;

舍去位的數值大於等於6時,進位后舍去;

當舍去位的數值等於5時,分兩種情況:5后面還有其他數字(非0),則進位后舍去;若5后面是0(即5是最后一個數字),則根據5前一位數的奇偶性來判斷是否需要進位,奇數進位,偶數舍去。

以上規則匯總成一句話:四舍六入五考慮,五后非零就進一,五后為零看奇偶,五前為偶應舍去,五前為奇要進一。我們舉例說明,取2位精度:

round(10.5551) = 10.56  
round(10.555)  = 10.56  
round(10.545)  = 10.54 

要在Java 5以上的版本中使用銀行家的舍入法則非常簡單,直接使用RoundingMode類提供的Round模式即可,示例代碼如下:

 1 public class Client {  
 2      public static void main(String[] args) {  
 3           //存款  
 4           BigDecimal d = new BigDecimal(888888);  
 5           //月利率,乘3計算季利率  
 6           BigDecimal r = new BigDecimal(0.001875*3);  
 7           //計算利息  
 8           BigDecimal i = d.multiply(r).setScale(2,RoundingMode.HALF_EVEN);  
 9           System.out.println("季利息是:"+i);  
10      }  
11 } 

在上面的例子中,我們使用了BigDecimal類,並且采用setScale方法設置了精度,同時傳遞了一個RoundingMode.HALF_EVEN參數表示使用銀行家舍入法則進行近似計算,BigDecimal和RoundingMode是一個絕配,想要采用什么舍入模式使用RoundingMode設置即可。目前Java支持以下七種舍入方式:

ROUND_UP: 遠離零方向舍入。

向遠離0的方向舍入,也就是說,向絕對值最大的方向舍入,只要舍棄位非0即進位。

ROUND_DOWN:趨向零方向舍入。

向0方向靠攏,也就是說,向絕對值最小的方向輸入,注意:所有的位都舍棄,不存在進位情況。

ROUND_CEILING:向正無窮方向舍入。

向正最大方向靠攏,如果是正數,舍入行為類似於ROUND_UP;如果為負數,則舍入行為類似於ROUND_DOWN。注意:Math.round方法使用的即為此模式。

ROUND_FLOOR:向負無窮方向舍入。

向負無窮方向靠攏,如果是正數,則舍入行為類似於 ROUND_DOWN;如果是負數,則舍入行為類似於 ROUND_UP。

HALF_UP: 最近數字舍入(5進)。

這就是我們最最經典的四舍五入模式。

HALF_DOWN:最近數字舍入(5舍)。

在四舍五入中,5是進位的,而在HALF_DOWN中卻是舍棄不進位。

HALF_EVEN :銀行家算法。

在普通的項目中舍入模式不會有太多影響,可以直接使用Math.round方法,但在大量與貨幣數字交互的項目中,一定要選擇好近似的計算模式,盡量減少因算法不同而造成的損失。

注意 根據不同的場景,慎重選擇不同的舍入模式,以提高項目的精准度,減少算法損失。

 


免責聲明!

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



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