此篇文章搬運(yùn)于:http://blog.csdn.net/aya19880214/article/details/45891581?(更多詳情點(diǎn)進(jìn)去閱讀,此篇僅作搬運(yùn))
Java浮點(diǎn)數(shù)float和double精確計(jì)算的精度誤差問題總結(jié)
1、float整數(shù)計(jì)算誤差
案例:會(huì)員積分字段采用float類型,導(dǎo)致計(jì)算會(huì)員積分時(shí),7位整數(shù)的數(shù)據(jù)計(jì)算結(jié)果出現(xiàn)誤差。
原因:超出float精度范圍,無法精確計(jì)算。
float和double的精度是由尾數(shù)的位數(shù)來決定的。浮點(diǎn)數(shù)在內(nèi)存中是按科學(xué)計(jì)數(shù)法來存儲(chǔ)的,其整數(shù)部分始終是一個(gè)隱含著的“1”,由于它是不變的,故不能對精度造成影響。
float:2^23 = 8388608,一共七位,這意味著最多能有7位有效數(shù)字,但絕對能保證的為6位,也即float的精度為6~7位有效數(shù)字;
double:2^52 = 4503599627370496,一共16位,同理,double的精度為15~16位。
難道只是位數(shù)多大的問題,字段類型換成double就可以解決嗎?對于本案例是這樣,因?yàn)槎际钦麛?shù)計(jì)算,但如果有小數(shù)位,就不一定了,見下面案例。
2、double小數(shù)轉(zhuǎn)bigdecimal后四舍五入計(jì)算有誤差
案例:
doubleg=12.35;
BigDecimal?bigG=newBigDecimal(g).setScale(1,?BigDecimal.ROUND_HALF_UP); //期望得到12.4
System.out.println("test?G:"+bigG.doubleValue());
test G:12.3
原因:
定義double g= 12.35; ?而在計(jì)算機(jī)中二進(jìn)制表示可能這是樣:定義了一個(gè)g=12.34444444444444449,
new BigDecimal(g)?? g還是12.34444444444444449
new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP); 得到12.3
正確的定義方式是使用字符串構(gòu)造函數(shù):
new BigDecimal("12.35").setScale(1, BigDecimal.ROUND_HALF_UP)
3、float和double做四則運(yùn)算誤差
案例:
public?class?Test{
public?static?void?main(String?args[]){
System.out.println(0.05+0.01);
System.out.println(1.0-0.42);
System.out.println(4.015*100);
System.out.println(123.3/100);
}
}
結(jié)果:
0.060000000000000005
0.5800000000000001
401.49999999999994
1.2329999999999999
原因:
那么為什么會(huì)出現(xiàn)精度丟失呢?在查閱了一些資料以后,我稍微有了一些頭緒,下面是本人的愚見,僅供參考。
首先得從計(jì)算機(jī)本身去討論這個(gè)問題。我們知道,計(jì)算機(jī)并不能識(shí)別除了二進(jìn)制數(shù)據(jù)以外的任何數(shù)據(jù)。無論我們使用何種編程語言,在何種編譯環(huán)境下工作,都要先把源程序翻譯成二進(jìn)制的機(jī)器碼后才能被計(jì)算機(jī)識(shí)別。以上面提到的情況為例,我們源程序里的2.4是十進(jìn)制的,計(jì)算機(jī)不能直接識(shí)別,要先編譯成二進(jìn)制。但問 題來了,2.4的二進(jìn)制表示并非是精確的2.4,反而最為接近的二進(jìn)制表示是2.3999999999999999。原因在于浮點(diǎn)數(shù)由兩部分組成:指數(shù)和尾數(shù),這點(diǎn)如果知道怎樣進(jìn)行浮點(diǎn)數(shù)的二進(jìn)制與十進(jìn)制轉(zhuǎn)換,應(yīng)該是不難理解的。如果在這個(gè)轉(zhuǎn)換的過程中,浮點(diǎn)數(shù)參與了計(jì)算,那么轉(zhuǎn)換的過程就會(huì)變得不可預(yù) 知,并且變得不可逆。我們有理由相信,就是在這個(gè)過程中,發(fā)生了精度的丟失。而至于為什么有些浮點(diǎn)計(jì)算會(huì)得到準(zhǔn)確的結(jié)果,應(yīng)該也是碰巧那個(gè)計(jì)算的二進(jìn)制與 十進(jìn)制之間能夠準(zhǔn)確轉(zhuǎn)換。而當(dāng)輸出單個(gè)浮點(diǎn)型數(shù)據(jù)的時(shí)候,可以正確輸出,如
double d = 2.4;
System.out.println(d);
輸出的是2.4,而不是2.3999999999999999。也就是說,不進(jìn)行浮點(diǎn)計(jì)算的時(shí)候,在十進(jìn)制里浮點(diǎn)數(shù)能正確顯示。這更印證了我以上的想法,即如果浮點(diǎn)數(shù)參與了計(jì)算,那么浮點(diǎn)數(shù)二進(jìn)制與十進(jìn)制間的轉(zhuǎn)換過程就會(huì)變得不可預(yù)知,并且變得不可逆。
事實(shí)上,浮點(diǎn)數(shù)并不適合用于精確計(jì)算,而適合進(jìn)行科學(xué)計(jì)算。這里有一個(gè)小知識(shí):既然float和double型用來表示帶有小數(shù)點(diǎn)的數(shù),那為什么我們不稱 它們?yōu)椤靶?shù)”或者“實(shí)數(shù)”,要叫浮點(diǎn)數(shù)呢?因?yàn)檫@些數(shù)都以科學(xué)計(jì)數(shù)法的形式存儲(chǔ)。當(dāng)一個(gè)數(shù)如50.534,轉(zhuǎn)換成科學(xué)計(jì)數(shù)法的形式為5.053e1,它 的小數(shù)點(diǎn)移動(dòng)到了一個(gè)新的位置(即浮動(dòng)了)。可見,浮點(diǎn)數(shù)本來就是用于科學(xué)計(jì)算的,用來進(jìn)行精確計(jì)算實(shí)在太不合適了。
4、bigdecimal構(gòu)造函數(shù)使用不當(dāng)帶來異常
案例:
BigDecimal其中一個(gè)構(gòu)造函數(shù)以雙精度浮點(diǎn)數(shù)作為輸入,另一個(gè)以整數(shù)和換算因子作為輸入,還有一個(gè)以小數(shù)的String表示作為輸入。要小心使用BigDecimal(double)構(gòu)造函數(shù),因?yàn)槿绻涣私馑瑫?huì)在計(jì)算過程中產(chǎn)生舍入誤差。請使用基于整數(shù)或String的構(gòu)造函數(shù)。
如果使用BigDecimal(double)構(gòu)造函數(shù)不恰當(dāng),在傳遞給 JDBCsetBigDecimal()方法時(shí),會(huì)造成似乎很奇怪的 JDBC 驅(qū)動(dòng)程序中的異常。例如,考慮以下 JDBC 代碼,該代碼希望將數(shù)字0.01存儲(chǔ)到小數(shù)字段:
PreparedStatement ps =
connection.prepareStatement("INSERT INTO Foo SET name=?, value=?");
ps.setString(1, "penny");
ps.setBigDecimal(2, new BigDecimal(0.01));
ps.executeUpdate();
在執(zhí)行這段似乎無害的代碼時(shí)會(huì)拋出一些令人迷惑不解的異常(這取決于具體的 JDBC 驅(qū)動(dòng)程序),因?yàn)?.01的雙精度近似值會(huì)導(dǎo)致大的換算值,這可能會(huì)使 JDBC 驅(qū)動(dòng)程序或數(shù)據(jù)庫感到迷惑。JDBC 驅(qū)動(dòng)程序會(huì)產(chǎn)生異常,但可能不會(huì)說明代碼實(shí)際上錯(cuò)在哪里,除非意識(shí)到二進(jìn)制浮點(diǎn)數(shù)的局限性。相反,使用BigDecimal("0.01")或BigDecimal(1,
2)構(gòu)造BigDecimal來避免這類問題,因?yàn)檫@兩種方法都可以精確地表示小數(shù)。
5、解決浮點(diǎn)數(shù)精確計(jì)算有誤差的方法
在《Effective?? Java》這本書中也提到這個(gè)原則,float和double只能用來做科學(xué)計(jì)算或者是工程計(jì)算,在商業(yè)計(jì)算中我們要用java.math.BigDecimal。使用BigDecimal并且一定要用String來夠造。
BigDecimal用哪個(gè)構(gòu)造函數(shù)?
BigDecimal(double?val)
BigDecimal(String?val)
上面的API簡要描述相當(dāng)?shù)拿鞔_,而且通常情況下,上面的那一個(gè)使用起來要方便一些。我們可能想都不想就用上了,會(huì)有什么問題呢?等到出了問題的時(shí)候,才發(fā)現(xiàn)參數(shù)是double的構(gòu)造方法的詳細(xì)說明中有這么一段:
Note:?the?results?of?this?constructor?can?be?somewhat?unpredictable.?One?might?assume?that?new?BigDecimal(.1)?is?exactly?equal?to?.1,?but?it?is?actually?equal?to?.1000000000000000055511151231257827021181583404541015625.?This?is?so?because?.1?cannot?be?represented?exactly?as?a?double?(or,?for?that?matter,?as?a?binary?fraction?of?any?finite?length).?Thus,?the?long?value?that?is?being?passed?in?to?the?constructor?is?not?exactly?equal?to?.1,?appearances?nonwithstanding.
The?(String)?constructor,?on?the?other?hand,?is?perfectly?predictable:?new?BigDecimal(".1")?is?exactly?equal?to?.1,?as?one?would?expect.?Therefore,?it?is?generally?recommended?that?the?(String)?constructor?be?used?in?preference?to?this?one.
原來我們?nèi)绻枰_計(jì)算,非要用String來夠造BigDecimal不可!
6、定點(diǎn)數(shù)和浮點(diǎn)數(shù)的區(qū)別
在計(jì)算機(jī)系統(tǒng)的發(fā)展過程中,曾經(jīng)提出過多種方法表達(dá)實(shí)數(shù)。典型的比如相對于浮點(diǎn)數(shù)的定點(diǎn)數(shù)(Fixed Point Number)。在這種表達(dá)方式中,小數(shù)點(diǎn)固定的位于實(shí)數(shù)所有數(shù)字中間的某個(gè)位置。貨幣的表達(dá)就可以使用這種方式,比如 99.00 或者 00.99 可以用于表達(dá)具有四位精度(Precision),小數(shù)點(diǎn)后有兩位的貨幣值。由于小數(shù)點(diǎn)位置固定,所以可以直接用四位數(shù)值來表達(dá)相應(yīng)的數(shù)值。SQL 中的 NUMBER 數(shù)據(jù)類型就是利用定點(diǎn)數(shù)來定義的。還有一種提議的表達(dá)方式為有理數(shù)表達(dá)方式,即用兩個(gè)整數(shù)的比值來表達(dá)實(shí)數(shù)。
定點(diǎn)數(shù)表達(dá)法的缺點(diǎn)在于其形式過于僵硬,固定的小數(shù)點(diǎn)位置決定了固定位數(shù)的整數(shù)部分和小數(shù)部分,不利于同時(shí)表達(dá)特別大的數(shù)或者特別小的數(shù)。最終,絕大多數(shù)現(xiàn)代的計(jì)算機(jī)系統(tǒng)采納了所謂的浮點(diǎn)數(shù)表達(dá)方式。這種表達(dá)方式利用科學(xué)計(jì)數(shù)法來表達(dá)實(shí)數(shù),即用一個(gè)尾數(shù)(Mantissa ),一個(gè)基數(shù)(Base),一個(gè)指數(shù)(Exponent)以及一個(gè)表示正負(fù)的符號(hào)來表達(dá)實(shí)數(shù)。比如 123.45 用十進(jìn)制科學(xué)計(jì)數(shù)法可以表達(dá)為 1.2345 × 102 ,其中 1.2345 為尾數(shù),10 為基數(shù),2 為指數(shù)。浮點(diǎn)數(shù)利用指數(shù)達(dá)到了浮動(dòng)小數(shù)點(diǎn)的效果,從而可以靈活地表達(dá)更大范圍的實(shí)數(shù)。
在MySQL中使用浮點(diǎn)數(shù)類型和定點(diǎn)數(shù)類型來表示小數(shù)。浮點(diǎn)數(shù)類型包括單精度浮點(diǎn)數(shù)(FLOAT型)和雙精度浮點(diǎn)數(shù)(DOUBLE型)。定點(diǎn)數(shù)類型就是DECIMAL型。MySQL的浮點(diǎn)數(shù)類型和定點(diǎn)數(shù)類型如下表所示:
類型名稱字節(jié)數(shù)負(fù)數(shù)的取值范圍非負(fù)數(shù)的取值范圍
FLOAT4-3.402823466E+38~
-1.175494351E-380和1.175494351E-38~
3.402823466E+38
DOUBLE8-1.7976931348623157E+308~
-2.2250738585072014E-3080和2.2250738585072014E-308~
1.7976931348623157E+308
DECIMAL(M,D)或DEC(M,D)M+2同DOUBLE型同DOUBLE型
從上表中可以看出,DECIMAL型的取值范圍與DOUBLE相同。但是,DECIMAL的有效取值范圍由M和D決定,而且DECIMAL型的字節(jié)數(shù)是M+2,也就是說,定點(diǎn)數(shù)的存儲(chǔ)空間是根據(jù)其精度決定的。
7、bigdecimal比等方法
如浮點(diǎn)類型一樣,BigDecimal也有一些令人奇怪的行為。尤其在使用equals()方法來檢測數(shù)值之間是否相等時(shí)要小心。equals()方法認(rèn)為,兩個(gè)表示同一個(gè)數(shù)但換算值不同(例如,100.00和100.000)的BigDecimal值是不相等的。然而,compareTo()方法會(huì)認(rèn)為這兩個(gè)數(shù)是相等的,所以在從數(shù)值上比較兩個(gè)BigDecimal值時(shí),應(yīng)該使用compareTo()而不是equals()。
另外還有一些情形,任意精度的小數(shù)運(yùn)算仍不能表示精確結(jié)果。例如,1除以9會(huì)產(chǎn)生無限循環(huán)的小數(shù).111111...。出于這個(gè)原因,在進(jìn)行除法運(yùn)算時(shí),BigDecimal可以讓您顯式地控制舍入。movePointLeft()方法支持 10 的冪次方的精確除法。
與零比較:
int r=big_decimal.compareTo(BigDecimal.Zero); //和0,Zero比較
if(r==0) //等于
if(r==1) //大于
if(r==-1) //小于
8、簡化bigdecimal計(jì)算的小工具類
如果我們要做一個(gè)加法運(yùn)算,需要先將兩個(gè)浮點(diǎn)數(shù)轉(zhuǎn)為String,然后夠造成BigDecimal,在其中一個(gè)上調(diào)用add方法,傳入另一個(gè)作為參數(shù),然后把運(yùn)算的結(jié)果(BigDecimal)再轉(zhuǎn)換為浮點(diǎn)數(shù)。你能夠忍受這么煩瑣的過程嗎?網(wǎng)上提供的工具類Arith來簡化操作。它提供以下靜態(tài)方法,包括加減乘除和四舍五入:
public?? static?? double?? add(double?? v1,double?? v2)
public?? static?? double?? sub(double?? v1,double?? v2)
public?? static?? double?? mul(double?? v1,double?? v2)
public?? static?? double?? div(double?? v1,double?? v2)
public?? static?? double?? div(double?? v1,double?? v2,int?? scale)
public?? static?? double?? round(double?? v,int?? scale)
importjava.math.BigDecimal;
/**
*?進(jìn)行BigDecimal對象的加減乘除,四舍五入等運(yùn)算的工具類
*?@author?ameyume
*
*/
publicclassArith?{
/**
*?由于Java的簡單類型不能夠精確的對浮點(diǎn)數(shù)進(jìn)行運(yùn)算,這個(gè)工具類提供精
*?確的浮點(diǎn)數(shù)運(yùn)算,包括加減乘除和四舍五入。
*/
//默認(rèn)除法運(yùn)算精度
privatestaticfinalintDEF_DIV_SCALE?=10;
//這個(gè)類不能實(shí)例化
privateArith(){
}
/**
*?提供精確的加法運(yùn)算。
*?@param?v1?被加數(shù)
*?@param?v2?加數(shù)
*?@return?兩個(gè)參數(shù)的和
*/
publicstaticdoubleadd(doublev1,doublev2){
BigDecimal?b1?=newBigDecimal(Double.toString(v1));
BigDecimal?b2?=newBigDecimal(Double.toString(v2));
returnb1.add(b2).doubleValue();
}
/**
*?提供精確的減法運(yùn)算。
*?@param?v1?被減數(shù)
*?@param?v2?減數(shù)
*?@return?兩個(gè)參數(shù)的差
*/
publicstaticdoublesub(doublev1,doublev2){
BigDecimal?b1?=newBigDecimal(Double.toString(v1));
BigDecimal?b2?=newBigDecimal(Double.toString(v2));
returnb1.subtract(b2).doubleValue();
}
/**
*?提供精確的乘法運(yùn)算。
*?@param?v1?被乘數(shù)
*?@param?v2?乘數(shù)
*?@return?兩個(gè)參數(shù)的積
*/
publicstaticdoublemul(doublev1,doublev2){
BigDecimal?b1?=newBigDecimal(Double.toString(v1));
BigDecimal?b2?=newBigDecimal(Double.toString(v2));
returnb1.multiply(b2).doubleValue();
}
/**
*?提供(相對)精確的除法運(yùn)算,當(dāng)發(fā)生除不盡的情況時(shí),精確到
*?小數(shù)點(diǎn)以后10位,以后的數(shù)字四舍五入。
*?@param?v1?被除數(shù)
*?@param?v2?除數(shù)
*?@return?兩個(gè)參數(shù)的商
*/
publicstaticdoublediv(doublev1,doublev2){
returndiv(v1,v2,DEF_DIV_SCALE);
}
/**
*?提供(相對)精確的除法運(yùn)算。當(dāng)發(fā)生除不盡的情況時(shí),由scale參數(shù)指
*?定精度,以后的數(shù)字四舍五入。
*?@param?v1?被除數(shù)
*?@param?v2?除數(shù)
*?@param?scale?表示表示需要精確到小數(shù)點(diǎn)以后幾位。
*?@return?兩個(gè)參數(shù)的商
*/
publicstaticdoublediv(doublev1,doublev2,intscale){
if(scale<0){
thrownewIllegalArgumentException(
"The?scale?must?be?a?positive?integer?or?zero");
}
BigDecimal?b1?=newBigDecimal(Double.toString(v1));
BigDecimal?b2?=newBigDecimal(Double.toString(v2));
returnb1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
*?提供精確的小數(shù)位四舍五入處理。
*?@param?v?需要四舍五入的數(shù)字
*?@param?scale?小數(shù)點(diǎn)后保留幾位
*?@return?四舍五入后的結(jié)果
*/
publicstaticdoubleround(doublev,intscale){
if(scale<0){
thrownewIllegalArgumentException(
"The?scale?must?be?a?positive?integer?or?zero");
}
BigDecimal?b?=newBigDecimal(Double.toString(v));
BigDecimal?one?=newBigDecimal("1");
returnb.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
*?提供精確的類型轉(zhuǎn)換(Float)
*?@param?v?需要被轉(zhuǎn)換的數(shù)字
*?@return?返回轉(zhuǎn)換結(jié)果
*/
publicstaticfloatconvertsToFloat(doublev){
BigDecimal?b?=newBigDecimal(v);
returnb.floatValue();
}
/**
*?提供精確的類型轉(zhuǎn)換(Int)不進(jìn)行四舍五入
*?@param?v?需要被轉(zhuǎn)換的數(shù)字
*?@return?返回轉(zhuǎn)換結(jié)果
*/
publicstaticintconvertsToInt(doublev){
BigDecimal?b?=newBigDecimal(v);
returnb.intValue();
}
/**
*?提供精確的類型轉(zhuǎn)換(Long)
*?@param?v?需要被轉(zhuǎn)換的數(shù)字
*?@return?返回轉(zhuǎn)換結(jié)果
*/
publicstaticlongconvertsToLong(doublev){
BigDecimal?b?=newBigDecimal(v);
returnb.longValue();
}
/**
*?返回兩個(gè)數(shù)中大的一個(gè)值
*?@param?v1?需要被對比的第一個(gè)數(shù)
*?@param?v2?需要被對比的第二個(gè)數(shù)
*?@return?返回兩個(gè)數(shù)中大的一個(gè)值
*/
publicstaticdoublereturnMax(doublev1,doublev2){
BigDecimal?b1?=newBigDecimal(v1);
BigDecimal?b2?=newBigDecimal(v2);
returnb1.max(b2).doubleValue();
}
/**
*?返回兩個(gè)數(shù)中小的一個(gè)值
*?@param?v1?需要被對比的第一個(gè)數(shù)
*?@param?v2?需要被對比的第二個(gè)數(shù)
*?@return?返回兩個(gè)數(shù)中小的一個(gè)值
*/
publicstaticdoublereturnMin(doublev1,doublev2){
BigDecimal?b1?=newBigDecimal(v1);
BigDecimal?b2?=newBigDecimal(v2);
returnb1.min(b2).doubleValue();
}
/**
*?精確對比兩個(gè)數(shù)字
*?@param?v1?需要被對比的第一個(gè)數(shù)
*?@param?v2?需要被對比的第二個(gè)數(shù)
*?@return?如果兩個(gè)數(shù)一樣則返回0,如果第一個(gè)數(shù)比第二個(gè)數(shù)大則返回1,反之返回-1
*/
publicstaticintcompareTo(doublev1,doublev2){
BigDecimal?b1?=newBigDecimal(v1);
BigDecimal?b2?=newBigDecimal(v2);
returnb1.compareTo(b2);
}
}
參考:
http://justjavac.iteye.com/blog/1073775
http://www.iteye.com/problems/51604
http://blog.163.com/howl_prowler/blog/static/2661971520114553211964/
http://www.cnblogs.com/wingsless/p/3426108.html
http://zhidao.baidu.com/link?url=2L4pkHgVCXlwEeDM0GRHY2gYUwR9d2JC3knqxvHwdyrrdz_LwK92gVAaIy3hhKEQYdUwNjMLe_RJO3cl8sJvbcAnFK-_rMS4Oy_viystUEe