数值计算:注意精度、舍入和溢出问题
在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal
1. Double的坑
四则运算:
public static void main(String[] args) throws Exception {
System.out.println(0.1+0.2);
System.out.println(1.0-0.8);
System.out.println(4.015*100);
System.out.println(123.3/100);
double amount1 = 2.15;
double amount2 = 1.10;
if (amount1 - amount2 == 1.05)
System.out.println("OK");
}
可以看到,输出结果和我们预期的很不一样。比如,0.1+0.2 输出的不是 0.3 而是0.30000000000000004;再比如,对 2.15-1.10 和 1.05 判等,结果判等不成立。
- 出现这种问题的主要原因是,计算机是以二进制存储数值的,浮点数也不例外。Java 采用
了IEEE 754 标准实现浮点数的表达和运算,你可以通过这里查看数值转化为二进制的
结果。
- 比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就
是 0.1000000000000000055511151231257827021181583404541015625。对于计算
机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。
在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal
#### BigDecimal 四则运算:
public static void main(String[] args) throws Exception {
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));
System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));
System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));
}
可以看到 ,运算还是不精确,只不过是精度高了,使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化
public static void main(String[] args) throws Exception {
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));
System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100")));
System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));
}
2. 考虑浮点数舍入和格式化的方式
关于浮点类型的四舍五入 也是千奇百怪,让人莫不着头脑
public static void main(String[] args) throws Exception {
double num1 = 3.35;
float num2 = 3.35f;
System.out.println(String.format("%.1f", num1));//四舍五入
System.out.println(String.format("%.1f", num2));
}
如果我们希望使用其他舍入方式来格式化字符串的话,可以设置 DecimalFormat
public static void main(String[] args) throws Exception {
double num1 = 3.35; float num2 = 3.35f;
DecimalFormat format = new DecimalFormat("#.##");
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num1));
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num2));
}
因此,即使通过 DecimalFormat 来精确控制舍入方式,double 和 float 的问题也可能产
生意想不到的结果,所以浮点数避坑第二原则:浮点数的字符串格式化也要通过BigDecimal 进行。
public static void main(String[] args) throws Exception {
BigDecimal num1 = new BigDecimal("3.35");
BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN);
System.out.println(num2);
BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);
System.out.println(num3);
}
3. BigDecimal不能使用equals
案例 :
public static void main(String[] args) throws Exception {
BigDecimal a = new BigDecimal(0.00);
BigDecimal b = new BigDecimal(0);
boolean result = a.equals(b);
System.out.println("a equals b -->" + result);
BigDecimal c = new BigDecimal("0.00");
BigDecimal d = new BigDecimal("0");
boolean result1 = c.equals(d);
System.out.println("c equals d -->" + result1);
}
结果:
a equals b -->true
c equals d -->false
我们来看下 BigDecimal 的 equals 方法 源码,equals 比较的是 BigDecimal 的 value 和 scale,1.0 的 scale 是 1,1 的scale 是 0,所以结果一定是 false:
/**
* Compares this {@code BigDecimal} with the specified
* {@code Object} for equality. Unlike {@link
* #compareTo(BigDecimal) compareTo}, this method considers two
* {@code BigDecimal} objects equal only if they are equal in
* value and scale (thus 2.0 is not equal to 2.00 when compared by
* this method).
*
* @param x {@code Object} to which this {@code BigDecimal} is
* to be compared.
* @return {@code true} if and only if the specified {@code Object} is a
* {@code BigDecimal} whose value and scale are equal to this
* {@code BigDecimal}'s.
* @see #compareTo(java.math.BigDecimal)
* @see #hashCode
*/
@Override
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}
如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法
public static void main(String[] args) throws Exception {
BigDecimal c = new BigDecimal("0.00");
BigDecimal d = new BigDecimal("0");
boolean result2 = c.compareTo(d) == 0;
System.out.println("c compareTo d -->" + result2);
}
结果:
c compareTo d -->true
4. 数值溢出问题
数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的基本数值类型都有超
出表达范围的可能性。
对Long 的最大值 进行 + 1
public static void main(String[] args) throws Exception {
long l = Long.MAX_VALUE;
System.out.println(l + 1);
System.out.println(l + 1 == Long.MIN_VALUE);
}
结果: 输出结果是一个负数,因为 Long 的最大值 +1 变为了 Long 的最小值:
-9223372036854775808
true
改进:
public static void main(String[] args) throws Exception {
BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE));
System.out.println(i.add(BigInteger.ONE).toString());
}
结果 :
9223372036854775808
- 通过 BigInteger 对 Long 的最大值加 1 一点问题都没有
个人博客地址:http://blog.yanxiaolong.cn/