开始前,会这些就够了
- 知道
int有固定的可表示范围。 - 会读数组下标、整数除法与 % 余数运算。
- 先读 变量、基本类型与运算符:先算对一个表达式:先掌握
int的范围、运算顺序与类型转换,再定位绝对值的边界。
你把哈希值映射到三个桶,写下 Math.abs(hash) % 3。普通正数和负数都通过了测试,某次却出现 -2,下标访问失败。绝对值怎么还能是负数?
问题藏在 int 的最小值:它的负数一侧,比正数一侧多容纳一个数。把这个唯一的边界样例放进测试,就能看到普通输入始终没有触发的路径。
把例子带到自己的电脑上
下载完整练习包
需要 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- Main.java:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 Main.java
java -cp . Main解压后,在含 Main.java 的目录打开终端。命令分别回车执行。固定输入的独立 Java 17 程序,双 JDK 实测;互动区只回放记录。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
同一个哈希值,四种写法会落进哪个桶
正在准备实验控件…
从本页 Java 程序实际运行的 6 × 3 组结果中选取记录,不在浏览器模拟 Java int 溢出。桶数仅提供已实测的三个正值。
先看那个无法放回 int 的正数
Integer.MIN_VALUE 是 -2147483648,int 的最大值却是 2147483647。最小值对应的数学绝对值是 2147483648,已经超过 int 能保存的正数范围。Math.abs(int) 对这个参数返回原来的最小值,因此结果仍然为负。
在本次程序里,Math.abs(Integer.MIN_VALUE) % 3 得到 -2。改成 4 个桶时却得到 0,看上去正常;改成 10 个桶又得到 -8。只测一种恰好整除的桶数量,可能把问题藏起来。
这不是随机数或并发问题,也不要求真的找到某个特殊字符串。直接传入最小整数,就可以独立验证下标计算函数的边界。
Java 的余数,不保证落在非负区间
Java 整数除法朝零截断,% 与这种除法配套。用 -7 和 3 看:-7 / 3 是 -2,-7 % 3 是 -1,二者满足 -2 × 3 + (-1) = -7。负被除数的余数可能仍然为负。
分桶希望得到的通常是 0 到 bucketCount - 1。对于正桶数,Math.floorMod(hash, bucketCount) 满足这个范围;在 -7、3 的例子里,它给出 2,对应向下取整的商 -3。
本页比较同一个 hash、同一个桶数下的四种表达式:原始 %、先 abs 后 %、先 % 后 abs,以及 floorMod。不要只问有没有异常,还要观察不同规则把同一个输入分到了哪里。
两个都合法的下标,也可能指向不同桶
Math.abs(hash % buckets) 在正 int 桶数条件下可以得到范围内结果,因为余数的绝对值小于桶数,已经避开 int 最小值的绝对值溢出。但它与 floorMod 不是同一个映射规则。
例如 -7、3:先取余再取绝对值得到 1,floorMod 得到 2。两个数字都是合法下标,指向的桶却不同。因此不能把“都不会越界”说成“替换前后业务行为不变”。
如果数据已经按旧算法写入分区或缓存槽位,更改规则可能让查询去另一个位置寻找同一个键。上线前需要考虑已有数据的定位与迁移;本文只演示本地整数映射,没有执行真实数据库分片或迁移。
需要绝对值本身,与需要下标是两件事
如果业务真正需要这个 int 的数学绝对值,可以先拓宽到 long,再计算 Math.abs((long) hash)。本例得到 2147483648。顺序很重要:先用 int 做完 abs,再转换为 long,已经来不及修复那次溢出。
如果业务要求结果必须能装进 int,并希望溢出时明确失败,Java 17 中可以使用 Math.absExact。它对最小 int 抛出 ArithmeticException。本例捕获异常只为展示结果,不把异常吞掉当成成功值。
如果目标只是正桶数下的非负下标,则直接表达这个目的更清楚。先确保桶数大于 0,再使用约定的映射函数;桶数为 0 无法分桶,负桶数也不满足这里讨论的索引条件。
保存完整程序,再核对输出
程序打印一行最小整数的边界观察,再打印 6 个哈希值 × 3 个正桶数,共 18 组下标对照。内部断言检查 floorMod 和先取余再 abs 的范围,同时保留“两个合法映射不同”的反例。
2026-10-26,分别在 Microsoft OpenJDK 17.0.20.1 与 21.0.12 上使用 --release 17 编译并运行,两个版本输出一致。互动区回放这些固定记录;浏览器没有启动 JVM。程序没有联网、连接数据库或依赖第三方库。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
public class Main {
static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
public static void main(String[] args) {
int minimum = Integer.MIN_VALUE;
String exactFailure = "none";
try {
Math.absExact(minimum);
} catch (ArithmeticException error) {
exactFailure = error.getClass().getSimpleName();
}
check(Math.abs(minimum) == minimum, "int absolute value edge changed");
check(Math.abs((long) minimum) == 2147483648L, "widen before abs");
check(exactFailure.equals("ArithmeticException"), "absExact must reject overflow");
System.out.printf("{\"type\":\"edge\",\"value\":%d,\"absolute\":%d,"
+ "\"absoluteLong\":%d,\"absExactException\":\"%s\"}%n",
minimum, Math.abs(minimum), Math.abs((long) minimum), exactFailure);
int[] hashes = {minimum, -7, -1, 0, 1, 7};
int[] bucketCounts = {3, 4, 10};
for (int hash : hashes) {
for (int buckets : bucketCounts) {
int remainder = hash % buckets;
int absFirst = Math.abs(hash) % buckets;
int absAfter = Math.abs(hash % buckets);
int floor = Math.floorMod(hash, buckets);
check(floor >= 0 && floor < buckets, "floorMod index must be in range");
check(absAfter >= 0 && absAfter < buckets, "abs of bounded remainder");
System.out.printf("{\"type\":\"index\",\"hash\":%d,\"buckets\":%d,"
+ "\"remainder\":%d,\"absFirst\":%d,\"absAfter\":%d,"
+ "\"floorMod\":%d,\"absFirstInRange\":%s}%n",
hash, buckets, remainder, absFirst, absAfter, floor,
absFirst >= 0 && absFirst < buckets);
}
}
check(Math.abs(minimum) % 3 == -2 && Math.floorMod(minimum, 3) == 1,
"expected counterexample");
check(Math.abs(-7 % 3) == 1 && Math.floorMod(-7, 3) == 2,
"two valid mappings need not agree");
}
}
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-26 使用 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12 复跑这份源码并核对完整输出。固定输入的独立 Java 17 程序,双 JDK 实测;互动区只回放记录。
运行后对照结果
{"type":"edge","value":-2147483648,"absolute":-2147483648,"absoluteLong":2147483648,"absExactException":"ArithmeticException"}
{"type":"index","hash":-2147483648,"buckets":3,"remainder":-2,"absFirst":-2,"absAfter":2,"floorMod":1,"absFirstInRange":false}
{"type":"index","hash":-2147483648,"buckets":4,"remainder":0,"absFirst":0,"absAfter":0,"floorMod":0,"absFirstInRange":true}
{"type":"index","hash":-2147483648,"buckets":10,"remainder":-8,"absFirst":-8,"absAfter":8,"floorMod":2,"absFirstInRange":false}
{"type":"index","hash":-7,"buckets":3,"remainder":-1,"absFirst":1,"absAfter":1,"floorMod":2,"absFirstInRange":true}
{"type":"index","hash":-7,"buckets":4,"remainder":-3,"absFirst":3,"absAfter":3,"floorMod":1,"absFirstInRange":true}
{"type":"index","hash":-7,"buckets":10,"remainder":-7,"absFirst":7,"absAfter":7,"floorMod":3,"absFirstInRange":true}
{"type":"index","hash":-1,"buckets":3,"remainder":-1,"absFirst":1,"absAfter":1,"floorMod":2,"absFirstInRange":true}
{"type":"index","hash":-1,"buckets":4,"remainder":-1,"absFirst":1,"absAfter":1,"floorMod":3,"absFirstInRange":true}
{"type":"index","hash":-1,"buckets":10,"remainder":-1,"absFirst":1,"absAfter":1,"floorMod":9,"absFirstInRange":true}
{"type":"index","hash":0,"buckets":3,"remainder":0,"absFirst":0,"absAfter":0,"floorMod":0,"absFirstInRange":true}
{"type":"index","hash":0,"buckets":4,"remainder":0,"absFirst":0,"absAfter":0,"floorMod":0,"absFirstInRange":true}
{"type":"index","hash":0,"buckets":10,"remainder":0,"absFirst":0,"absAfter":0,"floorMod":0,"absFirstInRange":true}
{"type":"index","hash":1,"buckets":3,"remainder":1,"absFirst":1,"absAfter":1,"floorMod":1,"absFirstInRange":true}
{"type":"index","hash":1,"buckets":4,"remainder":1,"absFirst":1,"absAfter":1,"floorMod":1,"absFirstInRange":true}
{"type":"index","hash":1,"buckets":10,"remainder":1,"absFirst":1,"absAfter":1,"floorMod":1,"absFirstInRange":true}
{"type":"index","hash":7,"buckets":3,"remainder":1,"absFirst":1,"absAfter":1,"floorMod":1,"absFirstInRange":true}
{"type":"index","hash":7,"buckets":4,"remainder":3,"absFirst":3,"absAfter":3,"floorMod":3,"absFirstInRange":true}
{"type":"index","hash":7,"buckets":10,"remainder":7,"absFirst":7,"absAfter":7,"floorMod":7,"absFirstInRange":true}范围正确,不等于分布均匀或分片完整
floorMod 解决的是本页指定条件下的索引范围。它不会自动让差的哈希均匀分布,也不会解决扩容后大量键迁移、热点键、跨语言算法一致性或分布式路由问题。
互动区展示的是固定 18 组 Java 输出。它不使用 JavaScript 数字运算冒充 Java int 的溢出规则,也没有启动后台服务。若你换成 long、自定义哈希或更大的桶数,需要按新的类型与约定补充验证。
记住这些要点
Math.abs(Integer.MIN_VALUE)仍然为负,因为对应正数超出int范围。- 对于正桶数,
floorMod给出从 0 到桶数减一的下标。 - 先取余再 abs 与
floorMod可能都合法,却映射到不同桶。 - 需要数学绝对值时先拓宽类型,或用
absExact明确报告溢出。
别把例子用过头
- 只测试普通正负数,漏掉最小整数。
- 只测 4 个桶,看到最小整数恰好余 0 就认为算法普遍安全。
- 把“不会越界”误认为“与旧映射完全相同”。
- 先进行
int的 abs,再转long,并期待恢复丢失的范围。 - 把索引范围正确当作哈希分布、扩容或分片迁移已经验证。
自己试一小步
现有系统使用 Math.abs(hash) % buckets,准备修复时需要记录哪些兼容性信息?
- 补充
Integer.MAX_VALUE、Integer.MIN_VALUE+ 1 和桶数 1,先预测再运行。 - 将旧规则与候选规则的结果逐项对照,单独记录旧规则越界和两种合法结果不一致。
- 写明桶数为 0 或负数的处理方式,并说明已有数据的定位是否依赖原映射。
做到这些,就算完成
- 每个有效下标都满足 0 <= index && index < buckets。
- 不把“有界”与“相同”混为一个断言。
- 把本地算术验证与数据库分片迁移验证分开,未执行的系统行为不作保证。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java 17 Math:abs、absExact 与 floorMod (新标签页)↗
abs 的最小整数例外、
absExact的溢出异常和floorMod的范围及符号规则。 - Java 语言规范 17:整数除法与余数 (新标签页)↗
整数除法向零截断;余数满足除法恒等式,负被除数可能产生负余数。