首页 / Java 入门教程 / 大数运算:BigInteger 与 BigDecimal

Java 入门教程

大数运算:BigInteger 与 BigDecimal

本教程共 100 篇 · 第 60 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程BigIntegerBigDecimal精确计算

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.ZEROONETWOTEN 现成可用,别自己 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.00ZERO 的 scale 不同。

这个特性还有个连带影响:把 BigDecimal 放进 HashSet 或当 HashMap 的键时,1.01.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`。