Apache Commons Numbers 1.3 数值计算示例与最佳实践详解

Apache Commons Numbers 1.3 数值计算实战与最佳实践,覆盖浮点比较、扩展精度、分数记账、素数应用、概率计算与大数溢出管理等场景并附实测回显。

Apache Commons NumbersApache Commons Numbers2026/10/15

Apache Commons Numbers 1.3 数值计算示例与最佳实践详解

目标:以工程视角组织 Apache Commons Numbers 1.3 的典型数值计算场景——浮点比较、扩展精度、分数精确记账、素数应用、概率计算、溢出管理、求根与 top-k,每个场景给出可运行示例、实测回显与选型建议,全部已在 Ubuntu 22.04 实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • 架构:amd64
  • JDK:OpenJDK 17.0.20.1
  • 构建工具:Apache Maven 3.6.3
  • 组件版本:Apache Commons Numbers 1.3(要求 Java 8 及以上)
  • Maven 坐标:org.apache.commons:commons-numbers-bom:1.3(BOM)统一管理 11 个运行时模块:commons-numbers-core、commons-numbers-angle、commons-numbers-arrays、commons-numbers-combinatorics、commons-numbers-complex、commons-numbers-field、commons-numbers-fraction、commons-numbers-gamma、commons-numbers-primes、commons-numbers-quaternion、commons-numbers-rootfinder
  • 工作目录:/root/numbers-demo(本章实战代码另存于 /root/practice)

二、浮点比较与舍入:Precision 的三套语义

2.1 问题背景

0.1 + 0.2 != 0.3 是二进制浮点的固有属性,直接 == 或 == 链式比较会让测试与业务判断随机失败。Precision 把比较变成显式策略选择。

2.2 实测示例

import org.apache.commons.numbers.core.Precision;

System.out.println("equals(1.1*3, 3.3) = " + Precision.equals(1.1 * 3, 3.3));
System.out.println("equals(NaN, NaN) = " + Precision.equals(Double.NaN, Double.NaN));
System.out.println("equalsIncludingNaN(NaN, NaN) = " + Precision.equalsIncludingNaN(Double.NaN, Double.NaN));
System.out.println("equalsIncludingNaN(0.0, -0.0) = " + Precision.equalsIncludingNaN(0.0, -0.0));
System.out.println("equalsWithRelativeTolerance(100.0, 100.00001, 1e-6) = "
        + Precision.equalsWithRelativeTolerance(100.0, 100.00001, 1e-6));
Precision.DoubleEquivalence eq = Precision.doubleEquivalenceOfEpsilon(1e-6);
System.out.println("eq.eq(1.0000005, 1.0) = " + eq.eq(1.0000005, 1.0));
System.out.println("eq.eq(1.000002, 1.0)  = " + eq.eq(1.000002, 1.0));
System.out.println("round(2.675, 2) = " + Precision.round(2.675, 2));
System.out.println("round(2.675, 2, HALF_UP) = "
        + Precision.round(2.675, 2, java.math.RoundingMode.HALF_UP));

实测回显:

equals(1.1*3, 3.3)                    = true
equals(NaN, NaN)                      = false
equalsIncludingNaN(NaN, NaN)          = true
equalsIncludingNaN(0.0, -0.0)         = true
equalsWithRelativeTolerance(100.0, 100.00001, 1e-6) = true
eq1e6.eq(1.0000005, 1.0) = true
eq1e6.eq(1.000002, 1.0)  = false
eq1e6.eq(NaN, NaN)       = true
round(2.675, 2)          = 2.68
round(2.675, 2, HALF_UP) = 2.68

验证结果:1.1*3 与 3.3 恰差 1 ULP,默认 1 ULP 比较判定相等;NaN 默认不相等(IEEE 语义),业务上需要把 NaN 视为同值时用 equalsIncludingNaN;1e-6 的绝对容差等价器能正确区分 5e-7 与 2e-6 的偏差。

💡 技术拓展:浮点比较策略选型对比:

策略方法适用风险
ULP 比较equals(a, b, maxUlps)通用、跨数量级0 附近需特判正负零
绝对误差equals(a, b, epsilon)值域固定的量(概率、归一化分数)大数值下过松
相对误差equalsWithRelativeTolerance物理量、金额汇总接近 0 时失效

本文默认 ULP(1~2 ulp),值域明确时用绝对误差,两者都不合适时先做归一化再比较。

