浮點運算
近期因為在公司的新系統(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:
我們可以從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钪羞€是以十進制為生活的維度)