使用Java开发多年,感觉自己的水平也在不断提升,但是被Java狂虐却从来都没变过,而且任何一个Java的小角落,都能把我虐的体无完肤,但是无奈要靠Java吃饭,还得恬着脸继续使用下去。
说说最近遇到的问题,则于新工作属于互联网金融,所以里面涉及到了大量的资金计算,资金计算对数字要求的比较严谨,作为一个粗心而又自大的Java程序员,一直没把这个当回事儿,于是又被Java吊打一遍。
下面记录一下Java中四则运算的一些需要注意的小坑。
数学计算,免不了要想到 int
long
double
这种数据类型,但double自古在程序界就特立独行,表面上看像数学中学到的小数,实际上完全两码事儿。下面趟一下:
Double的坑
加法坑
double d1 = 0.1;
d1 += 0.1;d1 += 0.1;d1 += 0.1;d1 += 0.1;d1 += 0.1;d1 += 0.1;d1 += 0.1;d1 += 0.1; // 连着加了8次
System.out.println(d1);
0.8999999999999999
不是我们期望的0.9
哦
减法坑
double d1 = 1.0;
double d2 = 0.1;
d1 -= d2;d1 -= d2;d1 -= d2;d1 -= d2;d1 -= d2;d1 -= d2;d1 -= d2;d1 -= d2;d1 -= d2; // 连着减了9次0.1
System.out.println(d1);
0.10000000000000014
不是我们期望的0.1
哦
乘法坑
double d1 = 123213.0;
d1 *= 0.35;
System.out.println(d1);
43124.549999999996
不是我们期望的43124.55
哦
除法坑
double result = 11.4/12;
System.out.println(result);
0.9500000000000001
不是我们期望的0.95
哦
** 由此可见,如果需要对资金进行加减乘除四则运算,千万不能使用double
类型的,结果总是出你意料。**
大数转为字符串坑
再试个别的,数学运算,总会使用比较大的数字吧,试一个上千万的数字吧:
double bigValue = 12345678.9;
System.out.println(bigValue);
1.23456789E7
竟然变成科学计数法了… … 这不是给人看的格式啊。
所以,如果你想要显示大的浮点数,请使用DecimalFormat自己来格式化。
除0坑
从小学数学就知道不能除以0,看看double的表现:
result = 1.0/0.0;
System.out.println(result);
result = 0.0/0.0;
System.out.println(result);
Infinity
NaN
确实不能除以0,但是结果出乎你的意料之外吧,没错,Double提供了几个方法来检查:Double.isInfinite(double)
// 是否是Infinite数Double.isFinite(double)
// 是否有有限的数Double.isNaN(double)
// 是否是这个数
NaN = Not -a-Number ,两个NaN的数一比较,竟然不相等,是不是越来越有意思了。
比较是否相等的坑
System.out.println(1.1 == 1.1? "true": "false");
System.out.println(1.1 == 1.100000000000001? "true": "false");
System.out.println(1.1 == 1.10000000000000009? "true": "false");
true
false
true
又出乎意料了
总结:
在进行资金相关的运算时,千万不能使用Double类型,否则一个不小心就让你好看。
那么不使用double
,使用哪个类型合适呢?Java提供了一个叫BigDecimal
的对象,专门用来做计算使用,但是BigDecimal也不是那么容易驾驭的,使用不当,还是会被爆出翔。
附:浮点数在计算机中的表示方法,提示Double为何如此诡异。
BigDecimal的坑
坑1:实例化对象
BigDecimal num = new BigDecimal(0.3);
System.out.println(num);
0.299999999999999988897769753748434595763683319091796875
这不是玩人嘛,换一种方法:
BigDecimal num = new BigDecimal("0.3");
System.out.println(num);
0.3
这总算可以了。
坑2:除法
BigDecimal d = BigDecimal.ONE;
d.divide(new BigDecimal("3"));
Exception in thread “main” java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
这是什么鬼?为毛报个异常啊…
好吧,保留两位小数试试
BigDecimal d = BigDecimal.ONE;
d.divide(new BigDecimal("3"), 2, BigDecimal.ROUND_HALF_UP);
System.out.println(d);
1
咦,为何不是0.33,数没变呢?
原因是:BigDecimal是不可变的,每次运算后返回新的对象,再改:
BigDecimal d = BigDecimal.ONE;
d = d.divide(new BigDecimal("3"), 2, BigDecimal.ROUND_HALF_UP);
System.out.println(d);
0.33
长出一口气,总算可以了。
坑3:小数部分舍入进位的坑
四舍五入,有啥可说的?还真有得说,舍入进位可以参看RoundingMode
枚举,里面包含各种舍入进位,需要仔细研读哦,否则一脚一个坑。
这里要说的不是舍入进位的类型,而是说BigDecimal中提供的方法:num.setScale(2, BigDecimal.ROUND_HALF_EVEN);
num.setScale(2, RoundingMode.HALF_EVEN);
这里面 BigDecimal.ROUND_HALF_EVEN 和 RoungingMode.HALF_EVEN 这两个东西有啥区别?
答案是没有区别,一样使用,RoungingMode 枚举是JDK1.5以后,Java支持枚举类型后加的,里面有一个oldMode,保存的就是BigDecimal中对应的常量。
坑4:保留位数的坑
将小数部分保留N位这是一个非常常用的操作,以前使用double时,都是采用:
1. 乘10的N次方
2. round
3. 除以10的N次方
这种方法来算,牛逼闪闪的BigDecimal必然会提供舍入方法,而且使用起来很简单:
BigDecimal num = new BigDecimal("1.2345678");
num = num.setScale(2, BigDecimal.ROUND_HALF_EVEN);
System.out.println(num);
1.23
事情本来很容易结束了,但是眼睛贱又扫了一眼别的地方,MathContext
,这是啥?好像也是用来舍入的,试试:
MathContext ctx = new MathContext(2);
num = new BigDecimal("12.34");
num1 = new BigDecimal("56.78");
num = num.multiply(num1, ctx); // 12.34*56.78 = 700.6652
System.out.println(num.toString());
7.0E+2
不是我们想看到700.67
呀,改一下
MathContext ctx = new MathContext(3);
701
MathContext ctx = new MathContext(4);
700.7
MathContext ctx = new MathContext(5);
700.67
MathContext ctx = new MathContext(6);
700.665
明白了,这个数字是所有数字的位数,而不是小数点后的位数。
坑5:比较相等的坑
num = new BigDecimal("1");
num1 = new BigDecimal("1.0");
System.out.println(num.equals(num1)); // 竟然不相等呀
System.out.println(num.compareTo(num1)); // 0
false
0
BigDecimal的equals大大出乎意料,可不能随便使用equals方法来比较两个BigDecimal的大小了。还好compareTo还是好用的… …