2.3 要点提示

  • 舍入默认 HALF_EVEN(银行家舍入),对账场景显式传 RoundingMode.HALF_UP;
  • representableDelta(1e300, 1e-300) 实测返回 0.0,说明增量在当前量级下不可表示,比较策略再宽松也无济于事,应先改变数值量纲;
  • 常量 Precision.EPSILON = 1.1102230246251565E-16 即 double 的机器精度,可作为绝对容差下限参考。

三、扩展精度求和与点积:Sum 与 DD

3.1 问题背景

浮点求和误差随项数与量级交替累积,朴素累加在对账、统计中会给出带系统偏差的结果。core 模块的 Sum(补偿求和)与 DD(double-double,约 32 位有效数字)是两个量级的解法。

3.2 实测示例:1 亿笔 0.01 对账

import org.apache.commons.numbers.core.Sum;

double naive = 0.0;
for (int i = 0; i < 100000000; i++) {
    naive += 0.01;
}
System.out.println("naive 1e8 次 0.01 = " + naive);

Sum s = Sum.create();
for (int i = 0; i < 100000000; i++) {
    s.add(0.01);
}
System.out.println("Sum 1e8 次 0.01 = " + s.getAsDouble());

实测回显:

naive 1e8 次 0.01 = 1000000.0007792843
Sum 1e8 次 0.01 = 1000000.0

验证结果:朴素累加误差约 7.8e-4(相当于账面多出近一分钱),Sum 补偿求和结果与精确值 1e6 完全一致,且开销仅为每项一次补偿加法。

3.3 实测示例:大小项交替的点积

import org.apache.commons.numbers.core.Sum;
import org.apache.commons.numbers.core.DD;

double[] a = {1e100, 1.0, -1e100};
double[] b = {1.0, 2.0, 1.0};
double dot = 0.0;
for (int i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
}
System.out.println("naive dot(a, b)     = " + dot);
System.out.println("Sum.ofProducts(a,b) = " + Sum.ofProducts(a, b).getAsDouble());

System.out.println("double: 1e16 + 1 = " + (1e16 + 1));
DD dd = DD.of(1e16).add(DD.of(1));
System.out.println("DD: 1e16 + 1     = " + dd.bigDecimalValue());
System.out.println("DD hi/lo         = " + dd.hi() + " / " + dd.lo());

实测回显:

naive dot(a, b)     = 0.0
Sum.ofProducts(a,b) = 2.0
== core: DD 双双精度 ==
double: 1e16 + 1        = 1.0E16
DD: 1e16 + 1            = 10000000000000001
DD hi/lo                = 1.0E16 / 1.0
DD sqrt(2).doubleValue  = 1.4142135623730951

验证结果:加粗大项先入账时小项 2.0 被完全吞掉,Sum.ofProducts 以乘积扩展精度保住答案;1e16 量级加 1,double 丢失、DD 保留(hi=1e16,lo=1.0)。

3.4 要点提示

  • 顺序敏感是双精度求和的本质缺陷,方差、损失、积分一律走 Sum;
  • DD 适合求和不奏效的病态场景(条件数极大、长程递推),代价是每次运算 2 倍以上开销;
  • Sum 线程不安全,并行流里先分段求和再 Sum.of 汇总。

四、用分数消灭浮点误差:Fraction 与 BigFraction

4.1 问题背景

比例、配比、时间码等本质是有理数的量,用 double 存储即引入不可消除的表示误差;Fraction/BigFraction 以两个整数存分子分母,运算全程精确。

4.2 实测示例:比例累加

import org.apache.commons.numbers.fraction.Fraction;

Fraction sum = Fraction.of(1, 7).add(Fraction.of(2, 7)).add(Fraction.of(4, 7));
System.out.println("Fraction: 1/7 + 2/7 + 4/7 = " + sum);
double d = 1.0 / 7 + 2.0 / 7 + 4.0 / 7;
System.out.println("double:   1/7 + 2/7 + 4/7 = " + d);

实测回显:

Fraction: 1/7 + 2/7 + 4/7 = 1
double:   1/7 + 2/7 + 4/7 = 1.0

验证结果:三分之七的组合在本例中 double 恰好也得到 1.0——说明项数少、分母小时 double 往往够用;但把同样的运算换成 1/3、1/6 累加几十项,或做连续乘法时 double 会立刻出现 0.9999999999999999 型偏差,Fraction 恒为精确值。是否需要分数,应看运算链长度与判定条件(如 sum == 1 的守卫)。

