Java double转BigDecimal的注意事项说明
先上结论:
不要直接用double变量作为构造BigDecimal的参数。
线上有这么一段Java代码逻辑:
1,接口传来一个JSON串,里面有个数字:57.3。
2,解析JSON并把这个数字保存在一个float变量。
3,把这个float变量赋值给一个BigDecimal对象,用的是BigDecimal的double参数的构造:
newBigDecimal(doubleval)
4,把这个BigDecimal保存到MySQL数据库,字段类型是decimal(15,2)。
这段代码逻辑在线上跑了好久了,数据库保存的值是57.3也没什么问题,但是在今天debug的时候发现,第三步的BigDecimal对象保存的值并不是57.3,而是57.299999237060546875,很明显,出现了精度的问题。
至于数据库最终保存了正确的57.3完全是因为字段类型设置为2位小数,超过2位小数就四舍五入,所以才得到了正确的结果,相当于MySQL给我们把这个精度问题掩盖了。
总觉得这是个坑,所以研究了一下相关的知识。
首先是BigDecimal的double参数构造,在官方JDK文档中对这个构造是这么描述的:
publicBigDecimal(doubleval)
TranslatesadoubleintoaBigDecimalwhichistheexactdecimalrepresentationofthedouble'sbinaryfloating-pointvalue.ThescaleofthereturnedBigDecimalisthesmallestvaluesuchthat(10scale×val)isaninteger.
Notes:
Theresultsofthisconstructorcanbesomewhatunpredictable.OnemightassumethatwritingnewBigDecimal(0.1)inJavacreatesaBigDecimalwhichisexactlyequalto0.1(anunscaledvalueof1,withascaleof1),butitisactuallyequalto0.1000000000000000055511151231257827021181583404541015625.Thisisbecause0.1cannotberepresentedexactlyasadouble(or,forthatmatter,asabinaryfractionofanyfinitelength).Thus,thevaluethatisbeingpassedintotheconstructorisnotexactlyequalto0.1,appearancesnotwithstanding.
TheStringconstructor,ontheotherhand,isperfectlypredictable:writingnewBigDecimal("0.1")createsaBigDecimalwhichisexactlyequalto0.1,asonewouldexpect.Therefore,itisgenerallyrecommendedthattheStringconstructorbeusedinpreferencetothisone.
WhenadoublemustbeusedasasourceforaBigDecimal,notethatthisconstructorprovidesanexactconversion;itdoesnotgivethesameresultasconvertingthedoubletoaStringusingtheDouble.toString(double)methodandthenusingtheBigDecimal(String)constructor.Togetthatresult,usethestaticvalueOf(double)method.
Parameters:
val-doublevaluetobeconvertedtoBigDecimal.
Throws:
NumberFormatException-ifvalisinfiniteorNaN.
翻译一下大概是这样的:
1,BigDecimal(doubleval)构造,用double当参数来构造一个BigDecimal对象。
2,但是这个构造不太靠谱(unpredictable),你可能以为BigDecimal(0.1)就是妥妥的等于0.1,但是你以为你以为的就是你以为的?还真不是,BigDecimal(0.1)这货实际上等于0.1000000000000000055511151231257827021181583404541015625,因为准确的来说0.1本身不能算是一个double(其实0.1不能代表任何一个定长二进制分数)。
3,BigDecimal(Stringval)构造是靠谱的,BigDecimal(“0.1”)就是妥妥的等于0.1,推荐大家用这个构造。
4,如果你非得用一个double变量来构造一个BigDecimal,没问题,我们贴心的提供了静态方法valueOf(double),这个方法跟newDecimal(Double.toString(double))效果是一样的。
说白了就是别直接拿double变量做参数,最好使用String类型做参数或者使用静态方法valueOf(double),我写了个例子试了一下:
publicstaticvoidmain(String[]args){ floata=57.3f; BigDecimaldecimalA=newBigDecimal(a); System.out.println(decimalA); doubleb=57.3; BigDecimaldecimalB=newBigDecimal(b); System.out.println(decimalB); doublec=57.3; BigDecimaldecimalC=newBigDecimal(Double.toString(c)); System.out.println(decimalC); doubled=57.3; BigDecimaldecimalD=BigDecimal.valueOf(d); System.out.println(decimalD); }
输出结果:
57.299999237060546875 57.2999999999999971578290569595992565155029296875 57.3 57.3
以后还是尽量按照官方推荐的套路来,否则不知道什么时候又给自己挖坑了。
补充:double转bigDecimal精度问题
float的精度:2^237位
double的精度:2^5216位
十进制转二进制存在精度差
doubleg=12.35; BigDecimalbigG=newBigDecimal(g).setScale(1,BigDecimal.ROUND_HALF_UP);//期望得到12.4 System.out.println(“testG:”+bigG.doubleValue()); testG:12.3
原因:
定义doubleg=12.35;而在计算机中二进制表示可能这是样:定义了一个g=12.34444444444444449,
newBigDecimal(g)g还是12.34444444444444449 newBigDecimal(g).setScale(1,BigDecimal.ROUND_HALF_UP);得到12.3
正确的定义方式是使用字符串构造函数:
newBigDecimal(“12.35”).setScale(1,BigDecimal.ROUND_HALF_UP)
首先得从计算机本身去讨论这个问题。我们知道,计算机并不能识别除了二进制数据以外的任何数据。无论我们使用何种编程语言,在何种编译环境下工作,都要先把源程序翻译成二进制的机器码后才能被计算机识别。以上面提到的情况为例,我们源程序里的2.4是十进制的,计算机不能直接识别,要先编译成二进制。但问题来了,2.4的二进制表示并非是精确的2.4,反而最为接近的二进制表示是2.3999999999999999。原因在于浮点数由两部分组成:指数和尾数,这点如果知道怎样进行浮点数的二进制与十进制转换,应该是不难理解的。如果在这个转换的过程中,浮点数参与了计算,那么转换的过程就会变得不可预知,并且变得不可逆。我们有理由相信,就是在这个过程中,发生了精度的丢失。而至于为什么有些浮点计算会得到准确的结果,应该也是碰巧那个计算的二进制与十进制之间能够准确转换。而当输出单个浮点型数据的时候,可以正确输出,如
doubled=2.4; System.out.println(d);
输出的是2.4,而不是2.3999999999999999。也就是说,不进行浮点计算的时候,在十进制里浮点数能正确显示。这更印证了我以上的想法,即如果浮点数参与了计算,那么浮点数二进制与十进制间的转换过程就会变得不可预知,并且变得不可逆。
事实上,浮点数并不适合用于精确计算,而适合进行科学计算。这里有一个小知识:既然float和double型用来表示带有小数点的数,那为什么我们不称它们为“小数”或者“实数”,要叫浮点数呢?因为这些数都以科学计数法的形式存储。当一个数如50.534,转换成科学计数法的形式为5.053e1,它的小数点移动到了一个新的位置(即浮动了)。可见,浮点数本来就是用于科学计算的,用来进行精确计算实在太不合适了。
在《EffectiveJava》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal。使用BigDecimal并且一定要用String来够造。
BigDecimal用哪个构造函数?
BigDecimal(doubleval) BigDecimal(Stringval)
上面的API简要描述相当的明确,而且通常情况下,上面的那一个使用起来要方便一些。我们可能想都不想就用上了,会有什么问题呢?等到出了问题的时候,才发现参数是double的构造方法的详细说明中有这么一段:
Note:theresultsofthisconstructorcanbesomewhatunpredictable.OnemightassumethatnewBigDecimal(.1)isexactlyequalto.1,butitisactuallyequalto.1000000000000000055511151231257827021181583404541015625.Thisissobecause.1cannotberepresentedexactlyasadouble(or,forthatmatter,asabinaryfractionofanyfinitelength).Thus,thelongvaluethatisbeingpassedintotheconstructorisnotexactlyequalto.1,appearancesnonwithstanding.
The(String)constructor,ontheotherhand,isperfectlypredictable:newBigDecimal(".1")isexactlyequalto.1,asonewouldexpect.Therefore,itisgenerallyrecommendedthatthe(String)constructorbeusedinpreferencetothisone.
原来我们如果需要精确计算,非要用String来够造BigDecimal不可!
简单来说精确计算,需要用到bigDeicmal的String构造
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。