开始前,会这些就够了
- 知道 JSON 的数字与字符串是不同类型。
- 会运行 Java 和 Node 命令;准备 JDK 17 与 Node 18+。
- 理解编号用于区分记录;本例无需数据库、浏览器、账号或网络。
- 先读浮点数的有限刻度,本篇观察大整数编号也可能合并。
虚构列表里有两个编号:9007199254740992 和 9007199254740993。Java 把它们存成 long,比较结果确实不同;发出的 JSON 原文也保留了最后一位。可客户端解析后,把它们放进集合只剩一个键。这时问题可能不在数据库,而在接收端把文字变成运行时数值的那一步。
本项目提供两个文件:Java 产生并核对编号,Node 使用原生 JSON.parse 和 JSON.stringify 完成往返。两个进程真的交换文本,再检查 Java 收回的值。所有输入均为固定虚构数据,临时文件会自动清理;它不是对真实接口或某个序列化框架的测试。
把例子带到自己的电脑上
下载完整练习包
需要 JDK 17 或更新版本与 Node.js 18 或更新版本;制作时实测 Node v24.18.0 + Microsoft OpenJDK 17.0.20.1。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 6 个文件包里有什么
- id_roundtrip.mjs:完整示例
- LongIdRoundTrip.java:配套 Java 程序
- README.md:环境、范围与独立练习
- observed.stdout.txt:制作时的观察记录
- verification.json:对应源码的验证范围
- SHA256SUMS.txt:文件内容摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
node --version
node id_roundtrip.mjs解压后在含示例文件的文件夹打开 PowerShell,按命令运行。先预测结果,再与当次输出对照。 真实 Java 与 Node 子进程之间的 JSON 往返;不代表 HTTP、数据库或真实业务校验。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
两个编号进来,还剩几个不同的键?
发送前的编号保留为十进制原文。选择 JSON 的写法,让当前浏览器真正解析一次,再查看收到的值与去重后的数量。
发送 2 个不同编号 → 收到 1 个不同的 Set 键
至少一个编号的整数内容已改变;相同的键无法再区分原来的两个对象。
发送的 JSON 原文
[9007199254740992,9007199254740993]编号 1
- 原始十进制编号
9007199254740992- 浏览器收到的值(number)
9007199254740992- 事后 BigInt(收到的 Number)
9007199254740992- 整数内容与原编号一致
- 是
- Number.isSafeInteger
- false
编号 2
- 原始十进制编号
9007199254740993- 浏览器收到的值(number)
9007199254740992- 事后 BigInt(收到的 Number)
9007199254740992- 整数内容与原编号一致
- 否
- Number.isSafeInteger
- false
把收到的值再次写成 JSON
[9007199254740992,9007199254740992]试着只切换“数字 / 字符串”,保留同一对编号。然后换成较小编号,检查问题是否仍会出现。
此面板真实执行浏览器的 JSON.parse、Set 和 BigInt,没有发送网络请求或操作业务对象。Number.isSafeInteger 为 false 并不表示每个值都已改变:2⁵³ 本身可精确表示,但相邻整数可能已合并。BigInt(Number) 只能转换现有数值,不能补回先前丢失的信息;默认 JSON.stringify 也不能直接序列化 BigInt,本面板回传的是收到的 Number 或 string。
先分开看:Java 数值、JSON 文字、客户端数值
Java long 是有范围的整数类型,本例两个值都在范围内,可以精确区分。JSON 则是一种交换文字格式:它可以写出很长的十进制数字,但不会要求每个接收程序都用同一种内部类型。RFC 8259 允许实现对数值范围与精度设限制,并提醒跨实现互通时关注这些边界。
Node 的普通 JSON.parse 会把 JSON 数字变成 JavaScript Number,把 JSON 字符串变成 String。因此响应正文里有正确的数字字符,不足以证明客户端变量仍然保留相同整数。应分别保存发送的原文、解析后的类型和值、重新发送的原文,才能确定变化发生在哪一层。
这里不是小数问题:相邻整数也可能合到一起
JavaScript Number 使用有限精度的二进制浮点表示。安全整数上界为 9007199254740991,也就是二的五十三次方减一。在安全范围内,整数不会与另一个整数映射到同一个 Number;超过这个范围,不能再依赖它逐个区分所有相邻整数。
本例第一项 9007199254740992 本身能精确表示,但下一项 9007199254740993 转成 Number 后也得到前一个值。所以两项的 Number.isInteger 都为真,Number.isSafeInteger 却都为假。“是整数”只说明当前数值没有小数部分;“不是安全整数”也不意味着这个具体输入必然已经变化,它表示相邻整数的区分保证不成立。
最后一位丢了,会影响记录身份
Node 将解析后的两项放进 Set,键数是一;将原始字符串形式的两项放进 Set,键数是二。对于编号,这不是显示时少一位的美观问题:如果应用用该值做缓存键、选中行标识或查找条件,就可能把不同记录当成同一项。实验只验证集合结果,没有真的查询或修改任何订单。
随后,Node 把数字数组重新转成 JSON,两个位置已经都是 9007199254740992。Java 收回后逐项与原编号比较,第一项 exact,第二项 changed。Java 的 long 在这一步没有溢出;它准确接收了客户端送回的另一个合法整数。只在后端检查“能不能转成 long”,发现不了这种身份变化。
事后换成 BigInt,不能猜回原来的末位
如果只拿到已经解析好的 Number,再执行 BigInt,转换的对象是当前保存的那个值。两项都得到 9007199254740992n。给变量换成能装大整数的类型,并不会同时找回它在上一层丢掉的信息;先 String(number) 再处理,也仍从已经变化的值出发。
对照路径从原始十进制字符串调用 BigInt,两项仍然不同。问题不在 BigInt 能否表示它们,而在转换发生之前保留了什么。本实验使用不带 reviver 的默认 JSON.parse;一些解析方案能够在转换时保留原始数字文字,需要另行验证。本文不声称所有 JSON 解析器都一定损失大整数,也不把事后修复与解析时保留混成一件事。
把编号当字符串传,是一份需要双方遵守的约定
本例的正确路径从 Java 输出开始就给编号加上 JSON 字符串引号。Node 解析后保留字符串,比较、作为键和返回时都不先转 Number;Java 收回后再按本例范围转回 long。最终两项都为 exact,键数仍是二。仅在页面显示之前补引号已经太晚,约定必须覆盖发送、接收与再次发送的整条路径。
字符串也要有规则。本例只接受正十进制编号,不允许前导零、正号或科学计数法,并限制到 Java long 的范围;代码对 01、+1、1e3 和超范围输入安排拒绝检查。这是教学接口自己选的政策,不是 JSON 标准禁止这些内容出现在字符串里。真实编号若允许前导零,强行转整数反而会改变它的身份。
如果前端需要计算,可以从经过校验的字符串得到 BigInt;传输时仍需选定表达形式。默认 JSON.stringify 直接遇到本例 BigInt 值会抛 TypeError。自行提供 replacer 或其他转换会改变行为,本例没有做这些定制;它明确使用字符串作为双方共同理解的传输类型。
把两个文件放在一起,只启动 Node 入口
下载并解压本篇练习包,保持 LongIdRoundTrip.java 与 id_roundtrip.mjs 在同一目录,运行下方命令。Node 会创建临时目录,复制 Java 源码,通过 Java 的单文件模式先生成 JSON,再把两种返回文本分别交给 Java 检查。每次子进程有等待上限,失败会明确报错。
如果系统里的 java 不是要使用的 JDK,可以把 java 可执行文件的完整路径作为 Node 命令的第一个参数。程序最终打印一份报告,包含 rawJsonFromJava、afterNativeJsonParse、两种返回 JSON、javaChecks 和清理状态。所有断言通过且临时目录删除后才打印 PASS;没有向当前练习目录写入生成数据。
java -version
node --version
node id_roundtrip.mjs
# 如果需要指定 JDK,可把 java 可执行文件的完整路径作为参数:
# node id_roundtrip.mjs "C:/path/to/jdk-17/bin/java.exe"怎样运行:将 LongIdRoundTrip.java 与 id_roundtrip.mjs 放在同一目录,启动 Node 入口即可。JDK 17 与 Node 18+;只用标准库,生成文件在临时目录自动清理。 2026-10-13 已用 Node v24.18.0 + Microsoft OpenJDK 17.0.20.1 执行这份示例并通过自带断言;下方为当次输出。真实 Java 与 Node 子进程之间的 JSON 往返;不代表 HTTP、数据库或真实业务校验。
运行后对照结果
{
"scope": "two fixture IDs; Java and Node subprocesses; no HTTP or database",
"nodeVersion": "v24.18.0",
"rawJsonFromJava": "{\"numbers\":[9007199254740992,9007199254740993],\"strings\":[\"9007199254740992\",\"9007199254740993\"]}",
"afterNativeJsonParse": {
"numbers": [
9007199254740992,
9007199254740992
],
"strings": [
"9007199254740992",
"9007199254740993"
],
"numberKeyCount": 1,
"stringKeyCount": 2,
"isInteger": [
true,
true
],
"isSafeInteger": [
false,
false
]
},
"bigIntFromParsedNumber": [
"9007199254740992",
"9007199254740992"
],
"bigIntFromOriginalString": [
"9007199254740992",
"9007199254740993"
],
"numericJsonReturnedToJava": "[9007199254740992,9007199254740992]",
"stringJsonReturnedToJava": "[\"9007199254740992\",\"9007199254740993\"]",
"javaChecks": {
"numericAtJava": {
"statuses": [
"exact",
"changed"
],
"distinct": false,
"receivedAsStrings": [
"9007199254740992",
"9007199254740992"
]
},
"stringsAtJava": {
"statuses": [
"exact",
"exact"
],
"distinct": true,
"receivedAsStrings": [
"9007199254740992",
"9007199254740993"
]
}
},
"defaultBigIntStringify": "TypeError; explicit transport representation is required",
"javaDecoderScope": "exact compact two-ID arrays from this fixture only; not a general JSON parser",
"temporaryDirectoryRemoved": true
}
PASS: Java long -> JSON -> Node -> JSON -> Java; exact string IDs preserved核对往返结果,也核对这个小程序没做什么
先确认 Java 原始输出两项不同,再看 Number 与 String 两组的键数,然后核对返回 Java 时的逐项比较。这个顺序能排除“只是控制台把长数字缩短显示”的猜测:数值路径发送回去的正文已经变化,接收端也读到了不同结果。字符串路径的原文与值则一直保留区别。
为避免引入第三方依赖,Java 回传检查器只识别本程序发出的紧凑双元素数组,再提取十进制编号;它不是通用 JSON 解析器,不支持任意空白、嵌套或转义。生产代码应使用受维护的解析库,把字段绑定为字符串后再校验。本例通过进程标准输入输出交换数据,没有测试 HTTP、数据库字段、Jackson 配置或任何浏览器页面。
最后按字段含义决定类型,别把全部数字改成文字
编号的主要工作是区分对象,本例选择字符串便于保持原样;数量、比例、测量值则有不同的计算需求。不能为了这一项修复,把所有 JSON 数字都改成字符串,随后又让各客户端各自猜怎样转换。若一个字段允许使用 Number,应写明范围并在接收边界验证;若它是文字身份,就避免不必要的数值转换。
上线前还要检查旧客户端、已有缓存、排序和接口文档是否仍按数字处理。字符串排序与数值排序并不自动相同;已经保存成错误值的数据也不会因为新响应加了引号便恢复。本文只证明这两项编号在所列运行时和路径上的行为,不能替代真实系统中所有读写入口的兼容检查。
记住这三点
- JSON 原文精确,不代表默认解析后的
Number仍能区分相邻大整数。 - 先失去信息、后转
BigInt,不能恢复原编号。 - 字符串编号需要端到端约定、格式与范围校验,以及旧客户端兼容检查。
别把例子用过头
- 把
Number.isInteger当成没有发生整数精度损失的证明。 - 先让 JSON 数字变成
Number,显示时才转字符串。 - 把教学用双元素数组识别器直接当成生产 JSON 解析器。
- 为了保住 ID,把所有数值字段改成文字却没有更新接口合同。
自己试一小步
如果旧客户端只接收数字编号,新客户端改用字符串编号,怎样设计并验证过渡合同?
- 自选安全整数边界内外的编号,并加入一项带前导零的文字编号;先写明各项的身份含义。
- 设计一份能够区分旧客户端与新客户端能力的输入输出规则,分别保存原文、运行时类型与返回结果。
- 自行实现一条拒绝不安全数字的入口,再验证字符串排序与数值排序是否符合你的页面需求。
做到这些,就算完成
- 提交自己的边界表与实际往返记录,没有把失败样本悄悄改成更小的数。
- 解释何时拒绝、何时保留文字身份,已损失的信息没有被宣称自动修复。
- 明确过渡需要哪些客户端与存储证据,不将两个虚构编号的成功推广为全系统已经兼容。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- RFC 8259:JSON 数字的互操作边界 (新标签页)↗
阅读第 6 节的数字语法、实现可设范围/精度限制,以及 binary64 整数互操作区间;支持 JSON 文字与接收端表示需要分别核对。
- ECMAScript:安全整数与最大安全整数 (新标签页)↗
阅读
Number.isInteger、Number.isSafeInteger与Number.MAX_SAFE_INTEGER;核对 2^53 和 2^53+1 映射到同一Number的明确例子。 - ECMAScript:JSON.parse 与 JSON.stringify (新标签页)↗
阅读
JSON.parse的类型映射,以及SerializeJSONProperty的Number、String、BigInt分支;仅说明无 reviver/replacer/toJSON定制的本例默认行为。 - ECMAScript:BigInt 与 NumberToBigInt (新标签页)↗
阅读
BigInt(value)、NumberToBigInt及BigInt.prototype.toString;支持转换使用当前Number的整数值,不能由该值恢复此前文字。 - Java 17:Long.parseLong (新标签页)↗
阅读十进制字符串到
long的解析与NumberFormatException条件。本例额外禁止前导零/正号的规范形式,是教学合同而非parseLong自带限制。