4.3 实测示例:浮点数的最佳分数逼近

import org.apache.commons.numbers.fraction.Fraction;
import org.apache.commons.numbers.fraction.BigFraction;

System.out.println("Fraction.from(3.14159, 100) = " + Fraction.from(3.14159, 100));
System.out.println("BigFraction.from(pi, 100) = " + BigFraction.from(Math.PI, 100));
System.out.println("BigFraction.from(pi, 100).doubleValue() = "
        + BigFraction.from(Math.PI, 100).doubleValue());
System.out.println("BigFraction.of(123456789012345, 987654321098765) = "
        + BigFraction.of(123456789012345L, 987654321098765L));

实测回显:

Fraction.from(3.14159, 100) = 22 / 7
BigFraction.from(pi, 100) = 22 / 7
BigFraction.from(pi, 100).doubleValue()  = 3.142857142857143
BigFraction.of(123456789012345, 987654321098765) = 24691357802469 / 197530864219753

验证结果:from(value, maxDenominator) 基于连分数展开给出分母受限的最优逼近,3.14159 在分母 100 内的最佳逼近就是经典的 22/7;长整型入参自动约分(最大公约数 5 被约掉)。

💡 技术拓展:Fraction 与 BigFraction 选型对比:

维度FractionBigFraction
存储两个 int两个 BigInteger
溢出分子分母超 21 亿溢出无上限
单次运算开销纳秒级微秒级(大数分配)
适用小分母比例、时间码连乘链、任意精度需求

本文两例分母都在 100 内,用 Fraction 足够;涉及连乘或外部输入分母不可控时用 BigFraction。

4.4 要点提示

  • 1.3 的 Fraction.of(-3, -6) 得 -1 / -2(只约分不翻符号),需要规范符号时自行处理;
  • Fraction.of(1, 0) 抛 FractionException: Denominator must be different from 0,该异常类包级私有,按 RuntimeException 捕获;
  • 分数不适合做迭代逼近类计算(每步都在制造更大的分母),迭代场景回到浮点并配 Sum。

五、素数与因子分解:Primes 的工程用法

5.1 问题背景

哈希表容量取素数能减少键分布规律的碰撞;布隆过滤器、随机数种子、密码学练习都依赖素数判定与生成。

5.2 实测示例:哈希容量与因子分解

import org.apache.commons.numbers.primes.Primes;

System.out.println("nextPrime(1024)  = " + Primes.nextPrime(1024));
System.out.println("nextPrime(65536) = " + Primes.nextPrime(65536));
System.out.println("isPrime(2147483647) = " + Primes.isPrime(2147483647));
System.out.println("primeFactors(360)   = " + Primes.primeFactors(360));
System.out.println("primeFactors(5460)  = " + Primes.primeFactors(5460));

实测回显:

isPrime(2)          = true
isPrime(97)         = true
isPrime(1)          = false
isPrime(100)        = false
isPrime(2147483647) = true
nextPrime(97)       = 97
nextPrime(100)      = 101
nextPrime(1)        = 2
primeFactors(360)   = [2, 2, 2, 3, 3, 5]
primeFactors(97)    = [97]
primeFactors(1024)  = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
primeFactors(5460)  = [2, 2, 3, 5, 7, 13]
nextPrime(1024)  = 1031
nextPrime(65536) = 65537

验证结果:1024 的下一个素数是 1031、65536 的下一个素数是 65537(费马素数),扩容时取 nextPrime(capacity) 即可;2^31 - 1 是素数(Miller-Rabin 实测瞬时判定);因子分解返回带重复因子的升序列表。

💡 技术拓展:素数判定算法对比:

算法复杂度适用
试除法O(√n)n 小于 1e6、教学场景
Miller-Rabin(确定性底数)O(k log n)32 位整数严格判定,本文所用
埃拉托斯特尼筛O(n log log n) 预处理一次性判定大量小素数

Primes.isPrime 是试除加 Miller-Rabin 的混合实现,单点判定免建筛表;需要反复查询区间内素数时自建 BitSet 筛更划算。

5.3 要点提示

  • nextPrime 对已是素数的入参原样返回,无需先判后取;
  • 入参 int 上限 2147483647,更大范围需要自实现或换库;
  • primeFactors 结果可直接累乘还原原数,适合做校验与教学演示。

