实际编程中,经常会遇到浮点数,这里以float为例,简单介绍下浮点数的一些基本知识。
IEEE754标准规定,浮点数包含“符号Sign”、“指数Exponent ”和“尾数Mantissa”三部分。
java数据类型 | 符号位 | 指数位 | 尾数位 | 长度 |
---|---|---|---|---|
float | 1 | 8 | 23 | 32 |
double | 1 | 11 | 52 | 64 |
float和double的尾数都含有一个隐含位,对于有限数,如果指数位不全为0 则尾数位首位 +1 ,如果指数位全为0,则尾数位首位 + 0,相当于多存储了一位数据。有限数的概念后面会介绍。
二进制浮点数实际表示为:
± X n X n − 1 . . . X 0 . X − 1 X − 2 . . . X − m = ± X n . X n − 1 . . . X 0 X − 1 X − 2 . . . X − m × 2 n ± X_nX_{n-1} ...X_0.X_{-1}X_{-2} ...X_{-m} = ± X_n.X_{n-1} ...X_0X_{-1}X_{-2} ...X_{-m} × 2^n ±XnXn−1...X0.X−1X−2...X−m=±Xn.Xn−1...X0X−1X−2...X−m×2n
其中,第一位为符号位。小数点. 之前的为整数位,小数点之后的为小数位。可以看出来,一个浮点数包含三个部分的数据要存储,一个符号位,一个数值部分,还有一个就是指数部分。下面我们以float为例,简单介绍下。
float规格化数据表示:
± 1. f × 2 E − 127 ± 1.f × 2^{E-127} ±1.f×2E−127
其中±为符号位,0表示正数,1表示负数。
E为指数,存储在指数位,8位二进制,其中[ 1,127 ) 算出来指数真值是负数, [ 127, 254 ] 是正数,0和255有特殊用途。
f为尾数,非0场景下,二进制表示,整数位固化为1,省略掉,所以可以用23位指数位表示24位尾数。
实际10.25存储成二进制,就是0 1000 0010 0100 1000 0000 0000 0000 000,1 + 8 + 23 总共32位。
以10.25为例,来说明下浮点数如何转化为二进制。
以0 1000 0010 0100 1000 0000 0000 0000 000为例
从前面可以了解到浮点数的转化规则。实际上,浮点数包含多种分类。
IEEE标准定义了6类浮点数:
指数 | 分类 | 隐含位 | 尾数 | 说明 |
---|---|---|---|---|
xx…xx | 有限数 | 1 | xx…xx | 指数非全0,非全1,有隐含位 |
00…00 | 0 | 0 | 0 | 尾数为0 |
00…00 | 弱规范数 | 无 | xx…xx | 尾数不是0 |
11…11 | ±∞ | 1 | 0 | 尾数不为0 |
11…11 | QNaN | 1 | 1xx…xx | 尾数高位为1,但尾数不为0 |
11…11 | SNaN | 1 | 0xx…xx | 尾数高位为0,但尾数不为0 |
IEEE标准要求:SNaN参与运算触发异常,QNaN则不触发异常。两者的区别在于尾数最高位不同。
弱规范数
表示0和最小有限数之间的数,指数和0一样,都是全0,但没有隐含位,尾数部分不为0
± ( f ) × 2 0 − o f f s e t ± (f) × 2^{0-offset} ±(f)×20−offset
特殊指数位,以float为例,指数位0主要是表示0和弱规范数,指数位为255则用来标识NaN和±∞。
根据有限数的规格化形式,指数取最小值1,隐含位是1,尾数取最小值0(23位都是0)
1.0 × 2 1 − 127 = 1.1754943508222875079687365372222 e − 38 1.0 × 2^{1-127} = 1.1754943508222875079687365372222e^{-38} 1.0×21−127=1.1754943508222875079687365372222e−38
根据有限数的规格化形式,指数取最大值254,隐含位是1,尾数取最大值(23位都是1):
1.11111111111111111111111 × 2 254 − 127 = ( 2 − 2 − 23 ) × 2 127 = 2 128 − 2 104 = 3.4028234663852885981170418348452 e 38 1.11111111111111111111111×2^{254-127} =(2-2^{-23})×2^{127} =2^{128}-2^{104} =3.4028234663852885981170418348452e^{38} 1.11111111111111111111111×2254−127=(2−2−23)×2127=2128−2104=3.4028234663852885981170418348452e38
指数是全0,但没有隐含位,尾数取最小值(最后一位为1),尾数包含整数位。
0.0000000000000000000001 × 2 0 − 127 = 2 − 22 × 2 − 127 = 1.4012984643248170709237295832899 e − 45 0.0000000000000000000001×2^{0-127} =2^{-22} × 2^{-127} =1.4012984643248170709237295832899e^{-45} 0.0000000000000000000001×20−127=2−22×2−127=1.4012984643248170709237295832899e−45
以float为例
public class FloatTest {
@Test
public void test() {
printFloatBitStr(10.25f);
printFloatBitStr(-10.25f);
// 最大的正float有限数
printFloatBitStr(Float.MAX_VALUE);
// 最小的正float有限数
printFloatBitStr(Float.MIN_NORMAL);
// 最小的正float弱规范数
printFloatBitStr(Float.MIN_VALUE);
// QNaN
printFloatBitStr(Float.NaN);
printFloatBitStr(10.25f);
// 表示不了,变成10.25f了
printFloatBitStr(10.250000059604645f);
// 表示不了,变成10.250001f了
printFloatBitStr(10.2500011f);
printFloat("0 10000010 01001000000000000000000");
printFloat("0 10000010 01001000000000000000001");
}
private void printFloatBitStr(Float f) {
String fs = Integer.toBinaryString(Float.floatToIntBits(f));
while (fs.length() < 32) {
fs = "0" + fs;
}
System.out.println(fs.substring(0, 1) + " " + fs.substring(1, 9) + " " + fs.substring(9) + " = " + f );
}
private void printFloat(String fs) {
System.out.println(fs + " = " + Float.intBitsToFloat(Integer.parseInt(fs.replaceAll(" ", ""), 2)) );
}
}
输出结果如下:
0 10000010 01001000000000000000000 = 10.25
1 10000010 01001000000000000000000 = -10.25
0 11111110 11111111111111111111111 = 3.4028235E38
0 00000001 00000000000000000000000 = 1.17549435E-38
0 00000000 00000000000000000000001 = 1.4E-45
0 11111111 10000000000000000000000 = NaN
0 10000010 01001000000000000000000 = 10.25
0 10000010 01001000000000000000000 = 10.25
0 10000010 01001000000000000000001 = 10.250001
0 10000010 01001000000000000000000 = 10.25
0 10000010 01001000000000000000001 = 10.250001
浮点数经常遇到精度丢失问题,以上面的10.25来说,二进制为 0 10000010 01001000000000000000000。
假设有另外一个二进制数,尾数位为 24位 010010000000000000000001, 其他位保持不变,则 这个数的值为:
+ 1.010010000000000000000001 × 2 130 − 127 = 1010.010000000000000000001 = 2 3 + 2 1 + 2 − 2 + 2 − 24 = 10.25 + 0.000000059604645 = 10.250000059604645 + 1.0100 1000 0000 0000 0000 0001 × 2^{130-127} = 1010.0 1000 0000 0000 0000 0001 = 2^3 + 2^1 + 2^{-2} + 2^{-24} = 10.25 + 0.000000059604645 = 10.250000059604645 +1.010010000000000000000001×2130−127=1010.010000000000000000001=23+21+2−2+2−24=10.25+0.000000059604645=10.250000059604645
但是float决定了 尾数位 为23 ,所以最后一个1 也就没有了。也就是0 10000010 01001000000000000000000到0 10000010 01001000000000000000000之间的数,只要不能用弱规范数表示的,都表示不了了。
从前面的java示例中,我们也能够看出来10.25到10.250001之间的很多数据都表示不了了,比方说10.250000059604645f,就向下取了,变成了10.25。同样的,计算过程中会存在位移,也会产生同样的后果,最终导致在一些精度丢失的问题。
使用java.math.BigDecimal#BigDecimal(java.lang.String)规避精度丢失问题
@Test
public void test2() {
// 此处以双精度 double为例,计算过程中,会存在位移,导致精度丢失。
System.out.println("0.05 + 0.01 = " + (0.05 + 0.01f) + " => " + add(10, 0.05, 0.01));
System.out.println("1.0 - 0.42 = " + (1.0f - 0.42f) + " => " + sub(10, 1.0, 0.42));
System.out.println("4.015 * 100 = " + (4.015 * 100) + " => " + mul(10, 4.015, 100d));
System.out.println("123.3 / 100 = " + (123.3 / 100) + " => " + div(10, 123.3, 100d));
}
public static Double add(int scale, Double ... args) {
BigDecimal sum = new BigDecimal("0");
for (double arg : args) {
sum = sum.add(new BigDecimal(String.valueOf(arg)));
}
return sum.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
public static Double sub(int scale, Double v1, Double v2) {
BigDecimal b1 = new BigDecimal(String.valueOf(v1));
BigDecimal b2 = new BigDecimal(String.valueOf(v2));
return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
public static Double mul(int scale, Double ... args) {
BigDecimal sum = new BigDecimal("1");
for (double arg : args) {
sum = sum.multiply(new BigDecimal(String.valueOf(arg)));
}
return sum.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
public static Double div(int scale, Double v1, Double v2) {
BigDecimal b1 = new BigDecimal(String.valueOf(v1));
BigDecimal b2 = new BigDecimal(String.valueOf(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
输出结果如下:
0.05 + 0.01 = 0.059999999776482585 => 0.06
1.0 - 0.42 = 0.58000004 => 0.58
4.015 * 100 = 401.49999999999994 => 401.5
123.3 / 100 = 1.2329999999999999 => 1.233
可以看到BigDecimal计算的结果是正确的,BigDecimal计算的原理就是将浮点数进行放大,放大成整数进行计算,几个主要变量如下:
//有多少位小数(即小数点后有多少位),也就是放大倍数
int scale;
//一共有多少位数字
int precision;
//字符串放大后,转为的long值,只有当传的字符串长度小于18时才使用该值。
long intCompact;
//当传的字符串长度大于等于18时才使用BigInteger表示数字,此时intCompact为Long.MAX_VALUE
BigInteger intVal;
微信扫一扫关注该公众号