图片来自网络
代码审查(CodeReview)是一种可以有效提高代码质量的方法。他可以帮助团体提高产品代码质量,提高产品的稳定性。更容易维护的代码会带来更少的技术债务,从整体上看,提高了软件开发和迭代的效率。
double精度丢失现象
程序开发时,Java开发人员常常使用double,但很多人都没有关注到double精度问题, 代码审查中遇到了double精度丢失,因此有了这边笔记
项目中,对方接口规范中明确费用使用String字符串类型,单位是分, 例如10元表示为字符串1000,代码走查时发现开发人员在业务系统中的DTO对象用Double来存储费用值;调用接口前将Double转换为String,仔细看发现转换处理存在问题,原始代码如下
//先乘以100转换成分
double fee = money*100;
//然后将分装换成字符串
itemDTO.setITEM_FEE(String.valueOf((int)fee));
运行下面的测试代码,会发现费用是2.51元,期望转换为字符串为251,但是实际上转换后的结果是250
public static void main(String[] args) {
Double money = 2.51d;
double fee = money * 100;
System.out.println(String.valueOf((int) fee));
}
//输出结果:250
这是由于double不是精确计算,存在精度丢失。运行下面的测试代码,会发现2.51d乘以100输出结果为250.99999999999997并不是251,再将250.99999999999997强转为整型int,那么小数点后的数值全部丢失 ,参考测试代码
public static void main(String[] args) {
Double money = 2.51d;
double fee = money * 100;
System.out.println(fee);
}
//输出结果:250.99999999999997
为什么会精度丢失
不论是double还是float都是浮点数, 计算机进行计算的时候采用二进制来计算,先将10进制转换成二进制,然后进行计算,最后再将二进制转换为十进制。浮点数会失去一定的精度
十进制转换二进制处理分整数部分和小数部分处理不同
1. 整数部分处理方法
除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数
例如25转换为二进制的步骤为
- 第一步将25除以2 商12余1
- 第二步将12除以2 商6余0
- 第三步将6除以2 ,商3余0
- 第四步将3除以2, 商1余1
-
第五步将1除以2,商0余1
- 第六步读数:(从下往上) 11001
2. 小数部分处理方法
乘2取整法,即将小数部分乘以2,然后取整数部分,剩下的小数部分继续乘以2,然后取整数部分,剩下的小数部分又乘以2,一直取到小数部分 为零为止。如果永远不能为零,就同十进制数的四舍五入一样,按照要求保留多少位小数时,就根据后面一位是0还是1,取舍,如果是零,舍掉,如果是1,向入一位。换句话说就是0舍1入。读数要从前面的整数读到后面的整数
例如将0.125转换为二进制数
-
第一步将0.125乘以2,得0.25,则整数部分为0,小数部分为0.25
-
第二步将小数部分0.25乘以2,得0.5,则整数部分为0,小数部分为0.5
-
第三步将小数部分0.5乘以2,得1.0,则整数部分为1,小数部分为0.0
-
第四步读数,从上往下,即为0.001
当然有的数值小数部分永远无法是0,那么就会做取舍操作
如何处理double精度丢失问题
有多重方法可以处理上述问题,但需要根据具体业务来定
1. Math函数使用
简单处理,在简单也业务背景下可以使用Math函数实现四舍五入处理
public static void main(String[] args) {
Double fee2 = Double.parseDouble("2.51");
System.out.println(Math.round(fee2 * 100));
}
//输出结果:251
常用Math函数方法
- Math.abs() 绝对值 ,例如 Math.abs(-3); 输出3,是整型就返回整型,浮点型就返回浮点型
- Math.round() 四舍五入,例如 Math.round(4.798); 输出5,返回的是整型
- Math.rint() 也是四舍五入,返回的是double型。但和Math.round() 不同的是,当Math.rint(2.5); 输出的是 2.0,rint判断四舍五入时,当如果距离两边的整数距离相同则取偶数,负数也同理。
- Math.random() 获取随机数,随机数的范围是 0.0 =< Math.random < 1.0,返回的是double型
- Math.pow() 计算次方,例如 Math.pow(4,2) ; 输出16.0 ,返回的是double型
- Math.ceil() 向上取整,ceil是天花板的意思,例如 Math.ceil(106.789); 输出107.0,返回的是double型
- Math.floor() 向下取整,floor是地板的意思,例如 Math.floor(93.881); 输出93.0,返回的是double型
- Math.E 是自然对数,e=2.7182818 Math.PI 是圆周率, Π=3.1415926
- Math.exp(x) 计算e^x, e是自然对数e=2.7182818 ,例如 Math.exp(1); 输出2.7182818
- Math.sqrt() 求算数平方根,例如 Math.sqrt(16); 输出4.0,返回的是double型
- Math.log() 计算以自然数为底数的对数值,自然数e=2.7182818,例如 Math.log(11.635);输出2.454,返回的是double型
- Math.log10() 计算以10为底数的对数值,例如 Math.log10(100); 输出2.0,返回的是double型
- Math.min() 返回两个数中的最小值 Math.max() 返回两个数中的最大值
- Math.toDegrees() 将弧度转换成角度,例如 Math.toDegrees(Math.PI/4); 输出 45.0,返回的是double型
- Math.toRadians() 将角度转换为弧度,例如 Math.toRadians(45); 输出 Π/4
- Math.sin() 计算正弦值,例如 Math.sin(Math.PI/2); 输出1.0;返回的是double型,
- Math.cos() 计算余弦值,例如 Math.cos(Math.PI); 输出-1.0;返回的是double型,
- Math.tan() 计算正切值,例如 Math.tan(Math.PI/4); 输出1.0;返回的是double型,
- ⭐需要注意的是括号里面的参数是写弧度值,而不是写角度制
- Math.asin() 通过正弦值计算角度,输出的是弧度制,例如 Math.asin(0.5); 输出 Π/6
- Math.acos() 通过余弦值计算角度,输出的是弧度制,例如 Math.acos(0.5); 输出 Π/3
- Math.atan() 通过正切值计算角度,输出的是弧度制,例如 Math.atan(1); 输出 Π/4
2.BigDecimal使用
Java在java.math包中提供的API类 java.math.BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应用中,需要对更大或者更小的数进行运算和处理。float和double只能用来做科学计算或者是工程计算
1.BigDecimal运算
在商业计算中因为精度问题可能会造成业务数据不准确,所以一般要用java.math.BigDecimal。BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象
-
add(BigDecimal) BigDecimal对象中的值相加,然后返回这个对象
-
subtract(BigDecimal) BigDecimal对象中的值相减,然后返回这个对象
-
multiply(BigDecimal) BigDecimal对象中的值相乘,然后返回这个对象
-
divide(BigDecimal) BigDecimal对象中的值相除,然后返回这个对象
2. BigDecimal构造器注意事项
-
BigDecimal(int) 创建一个具有参数所指定整数值的对象。
-
BigDecimal(double) 创建一个具有参数所指定双精度值的对象。
-
BigDecimal(long) 创建一个具有参数所指定长整数值的对象。
-
BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象
参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1,但是它实际上等于0.1000000000000000055511151231。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。
String 构造方法是完全可预知的:写入 newBigDecimal(“0.1”) 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言, 通常建议优先使用String构造方法。
当double必须用作BigDecimal的源时,请注意,此构造方法提供了一个准确转换;先使用Double.toString(double)方法,然后使用BigDecimal(String)构造方法,将double转换为String。也可以使用static valueOf(double)方法。
参考下面的测试代码:
public static void main(String[] args) {
BigDecimal b1=new BigDecimal(2.51d);
System.out.println(b1);
System.out.println(b1.multiply(new BigDecimal(100)));
System.out.println(b1.multiply(new BigDecimal(100)).toBigInteger());
System.out.println();
BigDecimal b2=new BigDecimal(Double.toString(2.51d));
System.out.println(b2);
System.out.println(b2.multiply(new BigDecimal("100")));
System.out.println(b2.multiply(new BigDecimal("100")).toBigInteger());
}
测试结果
2.5099999999999997868371792719699442386627197265625
250.9999999999999786837179271969944238662719726562500
250
2.51
251.00
251
3.BigDecimal四舍五入
BigDecimal.setScale()方法用于格式化小数点
-
setScale(1)表示保留一位小数,默认用四舍五入方式
-
setScale(1,BigDecimal.ROUND_DOWN)直接删除多余的小数位,如2.35会变成2.3
-
setScale(1,BigDecimal.ROUND_UP)进位处理,2.35变成2.4
-
setScaler(1,BigDecimal.ROUND_HALF_DOWN)四舍五入,2.35变成2.3,如果是5则向下取舍
-
setScaler(1,BigDecimal.ROUND_CEILING)接近正无穷大的舍入
-
setScaler(1,BigDecimal.ROUND_FLOOR)接近负无穷大的舍入,数字>0和ROUND_UP作用一样,数字<0和ROUND_DOWN作用一样
-
setScaler(1,BigDecimal.ROUND_HALF_EVEN)向最接近的数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入
上一篇:Java中serialVersionUID作用