浮點數(shù)計算誤差解析

浮點運算

近期因為在公司的新系統(tǒng)剛上線不久沥潭,還在每日填坑監(jiān)控中邀泉,所以會定期拉取error級別日志來觀察系統(tǒng)穩(wěn)定,在今日拉取的錯誤日志中發(fā)現(xiàn)一條error日志钝鸽,通過定位分析到是下面一段代碼拋出的斷言異常:

    val card1 = 3.33
    val card2=4.44
    val totalBalance = 7.77
   
    assert(card1+card2 == totalBalance, illegalArgumentException("數(shù)據(jù)異常"))

通過debug得知在card+card2兩個double類型相加得出來的值是7.7700000000000005汇恤;其實這是在程序在進行基本類型運算中,對于基本類型不夠了解造成的誤會拔恰,那么接下來我們來看看基本類型在計算機中到底是怎么進行類型計算的

Double類型與float精度類型

double:


微信圖片編輯_20180721160222.jpg

我們可以從wiki官方可以看到double類型在計算機中Double型數(shù)據(jù)根據(jù)IEEE754標準因谎,占用計算機64bit,即8個字節(jié)的內(nèi)存空間,浮點(小數(shù)位)占用52bit [bit 0 -51], 指數(shù)位占用(11bit) [bit 52 - bit 62], 符號位1bit(bit 63)

于是在我們給基礎類型計算時颜懊,計算機中都是通過轉換成2進制來轉換财岔,以上述為例,

val a=3.333  val b=4.44 
3.33+4.44

從二進制換算從做左到右換算河爹,ps:(這還是努力搜索的結果匠璧,二進制換算已經(jīng)都還給大一的學堂了)

整數(shù)型
3為整數(shù)型即21
浮點數(shù):
0.33*2=0.66取整數(shù)部分0,基數(shù)=0.66
0.66*2=1.32取整數(shù)部分1咸这,基數(shù)=0.32

以此類推夷恍,直到基數(shù)為0
那么 上述圖中double為52位,當無窮小數(shù)超出位數(shù)后則會自動舍棄掉媳维,最終結果就為7.7733301001..........那么就肯定不與7.77相等了

于是小編立即想到了另一個平時大范圍運用到的基本類型float酿雪,那么float是不是也會跟double一樣呢遏暴,通過下述實驗可以看出

    public static void main(String[] args) {
        float a=3.33f;
        float b=4.44f;
        System.out.println(a+b);
        float c=3.333333333333111f;
        float d=4.444444444444222f;
        System.out.println(c+d);
    }

console:

D:\Java\jdk1.8.0_151\bin\java
7.77
7.7777777

Process finished with exit code 0

那么是不是很奇怪呢,為什么到float類型的時候有時候是好的有時候不行呢
其實我們不難發(fā)現(xiàn)指黎,float的長度并沒有double長朋凉,他的尾數(shù)只有23bit,那么后面的都會自動舍棄掉袋励。

總結與解決方案

其實大部分coder包括小編自己侥啤,在畢業(yè)之后一直尊崇著運算變量必須使用decimal類型,包括還記得是剛工作兩年的時候反復閱讀《Effcitve java》中也提到double與float只能用做工業(yè)數(shù)字展示茬故,商業(yè)運算必須使用bigdecimal盖灸,double與float會產(chǎn)生精度問題,那么bigdecimal為什么可以避免上述問題呢

我們可以看下bigdecimal在運算上述中的結果:

       BigDecimal  g=new BigDecimal(3.33f);
       BigDecimal y=new BigDecimal(4.44f);
       BigDecimal u=g.add(y);
        System.out.println(u);
        BigDecimal t=g.add(y).setScale(2,BigDecimal.ROUND_UP);
        System.out.println(t);

console如下

D:\Java\jdk1.8.0_151\bin\java 
7.769999980926513671875
7.77

Process finished with exit code 0

那么我們可以從上述看出磺芭,decimal類型其實他并不是在原變量賦值計算赁炎,他會計算后賦值給新變量,并且如果不指定保留小數(shù)位以及四舍五入的參數(shù)钾腺,一樣會產(chǎn)生誤差徙垫,那么我們來看看bigdecimal的方法實現(xiàn)

 private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
        long sdiff = (long) scale1 - scale2;
        if (sdiff == 0) {
            return add(xs, ys, scale1);
        } else if (sdiff < 0) {
            int raise = checkScale(xs,-sdiff);
            long scaledX = longMultiplyPowerTen(xs, raise);
            if (scaledX != INFLATED) {
                return add(scaledX, ys, scale2);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
                return ((xs^ys)>=0) ? // same sign test
                    new BigDecimal(bigsum, INFLATED, scale2, 0)
                    : valueOf(bigsum, scale2, 0);
            }
        } else {
            int raise = checkScale(ys,sdiff);
            long scaledY = longMultiplyPowerTen(ys, raise);
            if (scaledY != INFLATED) {
                return add(xs, scaledY, scale1);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
                return ((xs^ys)>=0) ?
                    new BigDecimal(bigsum, INFLATED, scale1, 0)
                    : valueOf(bigsum, scale1, 0);
            }
        }
    }

我們從上面方法中可以看到,add方法其實他也是沒有改變核心原理放棒,在轉換成二進制之后進行遞歸計算姻报,但是他提供了 封裝的保留位數(shù)以及四舍五入的方法,所以在運用中給商業(yè)計算帶來了可控的保險间螟,當然因為他的實現(xiàn)吴旋,我們可以看到其開銷也是不小的,new了新的變量,以及l(fā)ong的位數(shù)值,double浮點數(shù)的轉換類型征炼。

綜上,我們在商業(yè)特別是跟有小數(shù)點相關的場景中笆焰,一定要使用bigdecimal類型進行賦值運算,并且在系統(tǒng)設計中就要統(tǒng)一制定好小數(shù)位保留長度以及四舍五入策略见坑,這樣在代碼中統(tǒng)一遵照就可以避免計算機在二進制運算帶來的誤差嚷掠,ps:(畢竟我們?nèi)粘I钪羞€是以十進制為生活的維度)

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荞驴,隨后出現(xiàn)的幾起案子叠国,更是在濱河造成了極大的恐慌,老刑警劉巖戴尸,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粟焊,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機项棠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門悲雳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人香追,你說我怎么就攤上這事合瓢。” “怎么了透典?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵晴楔,是天一觀的道長。 經(jīng)常有香客問我峭咒,道長税弃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任凑队,我火速辦了婚禮则果,結果婚禮上,老公的妹妹穿的比我還像新娘漩氨。我一直安慰自己西壮,他們只是感情好,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布叫惊。 她就那樣靜靜地躺著款青,像睡著了一般。 火紅的嫁衣襯著肌膚如雪霍狰。 梳的紋絲不亂的頭發(fā)上可都,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機與錄音蚓耽,去河邊找鬼。 笑死旋炒,一個胖子當著我的面吹牛步悠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瘫镇,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼鼎兽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了铣除?” 一聲冷哼從身側響起谚咬,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尚粘,沒想到半個月后择卦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年秉继,在試婚紗的時候發(fā)現(xiàn)自己被綠了祈噪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡尚辑,死狀恐怖辑鲤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杠茬,我是刑警寧澤月褥,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站瓢喉,受9級特大地震影響宁赤,放射性物質發(fā)生泄漏。R本人自食惡果不足惜灯荧,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一礁击、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逗载,春花似錦哆窿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至擦秽,卻和暖如春码荔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背感挥。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工缩搅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人触幼。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓硼瓣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親置谦。 傳聞我的和親對象是個殘疾皇子堂鲤,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容