六、复数与四元数:信号处理与空间插值

6.1 实测示例:复数代数

import org.apache.commons.numbers.complex.Complex;

Complex a = Complex.ofCartesian(3, 4);
Complex b = Complex.ofCartesian(1, -2);
System.out.println("a.multiply(b) = " + a.multiply(b));
System.out.println("a.divide(b)   = " + a.divide(b));
System.out.println("a.abs()       = " + a.abs());
System.out.println("a.sqrt()      = " + a.sqrt());
System.out.println("I.multiply(I) = " + Complex.I.multiply(Complex.I));

实测回显:

a.multiply(b) = (11.0,-2.0)
a.divide(b)   = (-1.0,2.0)
a.abs()       = 5.0
a.sqrt()      = (2.0,1.0)
I.multiply(I) = (-1.0,0.0)

验证结果:(3+4i)(1-2i) = 11-2i、(3+4i)/(1-2i) = -1+2i、(3+4i)^0.5 = 2+i,全部可手算复核;复数乘加是 FFT、数字滤波、信号卷积的原子操作,Numbers 的 Complex 不可变且无锁,可直接入并行流。

6.2 实测示例:四元数与球面插值

import org.apache.commons.numbers.quaternion.Quaternion;
import org.apache.commons.numbers.quaternion.Slerp;

Quaternion q = Quaternion.of(0.7071067811865476, 0.7071067811865476, 0.0, 0.0);
Quaternion r = Quaternion.of(0.0, 0.0, 0.0, 1.0);
Slerp slerp = new Slerp(q, r);
System.out.println("slerp(0.0) = " + slerp.apply(0.0));
System.out.println("slerp(0.5) = " + slerp.apply(0.5));
System.out.println("slerp(1.0) = " + slerp.apply(1.0));

实测回显:

slerp(0.5)    = [0.5000000000000001 0.5000000000000001 0.0 0.7071067811865476]
slerp(0.0)    = [0.7071067811865476 0.7071067811865476 0.0 0.0]
slerp(1.0)    = [0.0 0.0 0.0 1.0]

验证结果:t=0 与 t=1 精确还原两端姿态,t=0.5 位于球面大圆中点;与欧拉角插值相比,四元数 slerp 无万向节锁、角速度均匀,是相机与骨骼动画的标准解。

6.3 要点提示

  • 方法命名与 math3 不同:conj()/arg()/abs();
  • Quaternion.of(w, double 数组) 标量在前向量在后,getScalarPart/getVectorPart 对应取出;
  • quaternion 的 isUnit 运行期加载 core 的 Precision,离线 jar 直跑时 core 必须在 classpath。

七、特殊函数与概率计算:gamma 模块

7.1 实测示例:大数阶乘的对数概率

二项分布 P(X=k) = C(n,k) × p^k × (1-p)^(n-k),n=1000 时组合数超 long 上限,必须在对数空间计算。以下实测 C(1000, 500) 恰好出现 500 次正面的概率:

import org.apache.commons.numbers.combinatorics.LogBinomialCoefficient;

double logP = LogBinomialCoefficient.value(1000, 500) - 1000 * Math.log(2);
System.out.println("logBinomial(1000, 500) = " + LogBinomialCoefficient.value(1000, 500));
System.out.println("ln P(X=500) = " + logP);
System.out.println("P(X=500)    = " + Math.exp(logP));

实测回显:

logBinomial(1000, 500) = 689.4672615678512
ln P(X=500) = -3.6799189920940307
P(X=500)    = 0.025225018178363288

验证结果:ln C(1000,500) ≈ 689.47,减去 1000×ln2 ≈ 693.15 得对数概率 -3.68,换回概率约 2.52%,与正态近似(σ ≈ 15.81,P ≈ 0.0252)一致;全程无溢出。

7.2 实测示例:正态分布相关函数

import org.apache.commons.numbers.gamma.Erf;
import org.apache.commons.numbers.gamma.Erfc;
import org.apache.commons.numbers.gamma.InverseErfc;
import org.apache.commons.numbers.gamma.Gamma;
import org.apache.commons.numbers.gamma.LogGamma;

