大数运算:BigInteger 与 BigDecimal
本教程共 100 篇 · 第 60 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
60. 大数运算
本节目标:知道什么时候该用 BigInteger 和 BigDecimal,怎么构造才不丢精度,以及为什么判等不能用 equals。
long 最大约 9.2×10¹⁸,再大就悄悄溢出;double 算钱会算出 0.1 + 0.2 不等于 0.3 的尴尬。遇到「装不下」或「不能丢精度」的场景,Java 备了两个任意精度的类。
先看清浮点为什么会错
这不是 Java 的 bug,而是二进制浮点的固有限制:
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false
System.out.println(1.03 - 0.42); // 0.6100000000000001
System.out.println(4.35 * 100); // 434.99999999999994
```bash
原因是十进制的 0.1 换成二进制是个无限循环小数,就像十进制写不出精确的三分之一。double 只有 52 位尾数,只能存一个最接近的近似值。误差一路累积,就冒出了那串尾巴。
最后一行尤其致命:把 4.35 元换算成 435 分,取整后得到 434 分,一分钱就这么丢了。所有跟钱有关的计算,都不能用 double。
> [!WARNING]
> 别试图用「四舍五入到两位」来掩盖问题。误差会在累加、比较、取整时不断放大,最终对账对不上。正确做法是从一开始就不引入误差。
## BigInteger:想多大就多大
`BigInteger` 表示任意大小的整数,内部用数组存,只受内存限制。它继承自 `Number`,能转回各种基本类型。
```java
import java.math.BigInteger;
BigInteger a = new BigInteger("123456789012345678901234567890");
BigInteger b = BigInteger.valueOf(987654321); // 从 long 构造,推荐
BigInteger sum = a.add(b);
BigInteger prod = a.multiply(b);
System.out.println(sum);
第 4 行用 valueOf 从 long 构造,比字符串解析快,小数值还走缓存。数值超出 long 范围时才用字符串构造。
它不支持运算符,加减乘除全靠方法名。Java 不允许给自定义类重载运算符,所以 a + b 编译不过,只能写 a.add(b)。
常量 BigInteger.ZERO、ONE、TWO、TEN 现成可用,别自己 new。
Warning转回基本类型要防溢出。
longValue()超范围时会静默截断,给你一个完全错误的数字。用longValueExact()和intValueExact(),超范围直接抛异常,问题当场暴露。
BigInteger 的常用方法
除了加减乘除,它还内置了一堆数论运算:
a.subtract(b); // 减
a.divide(b); // 整除,直接丢掉小数部分
a.remainder(b); // 余数,符号跟被除数
a.mod(b); // 取模,结果永远非负
a.pow(10); // 乘方
a.gcd(b); // 最大公约数
a.modPow(b, m); // 模幂,RSA 加密的核心运算
a.shiftLeft(3); // 左移三位,相当于乘 8
a.abs(); a.negate(); // 绝对值、取负
a.isProbablePrime(20); // 概率法判素数,参数越大越可靠
a.compareTo(b); // 比大小
```bash
注意 `remainder` 和 `mod` 的区别:被除数为负时,前者给负余数,后者永远给非负结果。密码学场景基本都用 `mod`。
所有运算都返回新对象,原值不变——和 String 一样,BigInteger 也是不可变类。循环里累加会产生大量中间对象,数据量大时要留意。
## BigDecimal:算钱的正确姿势
### 构造方式决定成败
这是本章最重要的一条。同样表示 0.1,两种写法天差地别:
```java
import java.math.BigDecimal;
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1"));
// 0.1
System.out.println(BigDecimal.valueOf(0.1));
// 0.1
第 3 行为什么是那么一长串?因为 0.1 这个字面量本身就是个 double,它进入构造方法前已经是不精确的近似值了。BigDecimal 只是忠实地把这份误差原样记录下来,一位不落。
new BigDecimal(double) 是 BigDecimal 的头号陷阱。它没有错,但几乎从来不是你想要的。
Warning金额一律用
new BigDecimal("0.1")字符串构造。 手上只有一个 double 变量时用BigDecimal.valueOf(x),它内部走Double.toString(x),按你屏幕上看到的样子转换。绝不要写new BigDecimal(x)。
基本运算
BigDecimal x = new BigDecimal("0.1");
BigDecimal y = new BigDecimal("0.2");
System.out.println(x.add(y)); // 0.3,干干净净
System.out.println(x.multiply(y)); // 0.02
System.out.println(x.subtract(y)); // -0.1
```bash
同样不支持运算符,同样不可变,每次运算返回新对象。写成 `x.add(y);` 而不接返回值,是新手常犯的错——x 一点没变。
现成常量:`BigDecimal.ZERO`、`ONE`、`TEN`。
## scale:小数位数是它的身份证
`BigDecimal` 内部由一个无标度整数值和一个 scale(小数位数)组成。`1.230` 存的是整数 1230 加 scale 为 3。
```java
System.out.println(new BigDecimal("1.230").scale()); // 3
System.out.println(new BigDecimal("1.230").stripTrailingZeros()); // 1.23
stripTrailingZeros() 去掉末尾无意义的零。有个小坑:对 100 这样的整数,它会变成科学计数法 1E+2,打印时记得配 toPlainString()。
scale 会影响 equals 的判断,下面马上讲到。
除法必须指定舍入模式
divide 是最容易踩雷的方法。除不尽时它不知道要保留几位,会直接抛异常:
new BigDecimal("1").divide(new BigDecimal("3")); // 抛 ArithmeticException
```java
正确写法是同时给出保留位数和舍入模式:
```java
import java.math.RoundingMode;
BigDecimal r = new BigDecimal("1")
.divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);
System.out.println(r); // 0.33
常用舍入模式对照:
| 模式 | 含义 | 1.235 保留两位 |
|---|---|---|
HALF_UP | 四舍五入(日常理解的那种) | 1.24 |
HALF_DOWN | 五舍六入 | 1.23 |
HALF_EVEN | 银行家舍入,五时向偶数靠 | 1.24 |
UP | 远离零方向进位 | 1.24 |
DOWN | 朝零方向截断 | 1.23 |
CEILING | 朝正无穷 | 1.24 |
FLOOR | 朝负无穷 | 1.23 |
HALF_EVEN 是金融系统的默认选择。大量数据反复四舍五入会整体偏大,向偶数靠能让误差正负相抵,长期更公平。
给单个数改小数位数用 setScale:
System.out.println(new BigDecimal("1.235").setScale(2, RoundingMode.HALF_UP)); // 1.24
```bash
> [!TIP]
> 金额建议在存储和展示前统一 `setScale(2, RoundingMode.HALF_UP)`,中间计算过程保留更多位,最后一步才定型。中途反复舍入会累积误差。
## 判等要用 compareTo
`BigDecimal.equals` 同时比较数值和 scale,两者都相同才算相等:
```java
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1.00"))); // false
System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1.00"))); // 0
1.0 和 1.00 在数学上一样,在 BigDecimal 眼里却是两个不同的对象——精度信息也是它身份的一部分。
判断数值相等一律用 compareTo(...) == 0。 返回负数表示小于、0 表示相等、正数表示大于。
判断是否为零同理:写 x.compareTo(BigDecimal.ZERO) == 0,别写 x.equals(BigDecimal.ZERO),因为 0.00 和 ZERO 的 scale 不同。
这个特性还有个连带影响:把 BigDecimal 放进 HashSet 或当 HashMap 的键时,1.0 和 1.00 会被当成两个不同的键。要按数值去重,先统一 scale。
其他实用方法
BigDecimal[] r = new BigDecimal("10").divideAndRemainder(new BigDecimal("3"));
// r[0] 是商 3,r[1] 是余数 1
System.out.println(new BigDecimal("1234").movePointLeft(2)); // 12.34,分转元
System.out.println(new BigDecimal("12.34").movePointRight(2)); // 1234,元转分
```bash
`movePointLeft` 和 `movePointRight` 做单位换算特别顺手,比乘除一百再舍入清爽。
`max`、`min`、`pow`、`negate`、`abs`、`signum` 也都有,行为和名字一致,全部返回新对象。
## 常见疑问
**问:BigDecimal 比 double 慢多少?**
大概慢几十倍到上百倍。但金额计算的量级通常在每秒几千笔,这点开销完全无感。为了性能牺牲金额准确性,是彻头彻尾的因小失大。
**问:能不能干脆用 long 存「分」来算钱?**
可以,很多支付系统就这么做,性能好且不会有精度问题。代价是所有换算要自己处理,遇到千分之一的费率、利息分摊时容易出错。金额精度要求复杂就用 BigDecimal,简单收付款用 long 存最小单位也够。
**问:BigInteger 的运算会溢出吗?**
不会,它自动扩容。真正的限制是内存和速度——算几万位的乘法会明显变慢。
## 小结
数值超出 `long` 用 `BigInteger`,要精确小数尤其是金额用 `BigDecimal`。两者都不可变、都不支持运算符、都靠方法名做运算。BigDecimal 必须用字符串构造或 `valueOf`,`new BigDecimal(0.1)` 会把浮点误差原样带进来。`divide` 必须给保留位数和舍入模式,判等用 `compareTo` 而不是 `equals`。金额计算请远离 `double`。