一個關(guān)于浮點數(shù)多次累加的科普文章的搬運

此篇文章搬運于:http://blog.csdn.net/aya19880214/article/details/45891581?(更多詳情點進(jìn)去閱讀,此篇僅作搬運)

Java浮點數(shù)float和double精確計算的精度誤差問題總結(jié)

1、float整數(shù)計算誤差

案例:會員積分字段采用float類型,導(dǎo)致計算會員積分時,7位整數(shù)的數(shù)據(jù)計算結(jié)果出現(xiàn)誤差。

原因:超出float精度范圍,無法精確計算。

float和double的精度是由尾數(shù)的位數(shù)來決定的。浮點數(shù)在內(nèi)存中是按科學(xué)計數(shù)法來存儲的,其整數(shù)部分始終是一個隱含著的“1”,由于它是不變的,故不能對精度造成影響。

float:2^23 = 8388608,一共七位,這意味著最多能有7位有效數(shù)字,但絕對能保證的為6位,也即float的精度為6~7位有效數(shù)字;

double:2^52 = 4503599627370496,一共16位,同理,double的精度為15~16位。

難道只是位數(shù)多大的問題,字段類型換成double就可以解決嗎?對于本案例是這樣,因為都是整數(shù)計算,但如果有小數(shù)位,就不一定了,見下面案例。

2、double小數(shù)轉(zhuǎn)bigdecimal后四舍五入計算有誤差

案例:

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ìn)制表示可能這是樣:定義了一個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做四則運算誤差

案例:

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

原因:

那么為什么會出現(xiàn)精度丟失呢?在查閱了一些資料以后,我稍微有了一些頭緒,下面是本人的愚見,僅供參考。

首先得從計算機(jī)本身去討論這個問題。我們知道,計算機(jī)并不能識別除了二進(jìn)制數(shù)據(jù)以外的任何數(shù)據(jù)。無論我們使用何種編程語言,在何種編譯環(huán)境下工作,都要先把源程序翻譯成二進(jìn)制的機(jī)器碼后才能被計算機(jī)識別。以上面提到的情況為例,我們源程序里的2.4是十進(jìn)制的,計算機(jī)不能直接識別,要先編譯成二進(jìn)制。但問 題來了,2.4的二進(jìn)制表示并非是精確的2.4,反而最為接近的二進(jìn)制表示是2.3999999999999999。原因在于浮點數(shù)由兩部分組成:指數(shù)和尾數(shù),這點如果知道怎樣進(jìn)行浮點數(shù)的二進(jìn)制與十進(jìn)制轉(zhuǎn)換,應(yīng)該是不難理解的。如果在這個轉(zhuǎn)換的過程中,浮點數(shù)參與了計算,那么轉(zhuǎn)換的過程就會變得不可預(yù) 知,并且變得不可逆。我們有理由相信,就是在這個過程中,發(fā)生了精度的丟失。而至于為什么有些浮點計算會得到準(zhǔn)確的結(jié)果,應(yīng)該也是碰巧那個計算的二進(jìn)制與 十進(jìn)制之間能夠準(zhǔn)確轉(zhuǎn)換。而當(dāng)輸出單個浮點型數(shù)據(jù)的時候,可以正確輸出,如

double d = 2.4;

System.out.println(d);

輸出的是2.4,而不是2.3999999999999999。也就是說,不進(jìn)行浮點計算的時候,在十進(jìn)制里浮點數(shù)能正確顯示。這更印證了我以上的想法,即如果浮點數(shù)參與了計算,那么浮點數(shù)二進(jìn)制與十進(jìn)制間的轉(zhuǎn)換過程就會變得不可預(yù)知,并且變得不可逆。

事實上,浮點數(shù)并不適合用于精確計算,而適合進(jìn)行科學(xué)計算。這里有一個小知識:既然float和double型用來表示帶有小數(shù)點的數(shù),那為什么我們不稱 它們?yōu)椤靶?shù)”或者“實數(shù)”,要叫浮點數(shù)呢?因為這些數(shù)都以科學(xué)計數(shù)法的形式存儲。當(dāng)一個數(shù)如50.534,轉(zhuǎn)換成科學(xué)計數(shù)法的形式為5.053e1,它 的小數(shù)點移動到了一個新的位置(即浮動了)。可見,浮點數(shù)本來就是用于科學(xué)計算的,用來進(jìn)行精確計算實在太不合適了。

4、bigdecimal構(gòu)造函數(shù)使用不當(dāng)帶來異常

案例:

BigDecimal其中一個構(gòu)造函數(shù)以雙精度浮點數(shù)作為輸入,另一個以整數(shù)和換算因子作為輸入,還有一個以小數(shù)的String表示作為輸入。要小心使用BigDecimal(double)構(gòu)造函數(shù),因為如果不了解它,會在計算過程中產(chǎn)生舍入誤差。請使用基于整數(shù)或String的構(gòu)造函數(shù)。

