BigDecimal setScale()设置无效 scale()取得的值不是setScale()设置的值
最近查看rebate数据时,发现一个bug,主要现象是,当扣款支付宝的账号款项时,返回的是数字的金额为元,而数据库把金额存储为分,这中间要做元与分的转化,这个转化规则很简单,就是*100的,所以一开始代码很简单,如下。
- Float f = Float.valueOf(s);
- f =f*100;
- Long result = f.longValue();
Float f = Float.valueOf(s);
f =f*100;
Long result = f.longValue();
当s=”9.86”时,杯具出现了,result的结果为985而不是986,float的精度损失导致float(985.99994)转化为整形时,丢掉小数部分成为985,简单的方法,我们可以提高精度使用双精度的double类型,提高精度,比如
- Double d = Double.valueOf(s);
- d = d*100;
- Long result = d.longValue();
Double d = Double.valueOf(s);
d = d*100;
Long result = d.longValue();
当s=”9.86”时,确实能够得到正确结果,但是当s=”1219.86”时,这时候由于精度问题导致最终的result为121985为不是121986。当时以为使用double解决的问题,其实隐藏更隐蔽的bug。 针对这样的问题,如果使用C/C++语言,那么通用解决方案可以这样。
- Double d = Double.valueOf(s);
- d = d*100+0.5;// 注意这里,我们使用的是+0.5的形式。
- Long result = d.longValue();
Double d = Double.valueOf(s);
d = d*100+0.5;// 注意这里,我们使用的是+0.5的形式。
Long result = d.longValue();
但是,我们使用的java语言,java应该有更优雅的解决方案,那就是BigDecimal。
使用BigDecimal的解决方案成这个样子
- Double dd= Double.valueOf(s);
- BigDecimal bigD = new BigDecimal(dd);
- bigD = bigD.multiply(new BigDecimal(100));
- Long result = bigD.longValue();
Double dd= Double.valueOf(s);
BigDecimal bigD = new BigDecimal(dd);
bigD = bigD.multiply(new BigDecimal(100));
Long result = bigD.longValue();
狂晕,输出结果是985为不是986,打印bigD
- System.out.println(bigD.toString());
System.out.println(bigD.toString());
输出如下
- 985.9999999999999431565811391919851303100585937500
985.9999999999999431565811391919851303100585937500
不会再加上一个BigDecimal(0.5)吧。我相信在使用过BigDecimal过程中,肯定有那里不对的地方,multiply方法中可以传入精度,那就构造MathContext对象,修改如下。
- Double dd= Double.valueOf(s);
- BigDecimal bigD = new BigDecimal(dd);
- MathContextmc = new MathContext(4,RoundingMode.HALF_UP);
- //4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入
- bigD= bigD.multiply(new BigDecimal(100),mc);
- Long result = bigD.longValue();
Double dd= Double.valueOf(s);
BigDecimal bigD = new BigDecimal(dd);
MathContextmc = new MathContext(4,RoundingMode.HALF_UP);
//4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入
bigD= bigD.multiply(new BigDecimal(100),mc);
Long result = bigD.longValue();
最后结果输出为986,貌似已经找到完成解决方案,其实不然,注意到MathContext中的4了嘛?这是因为我们保留4位有效数字,假如我们输入的数字是大于4的,比如1219.86,最终输出结果是122000,这是因为1219.86保留4位有效数字时,第四位的9四舍五入,除去精确位补零,所以最终结果成了122000。问题就成了,我们必须知道元变分后的最终有效位数,”9.86”,有效位数是4,”19.86”有效位数是5,把字符串s的长度传过去就可以了,那么代码如下
- Double dd =Double.valueOf(s);
- BigDecimalbigD = new BigDecimal(dd);
- MathContextmc = new MathContext(s.length(),RoundingMode.HALF_UP);
- //4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入
- bigD= bigD.multiply(new BigDecimal(100),mc);
- Long result = bigD.longValue();
Double dd =Double.valueOf(s);
BigDecimalbigD = new BigDecimal(dd);
MathContextmc = new MathContext(s.length(),RoundingMode.HALF_UP);
//4表示取四位有效数字,RoundingMode.HALF_UP表示四舍五入
bigD= bigD.multiply(new BigDecimal(100),mc);
Long result = bigD.longValue();
至此,已经可以得到一个正确的元转分的代码,但是这里的s.length()终归不让人感觉舒服,接下来,我们探索BigDecimal原理,尝试用更优雅的方法解决这个问题。 BigDecimal,不可变的、任意精度的有符号十进制数。BigDecimal 由任意精度的整数非标度值 和 32 位的整数标度(scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂。因此,BigDecimal 表示的数值是 (unscaledValue × 10-scale)。我们知道BigDecimal有三个主要的构造函数
|
1 |
public BigDecimal(double val) |
将double表示形式转换为BigDecimal |
|
2 |
public BigDecimal(int val) |
将int表示形式转换为BigDecimal |
|
3 |
public BigDecimal(String val) |
将字符串表示形式转换为BigDecimal |
通过这三个构造函数,可以把double类型,int类型,String类型构造为BigDecimal对象,在BigDecimal对象内通过BigIntegerintVal存储传递对象数字部分,通过int scale;记录小数点位数,通过int precision;记录有效位数(默认为0)。 BigDecimal的加减乘除就成了BigInteger与BigInteger之间的加减乘除,浮点数的计算也转化为整形的计算,可以大大提供性能,并且通过BigInteger可以保存大数字,从而实现真正大十进制的计算,在整个计算过程中,还涉及scale的判断和precision判断从而确定最终输出结果。 我们先看一个例子
- BigDecimal d1 = new BigDecimal(0.6);
- BigDecimal d2 = new BigDecimal(0.4);
- BigDecimal d3 = d1.divide(d2);
- System.out.println(d3);
BigDecimal d1 = new BigDecimal(0.6);
BigDecimal d2 = new BigDecimal(0.4);
BigDecimal d3 = d1.divide(d2);
System.out.println(d3);
大家猜一下,以上输出结果是?再接着看下面的代码
- BigDecimal d1 = new BigDecimal(“0.6”);
- BigDecimal d2 = new BigDecimal(“0.4”);
- BigDecimal d3 = d1.divide(d2);
- System.out.println(d3);
BigDecimal d1 = new BigDecimal(“0.6”);
BigDecimal d2 = new BigDecimal(“0.4”);
BigDecimal d3 = d1.divide(d2);
System.out.println(d3);
看似相似的代码,其结果完全不同,第一个例子中,抛出异常。第二个例子中,输出打印结果为1.5。造成这种差异的主要原因是第一个例子中的创建BigDecimal时,0.6和0.4是浮动类型的,浮点型放入BigDecimal内,其存储值为
- 0.59999999999999997779553950749686919152736663818359375
- 0.40000000000000002220446049250313080847263336181640625
0.59999999999999997779553950749686919152736663818359375
0.40000000000000002220446049250313080847263336181640625
这两个浮点数相除时,由于除不尽,而又没有设置精度和保留小数点位数,导致抛出异常。而第二个例子中0.6和0.4是字符串类型,由于BigDecimal存储特性,通过BigInteger记录BigDecimal的值,所以,0.6和0.4可以非常正确的记录为
- 0.6
- 0.4
0.6
0.4
两者相除得出1.5来。 对于第一个例子,如果我们想得到正确结果,可以这样来
- BigDecimal d1 = new BigDecimal(0.6);
- BigDecimal d2 = new BigDecimal(0.4);
- BigDecimal d3 = d1.divide(d2, 1, BigDecimal.ROUND_HALF_UP);
BigDecimal d1 = new BigDecimal(0.6);
BigDecimal d2 = new BigDecimal(0.4);
BigDecimal d3 = d1.divide(d2, 1, BigDecimal.ROUND_HALF_UP);
现在看我们留下的那个问题,使用更优雅的方式解决元转化为分的方式,上一个问题中,我们通过传递s.length()从而获得精度,如果之前的s是double类型的,那边这样的方式就会有问题,通过上面的例子,我们可以调整为一下的通用方式
- Double dd= Double.valueOf(s);
- BigDecimal bigD = new BigDecimal(dd);
- bigD = bigD.multiply(newBigDecimal(100)). divide(1, 1, BigDecimal.ROUND_HALF_UP);
- Long result = bigD.longValue();
Double dd= Double.valueOf(s);
BigDecimal bigD = new BigDecimal(dd);
bigD = bigD.multiply(newBigDecimal(100)). divide(1, 1, BigDecimal.ROUND_HALF_UP);
Long result = bigD.longValue();
我们通过/1,然后设置保留小数点方式,以及设置数字保留模式,从而得到两个数乘积的小数部分。还有以下模式
枚举常量摘要 ROUND_CEILING 向正无限大方向舍入的舍入模式。 ROUND_DOWN 向零方向舍入的舍入模式。 ROUND_FLOOR 向负无限大方向舍入的舍入模式。 ROUND_HALF_DOWN 向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向下舍入。 ROUND_HALF_EVEN 向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。 ROUND_HALF_UP 向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向上舍入。 ROUND_UNNECESSARY 用于断言请求的操作具有精确结果的舍入模式,因此不需要舍入。(默认模式) ROUND_UP 远离零方向舍入的舍入模式。
总结: 1:尽量避免传递double类型,有可能话,尽量使用int和String类型。 2:做乘除计算时,一定要设置精度和保留小数点位数。 3:BigDecimal计算时,单独放到try catch内。
public static BigDecimal double2BigDecimal(double d, int scale){
BigDecimal db = new BigDecimal();
db.setScale(scale, BigDecimal.ROUND_HALF_UP);
return db;
}
以上方法返回的BigDecimal.scale()方法并不是我指定的值,于是修改成以下方法解决:
public static BigDecimal double2BigDecimal(double d, int scale){
BigDecimal db = new BigDecimal();
return db.divide(new BigDecimal(1), scale, BigDecimal.ROUND_HALF_UP); ;
}
参考资料 IEEE 754简介: http://baike.baidu.com/view/1698149.htm IEEE 754官方协议:http://grouper.ieee.org/groups/754/ BigDecimal函数列表:http://hi.baidu.com/logan9999/item/eeaea014677323fd9c778abd 浮点数与IEEE 754: http://www.cnblogs.com/kingwolfofsky/archive/2011/07/21/2112299.html MathContext:http://doc.java.sun.com/DocWeb/api/all/java.math.MathContext BigDecimal:http://doc.java.sun.com/DocWeb/api/all/java.math.BigDecimal
BigDecimal setScale()设置无效 scale()取得的值不是setScale()设置的值的更多相关文章
- EditText中imeOptions属性使用及设置无效解决
虽然通常输入法软键盘右下角会是回车按键 但我们经常会看到点击不同的编辑框,输入法软键盘右下角会有不同的图标 点击浏览器网址栏的时候,输入法软键盘右下角会变成“GO”或“前往” 而我们点击Google搜 ...
- CSS布局与定位——height百分比设置无效/背景色不显示
CSS布局与定位——height百分比设置无效/背景色不显示 html元素属性width和height的值有两种表达方式,一是固定像素如“100px”,一是百分比如“80%”, 使用百分比的好处是元素 ...
- FineUI小技巧(2)将表单内全部字段禁用、只读、设置无效标识
需求描述 对表单内的所有字段进行操作也是常见需求,这些操作有: 禁用:表单字段变灰,不响应用户动作. 只读:表单字段不变灰,但不接受用户输入(实际上是设置DOM节点的readonly属性),有触发器的 ...
- div宽度设置无效问题解决
问题描述: 要设置两个div在同一行显示,都加入了display:inline样式,但是其中一个div的宽度设置无效,在浏览器显示它的宽度始终是1003px. 解决办法: 方法1/给div加入样式:f ...
- Windows上最大传输单元MTU值的查看和设置
最近使用ssh工具在VPN环境下连接一个生产环境的Linux主机的时候,发现经常出现输入命令后卡死的情况.最开始以为是Linux主机的问题,问了一些老同事之后发现原来是我自己电脑的最大传输单元MTU和 ...
- postman插件部分Header设置无效的解决办法
在使用chrome的postman插件模拟http请求的时候,碰到了设置的部分Headers无效的问题,比如说Referer设置后就无效,经过查询发现了问题原因,原因的具体说明参考postman官网的 ...
- JQuery/JS select标签动态设置选中值、设置禁止选择 button按钮禁止点击 select获取选中值
//**1.设置选中值:(根据索引确定选中值)**// var osel=document.getElementById("selID"); //得到select的ID var o ...
- 设置UniDbGrid的整行显示颜色,如果某字段值是我们的控制字段
设置UniDbGrid的整行显示颜色,如果某字段值是我们的控制字段,使用下列判断设置更快捷一点: procedure TUniForm.UniDBGridDrawColumnCell(Sender: ...
- ScrollView子控件高度设置无效
ScrollView子控件高度设置无效 简述 项目中引入了第三方的下拉刷新包PullToRefreshScrollView. 由于我之前布局未考虑下拉刷新功能.后来暂时发现添加上去,发现.子控件的高度 ...
随机推荐
- Win10双网卡不双待攻略
声明:本文与个人的技术兴趣毫无关系,只是因为有迫不得已的实际需求,才不得不想办法.所谓"艰难困苦,玉汝于成",大概就是这个道理.(PS:标题我没有写错,不是双网卡双待,就是双网卡但 ...
- elasticsearch 6.x 安装与注意
1. 下载,解压 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.5.4.tar.gztar -zx ...
- android的计算器
今天我闲着无聊,便想仿照Iphone的计算器自己写一个出来玩玩,于是就开动脑经,来场头脑风暴了!我拿什么了写呢?wp?这是个不错的选择,但是我最近在研究安卓的开发(求各位MSP不要烧我,我买不起MAC ...
- Mysql工作流程分析
Mysql工作流程图 工作流程分析 1. 所有的用户连接请求都先发往连接管理器 2. 连接管理器 (1)一直处于侦听状态 (2)用于侦听用户请求 3. 线程管理器 (1)因为每个用户 ...
- 常用命令(Linux、Android、adb)
1. Linux 2. Android 1. pm (package Manager) 1). 安装APK(一般不使用,用adb我感觉更好) pm install -r /data/card/Ap ...
- [转]jquerUI Dialog中隐藏标题栏的关闭"X"按钮
本文转自:http://blog.chinaunix.net/uid-144593-id-2804206.html 方法1. 在CSS文件中添加如下样式既可 .ui-dialog-titlebar-c ...
- LINQ-from多from
简: LINQ全称是Language Integrated Query,中文“语言集成查询”.LINQ是一种查询技术,有LINQ toSQL.LINQ to Object. LINQ to ADO. ...
- Jsp&Servlet入门级项目全程实录第1讲
惯例广告一发,对于初学真,真的很有用www.java1234.com,去试试吧! 1.jdbc数据库连接(略) 2.登录表单 2.1设置内边距 <dir style="padding- ...
- [javaSE] 网络编程(URLConnection)
获取URL对象,new出来,构造参数:String的路径 调用URL对象的openConnection()方法,获取URLConnection对象 调用URLConnection对象的getInput ...
- 基于环境变量为多用户配置不同的JDK(win)
情景 同一服务器同时部署不同项目需要使用不同的JRE 环境 Windows Server 2008 需求 用户admin 需要使用jdk1.8 用户admin1 需要使用jdk1.7 解决 通过配置用 ...