System.out.println("Erf.value(1.0)  = " + Erf.value(1.0));
System.out.println("Erfc.value(1.0) = " + Erfc.value(1.0));
System.out.println("InverseErfc.value(Erfc.value(1.0)) = " + InverseErfc.value(Erfc.value(1.0)));
System.out.println("Gamma.value(5.0)    = " + Gamma.value(5.0));
System.out.println("LogGamma.value(100.0) = " + LogGamma.value(100.0));
System.out.println("Math.log(Gamma.value(100)) = " + Math.log(Gamma.value(100.0)));

实测回显:

Erf.value(1.0)          = 0.8427007929497149
Erfc.value(1.0)         = 0.15729920705028513
InverseErfc.value(Erfc.value(1.0)) = 1.0
Gamma.value(5.0)        = 24.0
Gamma.value(100.0)      = 9.332621544394415E155
LogGamma.value(100.0)   = 359.1342053695754
Math.log(Gamma.value(100)) 核对 = 359.1342053695754

验证结果:Erf 与 Erfc 互补(和为 1),InverseErfc 逆运算精确还原;Gamma(5) = 4! = 24;Gamma(100) ≈ 9.3e155 尚未溢出,其自然对数与 LogGamma 逐位一致,再往上 Gamma 变无穷而 LogGamma 依旧可用。

7.3 要点提示

  • 概率密度连乘(似然函数)一律转对数空间求和,再 Math.exp 一次还原,这是与 LogGamma/LogBeta 的组合套路;
  • Digamma.value(1.0) = -0.5772156649015314(负欧拉常数)可作为实现正确性的快速校验点;
  • 大参数 Beta 走 LogBeta。

八、组合数学与溢出管理:combinatorics 三件套

8.1 实测示例:同一组合数的三种精度

import org.apache.commons.numbers.combinatorics.BinomialCoefficient;
import org.apache.commons.numbers.combinatorics.BinomialCoefficientDouble;
import org.apache.commons.numbers.combinatorics.LogBinomialCoefficient;
import org.apache.commons.numbers.combinatorics.Factorial;