如果使用BigDecimal(double)構(gòu)造函數(shù)不恰當(dāng),在傳遞給 JDBCsetBigDecimal()方法時,會造成似乎很奇怪的 JDBC 驅(qū)動程序中的異常。例如,考慮以下 JDBC 代碼,該代碼希望將數(shù)字0.01存儲到小數(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í)行這段似乎無害的代碼時會拋出一些令人迷惑不解的異常(這取決于具體的 JDBC 驅(qū)動程序),因為0.01的雙精度近似值會導(dǎo)致大的換算值,這可能會使 JDBC 驅(qū)動程序或數(shù)據(jù)庫感到迷惑。JDBC 驅(qū)動程序會產(chǎn)生異常,但可能不會說明代碼實際上錯在哪里,除非意識到二進(jìn)制浮點數(shù)的局限性。相反,使用BigDecimal("0.01")或BigDecimal(1,

2)構(gòu)造BigDecimal來避免這類問題,因為這兩種方法都可以精確地表示小數(shù)。

5、解決浮點數(shù)精確計算有誤差的方法

在《Effective?? Java》這本書中也提到這個原則,float和double只能用來做科學(xué)計算或者是工程計算,在商業(yè)計算中我們要用java.math.BigDecimal。使用BigDecimal并且一定要用String來夠造。

BigDecimal用哪個構(gòu)造函數(shù)?

BigDecimal(double?val)

BigDecimal(String?val)

上面的API簡要描述相當(dāng)?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)绻枰_計算,非要用String來夠造BigDecimal不可!

6、定點數(shù)和浮點數(shù)的區(qū)別

在計算機(jī)系統(tǒng)的發(fā)展過程中,曾經(jīng)提出過多種方法表達(dá)實數(shù)。典型的比如相對于浮點數(shù)的定點數(shù)(Fixed Point Number)。在這種表達(dá)方式中,小數(shù)點固定的位于實數(shù)所有數(shù)字中間的某個位置。貨幣的表達(dá)就可以使用這種方式,比如 99.00 或者 00.99 可以用于表達(dá)具有四位精度(Precision),小數(shù)點后有兩位的貨幣值。由于小數(shù)點位置固定,所以可以直接用四位數(shù)值來表達(dá)相應(yīng)的數(shù)值。SQL 中的 NUMBER 數(shù)據(jù)類型就是利用定點數(shù)來定義的。還有一種提議的表達(dá)方式為有理數(shù)表達(dá)方式,即用兩個整數(shù)的比值來表達(dá)實數(shù)。

定點數(shù)表達(dá)法的缺點在于其形式過于僵硬,固定的小數(shù)點位置決定了固定位數(shù)的整數(shù)部分和小數(shù)部分,不利于同時表達(dá)特別大的數(shù)或者特別小的數(shù)。最終,絕大多數(shù)現(xiàn)代的計算機(jī)系統(tǒng)采納了所謂的浮點數(shù)表達(dá)方式。這種表達(dá)方式利用科學(xué)計數(shù)法來表達(dá)實數(shù),即用一個尾數(shù)(Mantissa ),一個基數(shù)(Base),一個指數(shù)(Exponent)以及一個表示正負(fù)的符號來表達(dá)實數(shù)。比如 123.45 用十進(jìn)制科學(xué)計數(shù)法可以表達(dá)為 1.2345 × 102 ,其中 1.2345 為尾數(shù),10 為基數(shù),2 為指數(shù)。浮點數(shù)利用指數(shù)達(dá)到了浮動小數(shù)點的效果,從而可以靈活地表達(dá)更大范圍的實數(shù)。

