BigDecimal 学习笔记之 Double 转 BigDecimal
Java 中 BigDecimal 类有这么一个方法:
public BigDecimal(double val);
它是将一个 double 类型的数据转换成 BigDecimal。double 内部使用 64bit 来表示一个数,这样空间效率极高,但牺牲了精度;而 BigDecimal 内部使用 BigInteger 来存储有效数,不存在精度丢失的情况但空间效率较差。double 和 BigDecimal 都有其使用的场景,没有绝对好坏。
下面是这个方法的内部实现源码:
public BigDecimal(double val, MathContext mc) {
if (Double.isInfinite(val) || Double.isNaN(val))
throw new NumberFormatException("Infinite or NaN");
// Translate the double into sign, exponent and significand, according
// to the formulae in JLS, Section 20.10.22.
long valBits = Double.doubleToLongBits(val);
int sign = ((valBits >> 63) == 0 ? 1 : -1);
int exponent = (int) ((valBits >> 52) & 0x7ffL);
long significand = (exponent == 0
? (valBits & ((1L << 52) - 1)) << 1
: (valBits & ((1L << 52) - 1)) | (1L << 52));
exponent -= 1075;
// At this point, val == sign * significand * 2**exponent.
/*
* Special case zero to supress nonterminating normalization and bogus
* scale calculation.
*/
if (significand == 0) {
this.intVal = BigInteger.ZERO;
this.scale = 0;
this.intCompact = 0;
this.precision = 1;
return;
}
// Normalize
while ((significand & 1) == 0) { // i.e., significand is even
significand >>= 1;
exponent++;
}
int scale = 0;
// Calculate intVal and scale
BigInteger intVal;
long compactVal = sign * significand;
if (exponent == 0) {
intVal = (compactVal == INFLATED) ? INFLATED_BIGINT : null;
} else {
if (exponent < 0) {
intVal = BigInteger.valueOf(5).pow(-exponent).multiply(compactVal);
scale = -exponent;
} else { // (exponent > 0)
intVal = BigInteger.valueOf(2).pow(exponent).multiply(compactVal);
}
compactVal = compactValFor(intVal);
}
int prec = 0;
int mcp = mc.precision;
if (mcp > 0) { // do rounding
int mode = mc.roundingMode.oldMode;
int drop;
if (compactVal == INFLATED) {
prec = bigDigitLength(intVal);
drop = prec - mcp;
while (drop > 0) {
scale = checkScaleNonZero((long) scale - drop);
intVal = divideAndRoundByTenPow(intVal, drop, mode);
compactVal = compactValFor(intVal);
if (compactVal != INFLATED) {
break;
}
prec = bigDigitLength(intVal);
drop = prec - mcp;
}
}
if (compactVal != INFLATED) {
prec = longDigitLength(compactVal);
drop = prec - mcp;
while (drop > 0) {
scale = checkScaleNonZero((long) scale - drop);
compactVal = divideAndRound(compactVal, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode);
prec = longDigitLength(compactVal);
drop = prec - mcp;
}
intVal = null;
}
}
this.intVal = intVal;
this.intCompact = compactVal;
this.scale = scale;
this.precision = prec;
}
代码不长,但理解起来需要一点背景知识。我们来逐行分析。
long valBits = Double.doubleToLongBits(val);
这一步是获取 double 的内部表示,一共 64bit,刚好为 long 的长度。valBits 采用 BigEnding(大端序)表示,包含 1 个符号位,11 个指数位,52 个有效数位。
int sign = ((valBits >> 63) == 0 ? 1 : -1);
int exponent = (int) ((valBits >> 52) & 0x7ffL);
long significand = (exponent == 0
? (valBits & ((1L << 52) - 1)) << 1
: (valBits & ((1L << 52) - 1)) | (1L << 52));
exponent -= 1075;
这几步是解析 64bit 的 double 数据,将其分成三部分:sign(符号),exponent(指数)和 significand(有效数)。上面四条语句执行完后,double 所代表的数值等于(sign * significand * 2exponent)。第一和第二行代码根据位置直接获取 sign 和 exponent,但是第三和第四条语句对 significand 和 exponent 做了一些处理,这里我们主要理解后两条语句。
double 内部使用科学技术法来表示数据,sign 为符号位,exponent 为指数。exponent bias 为指数偏移值(1023),这个是什么东西?double 的指数位 exponent 有 11 位,这 11 位表示了一个无符号整数。实际的指数值需要将其减去指数偏移值(1023)得到。
举个例子,假如这 11 位十六进制为 #400 ,即十进制的 1024。那么这个 double 的指数实际为 1024 - 1023 = 1。
那 1.fraction 又是怎么来的呢?其实 1.fraction 是由 double 的 52 位有效数得来的。对于一个只包含 0 和 1 的二进制数,我们总可以通过科学计数法将其化成 1.xxx 格式的有效数(当然 0 除外),因此为了增加表示 double 的表示范围,我们可以省略最高位 1 的存储,只存储小数部分,也就是 52 位的有效数。
而指数部分为最小值 #000 时,指数部分变成了 1 - exponent bias,有效数部分 1.fraction 变成了 0.fraction。虽然直观上觉得这种特殊情况处理不太优美,但想想也在情理之中,要不然数字 0 怎么用 double 表示呢?
背景知识已说完,我们来看下上面的第三条语句:
long significand = (exponent == 0
? (valBits & ((1L << 52) - 1)) << 1
: (valBits & ((1L << 52) - 1)) | (1L << 52));
这条语句表达的意思就是:当指数部分为 0 时,以 0.fraction 方式取出有效数(同时左移一位,即乘以 2);当指数部分不为 0 时,以 1.fraction 方式取出有效数。此外,我们希望 significand 保存的是一个整数,我们只需在科学计数法中将指数部分再减去 52,significand 存储的数据就可以看做是整数了。
这也就是第四行代码表达的意思:
exponent -= 1075;
1075 = 1023 + 52,也就是指数部分减去指数偏移值和用于化整的 52。至此,代码最难理解(个人认为)的一部分已经解构完毕。这几行代码使用了 sign、exponent 和 significand 来表示 double 所代表的数据,即:double 的数据 = sign * significand * 2exponent
下面我们接着分析代码。
if (significand == 0) {
this.intVal = BigInteger.ZERO;
this.scale = 0;
this.intCompact = 0;
this.precision = 1;
return;
}
当有效数为 0 的时做特殊处理。
这里说下 BigDecimal 的内部表示。它主要由 intVal、scale、intCompact、precision 表示:
- intVal 类型为 BigInteger,用大整数来表示有效数。
- precision 表示精度,也就是有效数有多少位。
- scale 表示范围,也就是小数部分占多少位。
- intCompact 类型为 long,当有效数的绝对值不超过 Long.MAX_VALUE 时,使用 intCompact 来存储有效数提高计算效率。
while ((significand & 1) == 0) { // i.e., significand is even
significand >>= 1;
exponent++;
}
这是通过将有效数右移方式(也就是除以 2)化成奇数形式。
int scale = 0;
// Calculate intVal and scale
BigInteger intVal;
long compactVal = sign * significand;
if (exponent == 0) {
intVal = (compactVal == INFLATED) ? INFLATED_BIGINT : null;
} else {
if (exponent < 0) {
intVal = BigInteger.valueOf(5).pow(-exponent).multiply(compactVal);
scale = -exponent;
} else { // (exponent > 0)
intVal = BigInteger.valueOf(2).pow(exponent).multiply(compactVal);
}
compactVal = compactValFor(intVal);
}
这里是将上面的(sign * significand * 2exponent)转换成 intVal、compactVal 和 scale。
可以看到,判断条件对于指数的值分了三种情况,等于 0,小于 0 和大于 0。
- exponent 等于 0 时,判断了 compactVal(即 sign * significand)的值是否等于 INFLATED(即 Long.MIN_VALUE)。但其实这个判断永远为 false,因为根据之前的计算此处 significand 不会超过 53 位数,因此 sign * significand 无论如何也不可能等于 Long.MIN_VALUE。
- exponent 小于 0 时,sign * significand * 2exponent表示一个小数,代码采取了有效数化整、增加小数位的方法,将有效数设为 5-exponent * sign * significand,小数位增加(-exponent)位。这是为什么呢?
- exponent 大于 0 时,sign * significand * 2exponent表示整数,直接计算即可。
接着分析剩下代码。
int prec = 0;
int mcp = mc.precision;
if (mcp > 0) { // do rounding
int mode = mc.roundingMode.oldMode;
int drop;
if (compactVal == INFLATED) {
prec = bigDigitLength(intVal);
drop = prec - mcp;
while (drop > 0) {
scale = checkScaleNonZero((long) scale - drop);
intVal = divideAndRoundByTenPow(intVal, drop, mode);
compactVal = compactValFor(intVal);
if (compactVal != INFLATED) {
break;
}
prec = bigDigitLength(intVal);
drop = prec - mcp;
}
}
if (compactVal != INFLATED) {
prec = longDigitLength(compactVal);
drop = prec - mcp;
while (drop > 0) {
scale = checkScaleNonZero((long) scale - drop);
compactVal = divideAndRound(compactVal, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode);
prec = longDigitLength(compactVal);
drop = prec - mcp;
}
intVal = null;
}
}
这里主要根据 MathContext 设置的精度限制来取精抛弃多余位数。当 mc.precision 设置特定值(大于 0)时,需要取精。在取精计算中,主要分为两部分计算,即 if (compactVal == INFLATED)和 if (compactVal != INFLATED)两个分支。
第一个分支 if (compactVal == INFLATED)表达的意思为,如果 BigDecimal 的有效数很大,而且精度也超出设置的特定值,那么对 intVal 取精,直到符合精度要求或者用一个 long 足以表示。
第二个分支 if (compactVal != INFLATED)表达的意思为,如果 BigDecimal 的有效数足够用 compactVal 表示,那么对 compactVal 进行取精,将原有效数字段 intVal 字段设为 null 回收空间。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 解数独算法
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论