BigDecimal 学习笔记之 Double 转 BigDecimal

发布于 2023-09-06 12:15:31 字数 8813 浏览 30 评论 0

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 &amp; 1) == 0) { // i.e., significand is even
significand &gt;&gt;= 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 &lt; 0) {
intVal = BigInteger.valueOf(5).pow(-exponent).multiply(compactVal);
scale = -exponent;
} else { // (exponent &gt; 0)
intVal = BigInteger.valueOf(2).pow(exponent).multiply(compactVal);
}
compactVal = compactValFor(intVal);
}
int prec = 0;
int mcp = mc.precision;
if (mcp &gt; 0) { // do rounding
int mode = mc.roundingMode.oldMode;
int drop;
if (compactVal == INFLATED) {
prec = bigDigitLength(intVal);
drop = prec - mcp;
while (drop &gt; 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 &gt; 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

私野

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

qq_E2Iff7

文章 0 评论 0

Archangel

文章 0 评论 0

freedog

文章 0 评论 0

Hunk

文章 0 评论 0

18819270189

文章 0 评论 0

wenkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文