System.out.println("BinomialCoefficient.value(66, 33) = " + BinomialCoefficient.value(66, 33));
try {
    System.out.println("BinomialCoefficient.value(67, 33) = " + BinomialCoefficient.value(67, 33));
} catch (RuntimeException e) {
    System.out.println("BinomialCoefficient.value(67, 33) = " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
System.out.println("BinomialCoefficientDouble.value(67, 33) = "
        + BinomialCoefficientDouble.value(67, 33));
System.out.println("LogBinomialCoefficient.value(1000, 500) = "
        + LogBinomialCoefficient.value(1000, 500));
System.out.println("Factorial.value(20) = " + Factorial.value(20));
try {
    System.out.println("Factorial.value(21) = " + Factorial.value(21));
} catch (RuntimeException e) {
    System.out.println("Factorial.value(21) = " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
System.out.println("Factorial.doubleValue(170) = " + Factorial.doubleValue(170));

实测回显:

BinomialCoefficient.value(66, 33) = 7219428434016265740
BinomialCoefficient.value(67, 33) = ArithmeticException: long overflow
BinomialCoefficientDouble.value(67, 33) = 1.4226520737620287E19
LogBinomialCoefficient.value(1000, 500) = 689.4672615678512
Factorial.value(20) = 2432902008176640000
Factorial.value(21) = CombinatoricsException: Number 21 is out of range [0, 20]
Factorial.doubleValue(170) = 7.257415615307999E306

验证结果:C(66,33) = 7219428434016265740 是 long 的极限内最大组合数之一,C(67,33) 溢出直接抛 ArithmeticException: long overflow 而非回绕成负数;Double 版给出 1.42e19(末几位失真);Log 版可算到 n=1000;阶乘精确上限 20,浮点上限 170。

8.2 要点提示

  • 组合数/阶乘三件套(精确 long、Double、Log)与方法名一一对应,按数值范围选,不要用 try-catch 探测溢出做控制流;
  • 需要精确大数组合数时,用 BigFraction 组合 C(n,k) = BigFraction(n!) 亦可行,代价是大数开销;
  • 斯特林数(Stirling.stirlingS1/S2)与组合枚举(Combinations.of(n, k),惰性迭代)覆盖划分与遍历场景。

九、求根与 top-k:rootfinder 与 arrays

9.1 实测示例:Brent 法求非线性方程根

import org.apache.commons.numbers.rootfinder.BrentSolver;
import java.util.function.DoubleUnaryOperator;

BrentSolver solver = new BrentSolver(1e-12, 1e-12, 1e-12);
DoubleUnaryOperator f1 = x -> x * x - 2;
double r1 = solver.findRoot(f1, 0.0, 2.0);
System.out.println("root of x^2 - 2 in [0, 2] = " + r1);
System.out.println("f1(root)                  = " + f1.applyAsDouble(r1));
DoubleUnaryOperator f2 = Math::cos;
double r2 = new BrentSolver(1e-15, 1e-15, 1e-15).findRoot(f2, 0.0, 2.0);
System.out.println("root of cos(x) in [0, 2]  = " + r2);

实测回显:

root of x^2 - 2 in [0, 2]  = 1.4142135623731364
Math.sqrt(2) 核对          = 1.4142135623730951
f1(root)                   = 1.1723955140041653E-13
root of cos(x) in [0, 2]   = 1.5707963267948966
cos(root) 核对             = 6.123233995736766E-17

验证结果:x^2-2 的根与 Math.sqrt(2) 相差 4e-13,根处函数残差 1.17e-13 与 1e-12 精度设置吻合;cos(x) 在 [0,2] 的根为 pi/2。Brent 法兼取二分法的稳健与割线法的快收敛,区间端点异号即可,无需导数。

9.2 实测示例:Selection 求分位数与 top-k

import org.apache.commons.numbers.arrays.Selection;
import java.util.Arrays;

double[] data = {9, 3, 7, 1, 8, 2, 6, 5, 4, 0};
double[] copy = data.clone();
Selection.select(copy, 3);
System.out.println("a[3] (第4小的数) = " + copy[3]);
double[] copy2 = data.clone();
Selection.select(copy2, new int[]{0, 9});
System.out.println("after select(copy, [0, 9]): " + Arrays.toString(copy2));

实测回显:

after select(copy, 3): [0.0, 1.0, 2.0, 3.0, 8.0, 4.0, 6.0, 5.0, 7.0, 9.0]
a[3] (第4小的数) = 3.0
after select(copy, [0, 9]): [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]

验证结果:单关键索引 select 只保证第 k 位正确、两侧落位(平均 O(n)),比全排序省;双关键索引 {0, 9} 实测把最小、最大分位拉通后全数组升序就位。监控分位数(P50/P95/P99)场景,一次多关键索引调用即可拿到全部分位。

9.3 要点提示

  • findRoot 区间无变号直接抛 SolverException,调用前先粗扫区间;
  • Selection 原地修改入参数组,求 top-k 前先 clone;
  • 与 SortInPlace(键值同步排序)配合,可对 (分数, id) 结构按分数取 top-k 再映射回对象。

十、最佳实践清单

  1. 版本对齐:一律通过 commons-numbers-bom 引入模块,11 个模块保持同版本(本文 1.3),避免混版带来的 NoSuchMethodError。
  2. 按需引模块:core 约 100 KB 零依赖;gamma、combinatorics、arrays、primes、rootfinder 完全独立;引得越少,依赖树越干净。
  3. 比较不过等号:浮点判定一律 Precision 的 ULP/绝对/相对三选一,NaN 语义用 equalsIncludingNaN 显式表态。
  4. 求和防漂移:长序列、大跨度数值求和用 Sum(或 DD),统计与对账默认开启补偿求和。
  5. 溢出即异常是特性:pow、阶乘、组合数在边界抛异常,捕获后降级到 Double/Log 变体,而不是预先除以一个大数。
  6. 概率走对数空间:连乘概率与组合数统一用 LogGamma/LogBinomialCoefficient,最后一次性 exp。
  7. 精确量用分数:配比、时间码、需要 == 1 判定的场景用 Fraction/BigFraction;注意 1.3 不做符号归一。
  8. 单位入类型:度、弧度用 angle 模块的 Deg/Rad 值对象携带单位,杜绝手写换算。
  9. 异常捕获用公共父类:FractionException、CombinatoricsException 均为包级私有,catch RuntimeException 或 ArithmeticException。
  10. 运行编码显式化:生产环境 java 命令统一加 -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8,避免 locale 差异导致日志乱码。
  11. 离线 jar 直跑核对依赖:fraction/angle/complex/quaternion/field 依赖 core,手动 classpath 时带上 commons-numbers-core-1.3.jar。
  12. 迁移自 math3 时过一遍命名差异:conj 与 conjugate、arg 与 getArgument、静态工厂与 new 构造。

十一、参考链接