BigInteger / BigDecimal / Math 数值运算源码精读
概述
Java 的数值精度分三层:long/double(原生)、Math(数学函数)、BigInteger/BigDecimal(任意精度,用于金融、密码学、大数运算)。BigInteger 用 int 数组存大整数并实现分治乘法/模幂;BigDecimal 在其上加小数位(scale)与舍入模式;Math 提供高性能数学函数。本文基于 OpenJDK 21 源码拆解。
一、BigInteger 的内部存储
java
// java.math.BigInteger
public class BigInteger extends Number implements Comparable<BigInteger> {
final int signum; // 符号:-1 负数 / 0 零 / 1 正数
final int[] mag; // 绝对值(大端无符号 int 数组)
private int bitCountPlusOne; // 缓存:1 的个数
private int bitLengthPlusOne; // 缓存:位长度
}存储模型:
mag:int[],大端序无符号绝对值(mag[0] 最高 32 位)
signum 单独存储符号
→ 任意精度(受内存限制)
数值 = signum × (mag 表示的无符号大整数)
示例:
2^64 = 0x1_00000000_00000000
→ mag = [0x1, 0x0, 0x0](3 个 int)
-42 → signum = -1, mag = [42]1.1 valueOf 的小整数缓存
java
private static final BigInteger[] posCache = new BigInteger[17]; // [0, 16]
private static final BigInteger[] negCache = new BigInteger[17]; // [-16, 0]
public static BigInteger valueOf(long val) {
// 缓存 [-16, 16] 的小整数(常见值避免反复分配)
if (val == 0) return ZERO;
if (val > 0 && val <= MAX_CONSTANT) {
return posCache[(int) val]; // 命中缓存
} else if (val < 0 && val >= -MAX_CONSTANT) {
return negCache[(int) -val];
}
// 大数 → 正常构造
return new BigInteger(val);
}缓存 [-16, 16]:
高频小值(0/1/2...)直接复用缓存实例(== 比较可靠)
类似 IntegerCache(-128~127),但范围小(值域大,缓存成本高)二、add 逐 int 进位
java
public BigInteger add(BigInteger val) {
if (val.signum == 0) return this; // 加 0 返回自己
if (signum == 0) return val;
if (val.signum == signum) // 同号 → 绝对值相加
return new BigInteger(addMag(val), signum);
// 异号 → 相减(绝对值大的减小的,符号取大的)
int cmp = compareMagnitude(val);
if (cmp == 0) return ZERO; // 相等 → 0
int[] resultMag = (cmp > 0)
? subtractMag(mag, val.mag) // |this| > |val|
: subtractMag(val.mag, mag);
resultMag = trustedStripLeadingZeroInts(resultMag);
return new BigInteger(resultMag, cmp * signum); // 符号修正
}
private static int[] addMag(int[] a, int[] b) {
int[] longer = (a.length >= b.length) ? a : b;
int[] shorter = (a.length >= b.length) ? b : a;
int[] result = new int[longer.length + 1]; // 预留进位位
int carry = 0;
// 逐 int 相加 + 进位(模拟多精度加法)
for (int i = shorter.length - 1, j = longer.length - 1; i >= 0; i--, j--) {
int sum = longer[j] + shorter[i] + carry;
result[j + 1] = sum; // 低位
carry = (sum >>> 31) & 1; // 溢出位 → 进位
}
...
return result;
}add 算法:
同号 → 绝对值逐 int 相加(带进位)
异号 → 绝对值相减(比较大小定符号)
符号逻辑:signum 与"大小比较"共同决定结果符号
复杂度 O(n)三、Karatsuba 分治乘法
java
private static final int MULTIPLY_SQUARE_THRESHOLD = 80; // 阈值:80 个 int
private static BigInteger multiplyKaratsuba(BigInteger x, BigInteger y) {
int len = Math.max(x.mag.length, y.mag.length);
if (len <= KARATSUBA_THRESHOLD) { // 小规模 → 朴素乘法
return multiplyToLen(x.mag, y.mag);
}
// ① 分治:把两个数切成高低两半
int half = (len + 1) / 2;
BigInteger xl = x.getLower(half); // 低半
BigInteger xh = x.getUpper(half); // 高半
BigInteger yl = y.getLower(half);
BigInteger yh = y.getUpper(half);
// ② 三个子乘法(比朴素 4 次少 1 次)
BigInteger p1 = xh.multiply(yh); // 高×高
BigInteger p2 = xl.multiply(yl); // 低×低
BigInteger p3 = xh.add(xl).multiply(yh.add(yl)); // 交叉
// ③ 组合:p1<<2half + (p3-p1-p2)<<half + p2
BigInteger middle = p3.subtract(p1).subtract(p2);
return p1.shiftLeft(half * 32)
.add(middle.shiftLeft(half * 32)) ...;
}Karatsuba 分治:
朴素乘法 O(n²) → Karatsuba O(n^1.585)
原理:3 次乘法替代 4 次(a+b)(c+d) - ac - bd = ad+bc
阈值 80 int 以下用朴素(分治固定开销大)
再大(TOOM_COOK_THRESHOLD)→ Toom-Cook 3 路 O(n^1.465)
→ 大数乘法自动选择最优算法四、Montgomery 模幂
java
public BigInteger modPow(BigInteger exponent, BigInteger m) {
...
// 按指数位循环:平方-乘(square-and-multiply)
BigInteger base = this;
BigInteger result = ONE;
while (expLen > 0) {
if (exponent.testBit(0)) {
result = result.multiply(base).mod(m); // 位为 1 → 乘
}
base = base.multiply(base).mod(m); // 平方
exponent = exponent.shiftRight(1);
}
...
// 大指数优化:Montgomery 约简
// MontgomeryMultiplication.montgomeryMultiply(...)
// 把 mod 运算转换为移位 + 加法(避开昂贵的除法)
}modPow 优化:
基础:平方-乘(逐位扫描指数,O(log e) 次乘法+取模)
加速:Montgomery 约简(把 mod n 的除法换成 n 的幂的移位)
Toom-Cook 3 路乘法进一步加速大数乘
用途:RSA 加解密、DH 密钥交换(大指数模幂性能关键)五、BigDecimal 的内部存储
java
// java.math.BigDecimal
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal; // 数值(整数形式)
private final int scale; // 小数点右移位数
private final int precision; // 有效数字位数(懒计算)
private transient long intCompact; // 小值优化:直接存 long(|v| < 2^53 时)
}存储模型:
值 = intVal × 10^(-scale)
scale = 小数点后位数(0.002 → scale=3)
precision = 有效数字位数(0.002 → precision=1,即"2")
intCompact:常见小值直接用 long 存(避免 BigInteger 分配)
示例:
BigDecimal.valueOf(2, 3) = 2 × 10⁻³ = 0.002(scale=3, precision=1)
123.45 → intVal=12345, scale=25.1 scale 与 precision 语义
scale(小数点后位数):
123.45 → scale=2
0.002 → scale=3
12300(整数值)→ scale=0(可 -2 表示科学计数)
precision(有效数字):
123.45 → precision=5
0.002 → precision=1(前导零不算)
0.00200 → precision=3(2、0、0,尾零算)六、divide 与舍入模式
java
public BigDecimal divide(BigDecimal divisor, RoundingMode roundingMode) {
// ① 对齐 scale:被除数/除数按统一 scale 运算
...
// ② 除法:商 = intVal/divisor 的整数部分 + 余数
// ③ 精确除尽 → 直接返回
// ④ 除不尽 → 按 roundingMode 截断/舍入到指定 scale
return divide(divisor, scale, roundingMode); // scale 由上下文决定
}
public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
// 计算精确商的整数部分 → 再按 scale + roundingMode 处理小数部分
// 核心:divideAndRound / divideAndRoundWithRemainder
}除法舍入流程:
精确除法:无限精度(如 1/3 = 0.333...)→ 无法精确表示
scale 决定保留几位小数
roundingMode 决定丢弃部分的处理方式
不指定模式 → ArithmeticException(非终止小数)6.1 RoundingMode 八种模式
| 模式 | 行为 | 例(1.55 保留 1 位) |
|---|---|---|
UP | 远离零 | 1.6 |
DOWN | 趋向零 | 1.5 |
CEILING | 向正无穷 | 1.6 |
FLOOR | 向负无穷 | 1.5 |
HALF_UP | 四舍五入 | 1.6 |
HALF_DOWN | 五舍六入 | 1.5 |
HALF_EVEN | 银行家舍入(遇 5 取偶) | 1.6(1.65→1.6, 1.55→1.6) |
UNNECESSARY | 必须精确,否则抛异常 | 抛 ArithmeticException |
选择建议:
金融计费 → HALF_UP(或业务规定)
统计/科学 → HALF_EVEN(避免系统性偏差)
数据入库 → 确定模式避免运行时异常七、Math 常用函数
7.1 取整函数
java
public static double floor(double a) {
// 向负无穷取整
return StrictMath.floor(a); // native
}
public static double ceil(double a) {
// 向正无穷取整(-floor(-a))
return StrictMath.ceil(a); // native
}
public static long round(double a) {
// round = floor(a + 0.5)
long param = (long) a; // 注意:超过 long 范围 → MAX/MIN
if (a - param >= 0.5) { ... }
}取整语义:
floor(-1.5) = -2(向负无穷)
ceil(-1.5) = -1(向正无穷)
round(-1.5) = -1(floor(-1.5+0.5) = floor(-1.0) = -1)
注意:round 对负数与直觉不同(-1.5 → -1 而非 -2)7.2 sqrt / cbrt 的 native
java
public static double sqrt(double a) {
// JVM intrinsic(热点直接替换为硬件指令,几乎零开销)
return StrictMath.sqrt(a); // native → C 库 sqrt(fdlibm)
}
public static double cbrt(double a) {
return StrictMath.cbrt(a); // native → C 库 cbrt
}Math.sqrt 优化:
声明为 @IntrinsicCandidate → HotSpot JIT 替换为 SSE sqrtss/sqrtsd
→ 性能等同 C 的 sqrt
fdlibm:JDK 数学库的 C 参考实现(IEEE 754 兼容)7.3 StrictMath vs Math
差异:
StrictMath:所有方法 native + 严格算法 → 跨平台一致结果
(同一输入任何 JDK/平台结果完全相同 → 可复现)
Math:部分方法走 intrinsic/JIT 优化 → 平台可微差异
(性能优先,微小舍入差异可接受)
选择:
需要精确可复现 → StrictMath(测试/科学计算)
性能优先 → Math(生产业务)
Java 9+:Math 大部分委托 StrictMath,差异进一步缩小八、实现要点
数值运算核心:
BigInteger:signum + int[] mag 符号绝对值存储
valueOf:[-16,16] 缓存
add:同号逐 int 进位 / 异号相减定符号
multiply:朴素 → Karatsuba(80)→ Toom-Cook 自动分级
modPow:平方-乘 + Montgomery 约简(RSA 关键)
BigDecimal:intVal + scale + precision + intCompact 优化
scale 小数点后位数 / precision 有效数字
divide:精确除法 + scale + roundingMode 舍入
RoundingMode 8 种:UP/DOWN/CEILING/FLOOR/HALF_UP/HALF_DOWN/HALF_EVEN/UNNECESSARY
Math:floor/ceil/round 取整语义(round = floor(a+0.5))
sqrt/cbrt:native + intrinsic(SSE 硬件指令)
StrictMath vs Math:可复现 vs 性能
常见陷阱:
用 double 存钱 → 精度丢失(金融必须 BigDecimal + HALF_UP)
divide 不指定舍入 → ArithmeticException
round(-1.5) = -1 与直觉不符
BigDecimal 比较用 compareTo(equals 含 scale 差异)
BigInteger 转 long 溢出 → 用 longValueExact 抛异常