在MySQL中使用浮點數(shù)類型和定點數(shù)類型來表示小數(shù)。浮點數(shù)類型包括單精度浮點數(shù)(FLOAT型)和雙精度浮點數(shù)(DOUBLE型)。定點數(shù)類型就是DECIMAL型。MySQL的浮點數(shù)類型和定點數(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,也就是說,定點數(shù)的存儲空間是根據(jù)其精度決定的。

7、bigdecimal比等方法

如浮點類型一樣,BigDecimal也有一些令人奇怪的行為。尤其在使用equals()方法來檢測數(shù)值之間是否相等時要小心。equals()方法認(rèn)為,兩個表示同一個數(shù)但換算值不同(例如,100.00和100.000)的BigDecimal值是不相等的。然而,compareTo()方法會認(rèn)為這兩個數(shù)是相等的,所以在從數(shù)值上比較兩個BigDecimal值時,應(yīng)該使用compareTo()而不是equals()。

另外還有一些情形,任意精度的小數(shù)運算仍不能表示精確結(jié)果。例如,1除以9會產(chǎn)生無限循環(huán)的小數(shù).111111...。出于這個原因,在進(jìn)行除法運算時,BigDecimal可以讓您顯式地控制舍入。movePointLeft()方法支持 10 的冪次方的精確除法。

與零比較:

int r=big_decimal.compareTo(BigDecimal.Zero); //和0,Zero比較

if(r==0) //等于

if(r==1) //大于

if(r==-1) //小于

8、簡化bigdecimal計算的小工具類

如果我們要做一個加法運算,需要先將兩個浮點數(shù)轉(zhuǎn)為String,然后夠造成BigDecimal,在其中一個上調(diào)用add方法,傳入另一個作為參數(shù),然后把運算的結(jié)果(BigDecimal)再轉(zhuǎ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對象的加減乘除,四舍五入等運算的工具類

*?@author?ameyume

*

*/

publicclassArith?{

/**

*?由于Java的簡單類型不能夠精確的對浮點數(shù)進(jìn)行運算,這個工具類提供精

*?確的浮點數(shù)運算,包括加減乘除和四舍五入。

*/

//默認(rèn)除法運算精度

privatestaticfinalintDEF_DIV_SCALE?=10;

//這個類不能實例化

privateArith(){

}

/**

*?提供精確的加法運算。

*?@param?v1?被加數(shù)

*?@param?v2?加數(shù)

*?@return?兩個參數(shù)的和

*/

publicstaticdoubleadd(doublev1,doublev2){

BigDecimal?b1?=newBigDecimal(Double.toString(v1));

BigDecimal?b2?=newBigDecimal(Double.toString(v2));

returnb1.add(b2).doubleValue();

}

/**

*?提供精確的減法運算。

*?@param?v1?被減數(shù)

*?@param?v2?減數(shù)

*?@return?兩個參數(shù)的差

*/

publicstaticdoublesub(doublev1,doublev2){

BigDecimal?b1?=newBigDecimal(Double.toString(v1));

BigDecimal?b2?=newBigDecimal(Double.toString(v2));

returnb1.subtract(b2).doubleValue();

}

/**

*?提供精確的乘法運算。

*?@param?v1?被乘數(shù)

*?@param?v2?乘數(shù)

*?@return?兩個參數(shù)的積

*/

publicstaticdoublemul(doublev1,doublev2){

BigDecimal?b1?=newBigDecimal(Double.toString(v1));

BigDecimal?b2?=newBigDecimal(Double.toString(v2));

returnb1.multiply(b2).doubleValue();

}

/**

*?提供(相對)精確的除法運算,當(dāng)發(fā)生除不盡的情況時,精確到

*?小數(shù)點以后10位,以后的數(shù)字四舍五入。

*?@param?v1?被除數(shù)

*?@param?v2?除數(shù)

*?@return?兩個參數(shù)的商

*/

publicstaticdoublediv(doublev1,doublev2){

returndiv(v1,v2,DEF_DIV_SCALE);

}

/**

*?提供(相對)精確的除法運算。當(dāng)發(fā)生除不盡的情況時,由scale參數(shù)指

*?定精度,以后的數(shù)字四舍五入。

*?@param?v1?被除數(shù)

*?@param?v2?除數(shù)

*?@param?scale?表示表示需要精確到小數(shù)點以后幾位。

*?@return?兩個參數(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ù)點后保留幾位

*?@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();

}

/**

*?返回兩個數(shù)中大的一個值

*?@param?v1?需要被對比的第一個數(shù)

*?@param?v2?需要被對比的第二個數(shù)

*?@return?返回兩個數(shù)中大的一個值

*/

publicstaticdoublereturnMax(doublev1,doublev2){

BigDecimal?b1?=newBigDecimal(v1);

BigDecimal?b2?=newBigDecimal(v2);

returnb1.max(b2).doubleValue();

}

/**

*?返回兩個數(shù)中小的一個值

*?@param?v1?需要被對比的第一個數(shù)

*?@param?v2?需要被對比的第二個數(shù)

*?@return?返回兩個數(shù)中小的一個值

*/

publicstaticdoublereturnMin(doublev1,doublev2){

BigDecimal?b1?=newBigDecimal(v1);

BigDecimal?b2?=newBigDecimal(v2);

returnb1.min(b2).doubleValue();

}

/**

*?精確對比兩個數(shù)字

*?@param?v1?需要被對比的第一個數(shù)

*?@param?v2?需要被對比的第二個數(shù)

*?@return?如果兩個數(shù)一樣則返回0,如果第一個數(shù)比第二個數(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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