Apache Commons Numbers 1.3 常用 API 与核心模块详解
Apache Commons Numbers 1.3 常用 API 与核心模块详解,按 11 个模块族逐章拆解 Precision、Fraction、Complex、Quaternion 等核心类的用法并附实测回显。
Apache Commons Numbers 1.3 常用 API 与核心模块详解
目标:按模块族拆解 Apache Commons Numbers 1.3 的 11 个运行时模块,逐章给出核心类的 API 签名、调用示例与实测回显,全部示例已在 Ubuntu 22.04 的 OpenJDK 17 环境编译运行通过。
一、环境信息
- 操作系统: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)统一管理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共 11 个运行时模块 - 工作目录:
/root/numbers-demo
二、模块与 API 总览
Numbers 1.3 的设计基调是:不可变值对象、静态工厂构造、溢出即抛异常、单模块零额外依赖。11 个模块与核心类总览:
| 模块 artifactId | 核心类 | 一句话职责 |
|---|---|---|
| commons-numbers-core | Precision、ArithmeticUtils、Sum、DD | 浮点比较、整数运算与溢出、补偿求和、双双精度 |
| commons-numbers-angle | Angle(嵌套 Rad/Deg/Turn)、Reduce、CosAngle | 角度单位与归一化、向量夹角 |
| commons-numbers-arrays | Selection、SortInPlace、MultidimensionalCounter | 顺序统计量、键值同步排序、多维索引换算 |
| commons-numbers-combinatorics | Factorial、BinomialCoefficient、LogFactorial、Stirling、Combinations | 阶乘与组合数学 |
| commons-numbers-complex | Complex | 不可变复数 |
| commons-numbers-field | Field、FP64、FP64Field、FractionField | 域代数抽象与泛型算法 |
| commons-numbers-fraction | Fraction、BigFraction、ContinuedFraction、GeneralizedContinuedFraction | 分数与连分数 |
| commons-numbers-gamma | Gamma、LogGamma、Digamma、Trigamma、Erf、Erfc、Beta | 特殊函数族 |
| commons-numbers-primes | Primes | 素数判定、生成、分解 |
| commons-numbers-quaternion | Quaternion、Slerp | 四元数与球面插值 |
| commons-numbers-rootfinder | BrentSolver | Brent 法非线性求根 |
依赖关系极简:primes、arrays、combinatorics、gamma、rootfinder 完全不依赖其他模块;fraction、angle、complex、quaternion、field 依赖 core(Maven 下自动传递)。本章示例工程同时引入全部 11 个模块,用 BOM 锁定 1.3。
三、core 模块:精度控制与整数运算
3.1 Precision:浮点比较与舍入
浮点数不能直接用 == 比较,Precision 提供三套语义不同的比较策略:
| 策略 | 方法 | 适用场景 |
|---|---|---|
| ULP 位比较 | equals(a, b, maxUlps) | 通用默认,与数值大小无关,二进制位级容差 |
| 绝对误差 | equals(a, b, epsilon) | 数值范围已知且固定(如 [0,1] 的概率) |
| 相对误差 | equalsWithRelativeTolerance(a, b, tol) | 数值跨数量级(如 1e-3 到 1e6 的物理量) |
💡 技术拓展:浮点比较策略对比——绝对误差在数值大时形同虚设(1e10 与 1e10+0.001 的绝对误差很小但相对误差更小),相对误差在接近 0 时失效(0 与 1e-300 的相对误差无穷大),ULP 比较则在 0 与 -0.0、NaN 附近各有特判。本文示例统一使用 ULP 比较为主、绝对误差为辅的组合,NaN 处理用
equalsIncludingNaN系列。
import org.apache.commons.numbers.core.Precision;
// ULP 比较:1.1 * 3 与 3.3 相差 1 个 ULP
System.out.println("equals(1.1*3, 3.3) = " + Precision.equals(1.1 * 3, 3.3));
System.out.println("equals(1.1*3, 3.3, 1 ulp) = " + Precision.equals(1.1 * 3, 3.3, 1));
// NaN 与正负零的语义差
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));
System.out.println("compareTo(3.3, 3.3000000000000003, 1 ulp) = "
+ Precision.compareTo(3.3, 3.3000000000000003, 1));
// 舍入与对象化等价器
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));
Precision.DoubleEquivalence eq1e6 = Precision.doubleEquivalenceOfEpsilon(1e-6);
System.out.println("eq1e6.eq(1.0000005, 1.0) = " + eq1e6.eq(1.0000005, 1.0));
System.out.println("eq1e6.eq(1.000002, 1.0) = " + eq1e6.eq(1.000002, 1.0));
System.out.println("eq1e6.eq(NaN, NaN) = " + eq1e6.eq(Double.NaN, Double.NaN));
System.out.println("representableDelta(1.0, 3) = " + Precision.representableDelta(1.0, 3));
System.out.println("representableDelta(1e300, 1e-300) = " + Precision.representableDelta(1e300, 1e-300));
实测回显:
equals(1.1*3, 3.3) = true
equals(1.1*3, 3.3, 1 ulp) = true
equals(NaN, NaN) = false
equalsIncludingNaN(NaN, NaN) = true
equalsIncludingNaN(0.0, -0.0) = true
equalsWithRelativeTolerance(100.0, 100.00001, 1e-6) = true
compareTo(3.3, 3.3000000000000003, 1 ulp) = 0
round(3.14159, 2) = 3.14
round(2.675, 2) = 2.68
round(2.675, 2, HALF_UP) = 2.68
EPSILON = 1.1102230246251565E-16
eq1e6.eq(1.0000005, 1.0) = true
eq1e6.eq(1.000002, 1.0) = false
eq1e6.eq(NaN, NaN) = true
eq1e6.lte(1.0, 2.0) = true
EPSILON = 1.1102230246251565E-16
SAFE_MIN = 2.2250738585072014E-308
round(1234.5678, 2) = 1234.57
representableDelta(1.0, 3) = 3.0
representableDelta(1e300, 1e-300) = 0.0
验证结果与要点提示:equals(NaN, NaN) 返回 false 而带 IncludingNaN 的重载返回 true,与 IEEE 754 及业务直觉各有取舍,选择时注意方法名;两参版 equals(a, b) 等价于 1 ULP 比较;round 默认 HALF_EVEN(银行家舍入),2.675 舍到 2.68 是因为其二进制表示本就略大于 2.675,显式指定 RoundingMode.HALF_UP 才是日常的四舍五入语义;doubleEquivalenceOfEpsilon 把比较策略封装成可传递的对象,便于注入通用算法;representableDelta(1e300, 1e-300) 返回 0.0 说明在 1e300 附近根本无法表示 delta 为 1e-300 的增量。
3.2 ArithmeticUtils:整数运算与溢出检测
import org.apache.commons.numbers.core.ArithmeticUtils;
System.out.println("gcd(48, 18) = " + ArithmeticUtils.gcd(48, 18));
System.out.println("gcd(-48, 18) = " + ArithmeticUtils.gcd(-48, 18));
System.out.println("lcm(4, 6) = " + ArithmeticUtils.lcm(4, 6));
System.out.println("pow(2, 10) = " + ArithmeticUtils.pow(2, 10));
System.out.println("pow(3, 39) = " + ArithmeticUtils.pow(3L, 39));
try {
System.out.println("pow(3, 40) = " + ArithmeticUtils.pow(3L, 40));
} catch (ArithmeticException e) {
System.out.println("pow(3, 40) = ArithmeticException: " + e.getMessage());
}
System.out.println("isPowerOfTwo(1024) = " + ArithmeticUtils.isPowerOfTwo(1024));
System.out.println("isPowerOfTwo(1000) = " + ArithmeticUtils.isPowerOfTwo(1000));
System.out.println("divideUnsigned(-1, 4) = " + ArithmeticUtils.divideUnsigned(-1, 4));
System.out.println("remainderUnsigned(-1, 4) = " + ArithmeticUtils.remainderUnsigned(-1, 4));
实测回显:
gcd(48, 18) = 6
gcd(-48, 18) = 6
lcm(4, 6) = 12
pow(2, 10) = 1024
pow(3, 39) = 4052555153018976267
pow(3, 40) = ArithmeticException: long overflow
isPowerOfTwo(1024) = true
isPowerOfTwo(1000) = false
divideUnsigned(-1, 4) = 1073741823
remainderUnsigned(-1, 4) = 3
验证结果与要点提示:pow 在结果溢出 long 时直接抛 ArithmeticException: long overflow(3^39 尚可容纳,3^40 溢出),把静默回绕变成显式故障,这是 Numbers 对 JDK Math.pow 整数版最大的增强;gcd 对负数返回正最大公约数;divideUnsigned/remainderUnsigned 把入参解释为无符号 32 位数(-1 即 4294967295),做哈希扰动时很有用。
3.3 Sum:补偿求和与点积
import org.apache.commons.numbers.core.Sum;
double naive = 0.0;
for (int i = 0; i < 10; i++) {
naive += 0.1;
}
Sum s = Sum.create();
for (int i = 0; i < 10; i++) {
s.add(0.1);
}
System.out.println("naive 0.1 x 10 = " + naive);
System.out.println("Sum.add(0.1) x 10 = " + s.getAsDouble());
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());
实测回显:
naive 0.1 x 10 = 0.9999999999999999
Sum.add(0.1) x 10 = 1.0
naive dot(a, b) = 0.0
Sum.ofProducts(a,b) = 2.0
验证结果与要点提示:朴素累加 0.1 十次得到 0.9999999999999999,Sum 补偿求和得到精确的 1.0;点积案例更极端——大小交替抵消的项让朴素求和归零,Sum.ofProducts 以扩展精度给出正确答案 2.0。求和顺序敏感的统计场景(方差、损失函数)建议一律走 Sum。
3.4 DD:双双精度(double-double)
import org.apache.commons.numbers.core.DD;
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());
System.out.println("DD sqrt(2).doubleValue = " + DD.of(2).sqrt().doubleValue());
实测回显:
double: 1e16 + 1 = 1.0E16
DD: 1e16 + 1 = 10000000000000001
DD hi/lo = 1.0E16 / 1.0
DD sqrt(2).doubleValue = 1.4142135623730951
验证结果与要点提示:double 只有约 15.95 位十进制有效数字,1e16 量级加 1 直接丢失;DD 用一对 double(hi 主体、lo 补偿)把有效数字扩展到约 32 位,bigDecimalValue() 可以无损打印整个 hi+lo。DD 是纯 Java 实现、无依赖,适合 Kahan 求和不奏效的病态求和与长程积分。
四、angle 模块:角度单位与归一化
4.1 Angle:弧度、度、圈三种单位
import org.apache.commons.numbers.angle.Angle;
Angle.Rad halfPi = Angle.Rad.of(Math.PI / 2);
System.out.println("Rad.of(pi/2).toDeg().getAsDouble() = " + halfPi.toDeg().getAsDouble());
System.out.println("Deg.of(90).toRad().getAsDouble() = "
+ Angle.Deg.of(90).toRad().getAsDouble());
System.out.println("Deg.of(90).toTurn().getAsDouble() = "
+ Angle.Deg.of(90).toTurn().getAsDouble());
System.out.println("Rad.of(3.5*pi).toDeg().getAsDouble() = "
+ Angle.Rad.of(3.5 * Math.PI).toDeg().getAsDouble());
实测回显:
Rad.of(pi/2).toDeg().getAsDouble() = 90.0
Deg.of(90).toRad().getAsDouble() = 1.5707963267948966
Deg.of(90).toTurn().getAsDouble() = 0.25
Rad.of(3.5*pi).toDeg().getAsDouble() = 630.0
验证结果与要点提示:Angle 是抽象类,具体值对象是其三个嵌套类 Angle.Rad、Angle.Deg、Angle.Turn,单位信息编码在类型里(toDeg() 返回 Deg 对象,getAsDouble() 取值),从根源上避免度与弧度混用——这在纯 double 传参的代码里是最常见的坐标换算 bug。
4.2 归一化:normalizer 与 Reduce
import org.apache.commons.numbers.angle.Reduce;
import java.util.function.DoubleUnaryOperator;
DoubleUnaryOperator n360 = Angle.Deg.normalizer(0);
System.out.println("Deg.normalizer(0).apply(450) = " + n360.applyAsDouble(450));
DoubleUnaryOperator n2pi = Angle.Rad.normalizer(0);
System.out.println("Rad.normalizer(0).apply(3.5*pi) = " + n2pi.applyAsDouble(3.5 * Math.PI));
Reduce reduce = new Reduce(0.0, 2 * Math.PI);
System.out.println("new Reduce(0, 2pi).apply(3.5*pi) = " + reduce.applyAsDouble(3.5 * Math.PI));
实测回显:
Deg.normalizer(0).apply(450) = 90.0
Rad.normalizer(0).apply(3.5*pi) = 4.71238898038469
new Reduce(0, 2pi).apply(3.5*pi) = 4.71238898038469
验证结果与要点提示:两个易错点——normalizer(lowerBound) 的唯一参数是区间下界而非周期,把角度规约到 [下界, 下界 + 单位整圆);Reduce 构造参数顺序是 (offset, period),先是下界后是周期,写反会因周期为 0 而返回 NaN(实测 new Reduce(2*Math.PI, 0.0) 对任何输入均得 NaN)。Reduce 采用两级双精度归约,输入比周期大 2^50 倍以上才会失真。
4.3 CosAngle:向量夹角余弦
import org.apache.commons.numbers.angle.CosAngle;
System.out.println("CosAngle.value([1,2,3], [2,4,6]) = "
+ CosAngle.value(new double[]{1, 2, 3}, new double[]{2, 4, 6}));
System.out.println("CosAngle.value([1,0], [0,1]) = "
+ CosAngle.value(new double[]{1, 0}, new double[]{0, 1}));
实测回显:
CosAngle.value([1,2,3], [2,4,6]) = 1.0
CosAngle.value([1,0], [0,1]) = 0.0
验证结果与要点提示:平行向量余弦 1、正交向量余弦 0;与手写 dot/(|a||b|) 相比,CosAngle 内部用扩展精度处理点积与范数,长向量或病态方向(接近反向的短向量)下更稳。两向量长度须一致且非零,零向量抛异常。
五、fraction 模块:分数与连分数
5.1 Fraction:有理数精确运算
import org.apache.commons.numbers.fraction.Fraction;
Fraction f1 = Fraction.of(1, 3);
Fraction f2 = Fraction.of(1, 6);
System.out.println("f1.add(f2) = " + f1.add(f2));
System.out.println("f1.divide(f2) = " + f1.divide(f2));
System.out.println("Fraction.of(2, 4) = " + Fraction.of(2, 4));
System.out.println("Fraction.of(-3, -6) = " + Fraction.of(-3, -6));
System.out.println("Fraction.from(3.14159, 100) = " + Fraction.from(3.14159, 100));
try {
Fraction.of(1, 0);
} catch (RuntimeException e) {
System.out.println("Fraction.of(1, 0) = " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
System.out.println("compareTo: (-1/3) vs (-1/2) = " + Fraction.of(-1, 3).compareTo(Fraction.of(-1, 2)));
实测回显:
f1 = 1 / 3, f2 = 1 / 6
f1.add(f2) = 1 / 2
f1.subtract(f2) = 1 / 6
f1.multiply(f2) = 1 / 18
f1.divide(f2) = 2
f1.pow(2) = 1 / 9
f1.reciprocal() = 3
f1.abs() = 1 / 3
Fraction.of(2, 4) = 1 / 2 (numerator=1, denominator=2)
Fraction.of(-3, -6) = -1 / -2
Fraction.of(6, 3) = 2
f1.doubleValue() = 0.3333333333333333
Fraction.parse("3/4") = 3 / 4
Fraction.from(0.5) = 1 / 2
Fraction.from(3.14159, 100) = 22 / 7
Fraction.of(1, 0) = FractionException: Denominator must be different from 0
compareTo: (-1/3) vs (-1/2) = 1
验证结果与要点提示:四个必知行为——构造即按 gcd 约分(2/4 变 1/2);1.3 只约分不翻符号,of(-3, -6) 得到 -1 / -2 而非 1/2(负分母参与 equals 与排序无碍,compareTo 在 1.3 修复了负数比较错误,输出 1 表示 -1/3 大于 -1/2,正确);整数值 toString 省略分母(of(6, 3) 输出 2);FractionException 是包级私有类,包外只能按 RuntimeException 捕获。
💡 技术拓展:Fraction 与 BigFraction 选型对比——Fraction 用两个 int 存储、运算快、但分子分母超 21 亿即溢出(乘法尤甚);BigFraction 用 BigInteger 存储、范围无限、但每次运算都有大数分配开销。经验法则:小分母(媒体时间码、比例、小数网格)用 Fraction;连乘链、密码学或天文数值用 BigFraction;拿不准时先 BigFraction 跑通再做性能收窄。本文两个示例类都已实测。
5.2 BigFraction:任意精度有理数
import org.apache.commons.numbers.fraction.BigFraction;
import java.math.RoundingMode;
BigFraction b1 = BigFraction.of(1, 3);
BigFraction b2 = BigFraction.of(1, 6);
System.out.println("b1.add(b2) = " + b1.add(b2));
System.out.println("b1.bigDecimalValue(4, HALF_UP) = " + b1.bigDecimalValue(4, RoundingMode.HALF_UP));
System.out.println("BigFraction.of(123456789012345, 987654321098765) = "
+ BigFraction.of(123456789012345L, 987654321098765L));
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());
实测回显:
b1.add(b2) = 1 / 2
b1.bigDecimalValue(4, HALF_UP) = 0.3333
BigFraction.of(123456789012345, 987654321098765) = 24691357802469 / 197530864219753
BigFraction.from(pi, 100) = 22 / 7
BigFraction.from(pi, 100).doubleValue() = 3.142857142857143
验证结果与要点提示:bigDecimalValue(4, HALF_UP) 直接给出可控舍入的十进制表示,避免手写 BigDecimal 转换;from(pi, 100) 用连分数展开法给出分母不超过 100 的最佳有理逼近 22/7,比逐位扫描法稳得多;长整型入参自动 BigInteger 化并约分。
5.3 ContinuedFraction:连分数求值
import org.apache.commons.numbers.fraction.ContinuedFraction;
// 自然常数 e 的简单连分数展开:b0=2,其后系数按 1、2、1、1、4、1、1、6 循环递增
ContinuedFraction eCF = new ContinuedFraction() {
@Override
protected double getA(int n, double x) {
return 1.0;
}
@Override
protected double getB(int n, double x) {
if (n == 0) {
return 2.0;
}
if (n % 3 == 2) {
return 2.0 * ((n + 1) / 3);
}
return 1.0;
}
};
double e = eCF.evaluate(0.0, 1e-15, 50);
System.out.println("e via ContinuedFraction = " + e);
System.out.println("Math.E = " + Math.E);
System.out.println("abs diff = " + Math.abs(e - Math.E));
实测回显:
e via ContinuedFraction = 2.7182818284590455
Math.E = 2.718281828459045
abs diff = 4.440892098500626E-16
验证结果与要点提示:ContinuedFraction 是抽象类,子类实现 getA(n, x)(分子)与 getB(n, x)(分母),求值公式为 b0 加 a1/(b1 加 a2/(b2 加更深层的同构嵌套));evaluate(x, epsilon, maxIterations) 以修正的 Lentz 算法迭代到误差达标。连分数是计算特殊函数的标准手段,适合自定义收敛级数;注意 e 这类阶乘收敛级几十项即达机器精度,而 Brouncker 圆周率连分数收敛极慢(误差约 2/N),实测 5000 项仍达不到 1e-12,选型时先评估收敛速度。
六、primes 模块:素数判定与分解
6.1 Primes:静态工具类
import org.apache.commons.numbers.primes.Primes;
System.out.println("isPrime(97) = " + Primes.isPrime(97));
System.out.println("isPrime(2147483647) = " + Primes.isPrime(2147483647));
System.out.println("nextPrime(100) = " + Primes.nextPrime(100));
System.out.println("primeFactors(360) = " + Primes.primeFactors(360));
实测回显:
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]
💡 技术拓展:素数判定算法对比——试除法实现简单但 O(√n),n 到 1e9 就吃力;Miller-Rabin 概率判定配合确定性底数组合对 32 位整数是严格正确的,单次判定微秒级;埃氏筛适合一次性判定 1e7 以内的所有素数(占内存换时间)。Numbers 1.3 的
Primes.isPrime即小素数试除加 Miller-Rabin 的混合实现,本文实测 2^31 - 1 梅森素数瞬间返回 true。
验证结果与要点提示:nextPrime 入参已是素数时原样返回;primeFactors 返回升序分解列表(带重复因子);三方法入参均为 int(最大 2147483647),超出范围抛 IllegalArgumentException。
七、complex 模块:复数
7.1 构造与四则运算
import org.apache.commons.numbers.complex.Complex;
Complex a = Complex.ofCartesian(3, 4);
Complex b = Complex.ofCartesian(1, -2);
System.out.println("a = " + a);
System.out.println("a.add(b) = " + a.add(b));
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.arg() = " + a.arg());
System.out.println("a.conj() = " + a.conj());
System.out.println("I.multiply(I) = " + Complex.I.multiply(Complex.I));
实测回显:
a = (3.0,4.0), b = (1.0,-2.0)
a.add(b) = (4.0,2.0)
a.subtract(b) = (2.0,6.0)
a.multiply(b) = (11.0,-2.0)
a.divide(b) = (-1.0,2.0)
a.abs() = 5.0
a.arg() = 0.9272952180016122
a.conj() = (3.0,-4.0)
a.pow(2) = (-6.999999999999997,24.0)
a.sqrt() = (2.0,1.0)
I.multiply(I) = (-1.0,0.0)
验证结果与要点提示:三种静态工厂——ofCartesian(re, im) 直角坐标、ofPolar(mod, arg) 极坐标、ofCis(arg) 单位圆(实测 ofCis(pi) 得 (-1.0, 1.2246467991473532E-16),虚部残留来自 pi 的有限精度);方法名是 conj()/arg()/abs(),与 math3 的 conjugate()/getArgument()/abs() 不同;实部虚部常量 Complex.I 的平方严格等于 -1。
7.2 超越函数与开方
System.out.println("a.exp() = " + a.exp());
System.out.println("a.sqrt() = " + a.sqrt());
System.out.println("a.nthRoot(3) = " + a.nthRoot(3));
实测回显:
a.exp() = (-13.128783081462158,-15.200784463067954)
a.sqrt() = (2.0,1.0)
a.nthRoot(3) = [(1.6289371459221758,0.5201745023045458), (-1.2649529063577512,1.1506136983844508), (-0.36398423956442444,-1.6707882006889963)]
验证结果与要点提示:(3+4i)^0.5 = 2+i 可手算验证;nthRoot(k) 返回全部 k 个根(按辐角顺序);pow(2) 实测 (-6.999999999999997, 24.0) 实部带 3e-15 误差——复数幂走 exp/log 路径,纯整数次幂若需严格结果应手写乘法。
八、quaternion 模块:四元数
8.1 构造与 Hamilton 乘法
import org.apache.commons.numbers.quaternion.Quaternion;
System.out.println("I.multiply(J) = " + Quaternion.I.multiply(Quaternion.J));
System.out.println("J.multiply(K) = " + Quaternion.J.multiply(Quaternion.K));
System.out.println("K.multiply(I) = " + Quaternion.K.multiply(Quaternion.I));
Quaternion p = Quaternion.of(1.0, new double[]{2.0, 3.0, 4.0});
System.out.println("p = " + p);
System.out.println("p.normSq() = " + p.normSq());
Quaternion q = Quaternion.of(0.7071067811865476, 0.7071067811865476, 0.0, 0.0);
System.out.println("q.norm() = " + q.norm());
System.out.println("q.isUnit(1e-10) = " + q.isUnit(1e-10));
System.out.println("q.conjugate() = " + q.conjugate());
实测回显:
I.multiply(J) = [0.0 0.0 0.0 1.0]
J.multiply(K) = [0.0 1.0 0.0 0.0]
K.multiply(I) = [0.0 0.0 1.0 0.0]
p = of(1, [2,3,4]) = [1.0 2.0 3.0 4.0]
p.getScalarPart() = 1.0
p.getVectorPart() = [2.0, 3.0, 4.0]
p.normSq() = 30.0
q = [0.7071067811865476 0.7071067811865476 0.0 0.0]
q.norm() = 1.0
q.isUnit(1e-10) = true
q.conjugate() = [0.7071067811865476 -0.7071067811865476 -0.0 -0.0]
q.inverse() = [0.7071067811865475 -0.7071067811865475 -0.0 -0.0]
slerp(0.5) = [0.5000000000000001 0.5000000000000001 0.0 0.7071067811865476]
验证结果与要点提示:Hamilton 乘法 i×j=k、j×k=i、k×i=i 系全部实测成立;of(w, double 数组) 用标量加三维向量构造,getScalarPart/getVectorPart 对应取出;四元数乘法不满足交换律,Quaternion.multiply(I, J) 静态写法与 I.multiply(J) 实例写法结果一致(实测同值);unit 判定 isUnit(tol) 内部用到 core 的 Precision,纯 jar 直跑场景必须把 commons-numbers-core-1.3.jar 放进 classpath。
8.2 Slerp:球面线性插值
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]
验证结果与要点提示:slerp 的 apply(t) 要求 t 在 [0, 1],两端点精确还原;与对四元数分量做线性插值再归一化(nlerp)相比,slerp 保证角速度恒定,是相机轨迹与骨骼动画的标准做法。
九、gamma 模块:特殊函数
9.1 Gamma 函数族
import org.apache.commons.numbers.gamma.Gamma;
import org.apache.commons.numbers.gamma.LogGamma;
import org.apache.commons.numbers.gamma.Digamma;
import org.apache.commons.numbers.gamma.Trigamma;
System.out.println("Gamma.value(5.0) = " + Gamma.value(5.0));
System.out.println("Gamma.value(0.5) = " + Gamma.value(0.5));
System.out.println("Gamma.value(100.0) = " + Gamma.value(100.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)));
System.out.println("Digamma.value(1.0) = " + Digamma.value(1.0));
System.out.println("Trigamma.value(1.0) = " + Trigamma.value(1.0));
实测回显:
Gamma.value(5.0) = 24.0
Gamma.value(0.5) = 1.772453850905516
Gamma.value(100.0) = 9.332621544394415E155
LogGamma.value(100.0) = 359.1342053695754
Math.log(Gamma.value(100)) 核对 = 359.1342053695754
Digamma.value(1.0) = -0.5772156649015314
Trigamma.value(1.0) = 1.644934066848156
验证结果与要点提示:Gamma.value(n+1) = n!(实测 5.0 得 4! = 24),Gamma.value(0.5) = sqrt(pi);超过 171 后 Gamma 直接溢出为无穷,而 LogGamma 全程可用——99! 的对数 359.1342053695754 与先算 Gamma 再取对数的核对值逐位一致;Digamma.value(1) 理论值是负欧拉常数 -0.5772156649015329,实测 -0.5772156649015314,误差 1.5e-15,符合 double 精度预期。
9.2 误差函数与 Beta 函数
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.Beta;
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("Beta.value(2, 3) = " + Beta.value(2, 3));
System.out.println("LogBeta.value(2, 3) = " + org.apache.commons.numbers.gamma.LogBeta.value(2, 3));
实测回显:
Erf.value(1.0) = 0.8427007929497149
Erfc.value(1.0) = 0.15729920705028513
InverseErfc.value(Erfc.value(1.0)) = 1.0
Beta.value(2, 3) = 0.0833333333333333
LogBeta.value(2, 3) = -2.4849066497880004
验证结果与要点提示:Erf 即标准正态分布 CDF 的核心(P(|Z| < 1) = erf(1/sqrt(2))),配合 InverseErfc 可做正态分位数换算(实测逆运算精确还原 1.0);Beta(2,3) = Γ(2)Γ(3)/Γ(5) = 1×2/24 = 1/12 ≈ 0.08333,与实测一致;大参数场景一律走 LogBeta。
十、combinatorics 模块:组合数学
10.1 阶乘:精确与浮点两条路
import org.apache.commons.numbers.combinatorics.Factorial;
import org.apache.commons.numbers.combinatorics.LogFactorial;
System.out.println("Factorial.value(20) = " + Factorial.value(20));
System.out.println("Factorial.doubleValue(170) = " + Factorial.doubleValue(170));
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("LogFactorial.create().value(1000) = " + LogFactorial.create().value(1000));
实测回显:
Factorial.value(20) = 2432902008176640000
Factorial.doubleValue(170) = 7.257415615307999E306
Factorial.value(21) = CombinatoricsException: Number 21 is out of range [0, 20]
LogFactorial.create().value(1000) = 5912.128178488163
💡 技术拓展:阶乘溢出应对选型——n 不超过 20 用
Factorial.value(long 精确值,超出抛 CombinatoricsException);n 不超过 170 用Factorial.doubleValue(浮点近似,再大溢出为无穷);任意规模或参与除法约分的表达式用LogFactorial(对数空间,1000! 的对数照常计算)。组合数同理对应BinomialCoefficient、BinomialCoefficientDouble、LogBinomialCoefficient三件套。本文三类均已实测。
10.2 二项式系数与斯特林数
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.Stirling;
System.out.println("BinomialCoefficient.value(52, 5) = " + BinomialCoefficient.value(52, 5));
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("Stirling.stirlingS2(5, 2) = " + Stirling.stirlingS2(5, 2));
System.out.println("Stirling.stirlingS1(4, 2) = " + Stirling.stirlingS1(4, 2));
实测回显:
BinomialCoefficient.value(52, 5) = 2598960
BinomialCoefficient.value(66, 33) = 7219428434016265740
BinomialCoefficient.value(67, 33) = ArithmeticException: long overflow
BinomialCoefficientDouble.value(67, 33) = 1.4226520737620287E19
LogBinomialCoefficient.value(1000, 500) = 689.4672615678512
Stirling.stirlingS2(5, 2) = 15
Stirling.stirlingS1(4, 2) = 11
验证结果与要点提示:C(66,33) 是 long 能容纳的极限组合数(7.2e18),C(67,33) 溢出即抛异常,而非返回负数——溢出行为可预期;扑克牌型数 C(52,5) = 2598960 可手算复核;斯特林第二类数 S(5,2) = 15 表示把 5 个元素划分为 2 个非空集合的方式数。
10.3 Combinations:组合枚举
import org.apache.commons.numbers.combinatorics.Combinations;
import java.util.Iterator;
Combinations c = Combinations.of(5, 2);
System.out.println("Combinations.of(5, 2): n=" + c.getN() + " k=" + c.getK());
int count = 0;
StringBuilder first5 = new StringBuilder();
Iterator<int[]> it = c.iterator();
while (it.hasNext()) {
int[] combo = it.next();
if (count < 5) {
first5.append(java.util.Arrays.toString(combo)).append(" ");
}
count++;
}
System.out.println("前 5 组合: " + first5.toString().trim());
System.out.println("组合总数 = " + count);
实测回显:
Combinations.of(5, 2): n=5 k=2
前 5 组合: [0, 1] [0, 2] [1, 2] [0, 3] [1, 3]
组合总数 = 10(= C(5,2) = 10)
验证结果与要点提示:Combinations 实现 Iterable,按字典序惰性产出组合,不物化 C(n,k) 个数组,适合大 n 小 k 的遍历;枚举总数实测 10,与公式一致。
十一、arrays 模块:数组算法
11.1 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("after select(copy, 3): " + Arrays.toString(copy));
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));
实测回显:
before: [9.0, 3.0, 7.0, 1.0, 8.0, 2.0, 6.0, 5.0, 4.0, 0.0]
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(data, k) 是原地部分排序,只保证第 k 位为第 k 小(0 基),其余按快排分区落位,不必全排序;关键索引形式 select(data, int 数组) 一次定位多个分位点,实测传入 {0, 9} 后整个数组升序就位(最小与最大分位拉通整个数组)。适合求中位数、分位数与 top-k,平均 O(n)。
11.2 SortInPlace:键值同步排序
import org.apache.commons.numbers.arrays.SortInPlace;
double[] keys = {3.0, 1.0, 2.0};
double[] vals = {30.0, 10.0, 20.0};
SortInPlace.ASCENDING.apply(keys, vals);
System.out.println("keys = " + Arrays.toString(keys));
System.out.println("values = " + Arrays.toString(vals));
实测回显:
keys = [1.0, 2.0, 3.0]
values = [10.0, 20.0, 30.0]
验证结果与要点提示:按 keys 升序(SortInPlace.DESCENDING 为降序)重排 keys 并把 vals 同步置换,保持键值配对;典型用途是按权重排序候选集后保留对应对象引用索引。
11.3 MultidimensionalCounter:多维索引换算
import org.apache.commons.numbers.arrays.MultidimensionalCounter;
MultidimensionalCounter counter = MultidimensionalCounter.of(2, 3, 4);
System.out.println("getSize() = " + counter.getSize());
System.out.println("getSizes() = " + Arrays.toString(counter.getSizes()));
System.out.println("toUni(1, 2, 3) = " + counter.toUni(1, 2, 3));
System.out.println("toMulti(23) = " + Arrays.toString(counter.toMulti(23)));
System.out.println("toMulti(0) = " + Arrays.toString(counter.toMulti(0)));
实测回显:
getSize() = 24
getSizes() = [2, 3, 4]
toUni(1, 2, 3) = 23
toMulti(23) = [1, 2, 3]
toMulti(0) = [0, 0, 0]
验证结果与要点提示:行主序换算,(1,2,3) 在 2×3×4 张量中的线性下标为 1×12 + 2×4 + 3 = 23,且 toMulti(23) 实测还原为 [1, 2, 3],互为逆运算;下标越界抛 IndexOutOfBoundsException。注意 1.3 的 arrays 模块已无旧文档提到的 DoubleArray 类(1.0 稳定版前已移除),数组封装直接用 JDK 结构。
十二、field 与 rootfinder 模块
12.1 field:域代数抽象
import org.apache.commons.numbers.field.FP64;
import org.apache.commons.numbers.field.FP64Field;
import org.apache.commons.numbers.field.FractionField;
FP64 x = FP64.of(6);
FP64 y = FP64.of(4);
System.out.println("x.add(y) = " + x.add(y));
System.out.println("x.divide(y) = " + x.divide(y));
System.out.println("x.reciprocal() = " + x.reciprocal());
FP64Field f = FP64Field.get();
System.out.println("FP64Field.get().one() = " + f.one() + ", zero() = " + f.zero());
System.out.println("FractionField.get().one() = " + FractionField.get().one());
实测回显:
x = 6.0, y = 4.0
x.add(y) = 10.0
x.subtract(y) = 2.0
x.multiply(y) = 24.0
x.divide(y) = 1.5
x.reciprocal() = 0.16666666666666666
x.pow(2) = 36.0
x.isZero()/isOne() = false / false
FP64Field.get().one() = 1.0, zero() = 0.0
FractionField.get().one() = 1
验证结果与要点提示:Field 接口统一 plus/times/reciprocal 等域运算,FP64、Fraction、BigFraction、DD 各有对应 Field 单例;泛型算法只面对 Field,可零成本切换数值类型(同一套高斯消元既跑 double 也跑分数)。
12.2 rootfinder: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
验证结果与要点提示:构造三参数依次是相对误差、绝对误差、函数值误差;findRoot 要求区间端点函数值异号,否则抛 SolverException;x^2-2 的根 1.4142135623731364 与真值相差 4e-13,恰在 1e-12 函数值精度内,比 math3 的 Pegasus/Illinois 更贴近原始 Brent 实现的收敛判据。
十三、API 使用注意事项
- 静态工厂优先:所有值对象用
of系列构造(Fraction.of、Complex.ofCartesian、Quaternion.of),构造器不开放,API 演进不会被实现细节绑死。 - 符号归一缺失:
Fraction.of(-3, -6)结果是-1 / -2,1.3 仅按 gcd 约分;跨模块传递前如有需要自行归一(分母为负时分子分母同取反)。 - 异常可见性:
FractionException、CombinatoricsException等异常类是包级私有,catch 用公共父类(RuntimeException 或 ArithmeticException),具体类型看方法文档。 - 溢出即异常:
ArithmeticUtils.pow、BinomialCoefficient.value、Factorial.value溢出直接抛异常而非回绕,防御式代码要 catch 或改用 Double/Log 系列变体。 - 角度归一化参数序:
Angle.*.normalizer(lowerBound)的参数是下界;Reduce(offset, period)先下界后周期,顺序反了得 NaN。 - 传递依赖:fraction、angle、complex、quaternion、field 依赖 core;quaternion 的
isUnit在运行期加载 core 的 Precision,离线 jar 直跑时别漏掉 core 包。 - 与 math3 命名差异速查:
conj()对 math3conjugate()、arg()对getArgument()、构造一律静态工厂、素数判定升级为 Miller-Rabin。
十四、参考链接
- 1.3 完整 javadoc:https://commons.apache.org/proper/commons-numbers/javadocs/api-release/index.html
- 模块清单与下载:https://downloads.apache.org/commons/numbers/
- Maven 中央仓库:https://repo1.maven.org/maven2/org/apache/commons/
- 源码仓库:https://github.com/apache/commons-numbers
- 变更记录:https://commons.apache.org/proper/commons-numbers/changes-report.html
- 邮件列表与社区:https://commons.apache.org/mail-lists.html