开始前,会这些就够了
- 理解
String、异常和BigDecimal的基本比较。 - 知道数字的文本表示可能带小数点或分组分隔符。
- 先读 异常与资源释放:失败也要有出口:先认识异常与返回结果的不同失败通道,再看无异常但未读完整的情况。
导入表里有一格写着 12.50abc。代码调用数字解析方法,得到了 12.50,也没有进入异常处理,于是误以为整格都是合法数字。真正发生的是:解析器读到了一个可用的数字前缀,后面的字符还留在那里。
本实验不访问表格文件或数据库,只把几种文本放在内存里。每次记录解析结果、停止位置和错误位置,再把“能读出数字”“整段都已处理”“符合金额规则”分成三个判断。
把例子带到自己的电脑上
下载完整练习包
需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。附带 ParseMatrixDemo.java,可重新生成24格预设记录。下方命令运行基础 Java 标准库示例;附加对照的命令见对应 README。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 9 个文件包里有什么
- WholeNumberParseDemo.java:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
- ParseMatrixDemo.java:24格实际Java解析记录生成器
- parser-observations.json:两版JDK输出一致的结果表
- parser-README.md:矩阵运行方法与文本规则边界
- supplement-verification.json:附加实现的独立复跑记录
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 WholeNumberParseDemo.java
java -cp . WholeNumberParseDemo解压到独立练习文件夹,在含 WholeNumberParseDemo.java 的目录打开终端运行。固定12条输入、US/Germany两种显式地区的24格实际Java输出,JDK17与21一致;无任意文本输入,页面只展示这些已测记录。完整数字、按表示的小数位和范围分别检查;不保证千位分组文本严格。 额外运行步骤见 parser-README.md。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
解析器究竟读到哪里?
下面是同一份 Java 程序在两个地区规则下的实际记录。先选择输入,猜一猜它会得到哪个数、停在何处,再切换地区比较。
␠ 用来标出一个真实空格;程序从位置 0 开始,没有先去掉空白。
返回结果:BigDecimal,值为 12.50。
读取位置:5 / 8;错误位置为 -1(没有记录解析错误)。
整段有限十进制数:未通过。要求返回 BigDecimal、读到末尾,且没有记录解析错误。
本例金额规则:尚未进入;先通过整段数字检查。 当前 scale = 2。
24 格记录已于 2026-10-18 在 Microsoft OpenJDK 17.0.20.1 与 21.0.12 上编译运行,结果一致。页面只切换实测预设,没有在浏览器中运行 Java。完整生成器和原始记录在本篇 ZIP 的 parser-README.md 中。
第一步:把停止位置打印出来
12.50abc 一共有八个字符。本次实际结果是数值 12.50,index 为 5,error 为 -1。index 表示下一个还没处理的位置;因此 abc 三个字符仍在输入里。
error 为 -1 只表示这个 ParsePosition 没有记录解析错误。因为前面的数字有效,读取一个前缀本身不必算错误。不能把没有错误位置等同于已经走到字符串末尾。
源码还调用了只传 String 的 parse 重载,并确认它也得到 12.50。这使问题更贴近常见业务代码:添加 try/catch 仍然不足以发现未消费的后缀。
第二步:明确从零开始,并要求走到结尾
entire 每次新建 ParsePosition(0),防止上次的游标或错误状态混入新输入。然后检查结果确实为 BigDecimal、index 等于原字符串长度、没有错误位置。
这三个条件有不同作用:结果类型拒绝非有限数字等不符合此方法承诺的值;游标拒绝只读取前缀;错误位置保留解析阶段的失败信息。程序没有先 trim,因为是否接受前后空白也需要明确约定。
setParseBigDecimal(true) 让普通十进制值以 BigDecimal 返回,但特殊的 NaN 仍可返回 Double。源码因此检查实际类型,而不是直接强制转换后再把 ClassCastException 当成正常校验。
第三步:同样的文本,地区规则可以改变数值
文本 1,234 在 US 规则下得到 1234,在 Germany 规则下得到 1.234。两次都读完了全部字符,所以完整消费不能消除地区含义的歧义。
导入约定应同时说明文本的地区规则,或者采用固定的机器交换格式。不能根据服务器默认地区猜测每一行输入的意思,也不能解析失败后不断换地区直到某一种成功。
实验只使用两个显式 locale 的数字格式。它没有断言所有国家、所有分组写法都被严格审查;完整消费也不等同于对千位分组位置作了严格语法校验。
第四步:完整数字还要经过业务判断
12.345 和 -1.00 都能完整解析成 BigDecimal。实验自己的金额规则仍拒绝它们:金额要求非负、最多两位小数,并且不超过 10000。业务规则在解析之后单独执行。 程序另检查上界本身 10000 被接受,而 10000.01 被拒绝。
这里用 scale 检查小数位,因此规则针对当前表示;如果想把 12.500 看作可以接受的两位金额,需要另行明确是否归一化或允许无损缩减。不要暗中四舍五入后说输入原本就合法。
实跑覆盖固定字符串、两个 locale、前缀与全量检查、NaN、空输入和三条演示金额规则。没有运行货币兑换、国际化 UI、并发共享格式器或真实账务系统。
运行单文件,按实际输出逐项核对
保存为 WholeNumberParseDemo.java,在源码目录执行下方命令;需要完整 JDK 17 或更新版本。无需 Maven、网络或第三方依赖。
2026 年 10 月 18 日在 Windows 上分别以 Microsoft OpenJDK 17.0.20.1 与 21.0.12、--release 17 编译运行,输出一致。每项断言失败都会终止程序;下方是本次真实输出。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.math.BigDecimal;
import java.text.*;
import java.util.Locale;
public class WholeNumberParseDemo {
static int checks;
static void check(boolean ok) { if (!ok) throw new AssertionError(); checks++; }
static DecimalFormat format(Locale locale) {
DecimalFormat f = (DecimalFormat) NumberFormat.getNumberInstance(locale);
f.setParseBigDecimal(true); return f;
}
static BigDecimal entire(String text, Locale locale) {
ParsePosition p = new ParsePosition(0);
Number n = format(locale).parse(text, p);
if (!(n instanceof BigDecimal value) || p.getIndex() != text.length() || p.getErrorIndex() >= 0)
throw new IllegalArgumentException("not a complete finite decimal");
return value;
}
static boolean amountRule(BigDecimal value) {
return value.signum() >= 0 && value.scale() <= 2 && value.compareTo(new BigDecimal("10000")) <= 0;
}
public static void main(String[] args) throws Exception {
String text="12.50abc"; ParsePosition p=new ParsePosition(0);
Number prefix=format(Locale.US).parse(text,p);
check(prefix.toString().equals("12.50")); check(p.getIndex()==5); check(p.getErrorIndex()==-1);
System.out.println("prefix="+prefix+"; index="+p.getIndex()+"/"+text.length()+"; error="+p.getErrorIndex());
check(format(Locale.US).parse(text).toString().equals("12.50"));
boolean rejected=false;
try { entire(text,Locale.US); } catch(IllegalArgumentException e) { rejected=true; }
check(rejected); System.out.println("whole_input=reject_suffix");
BigDecimal us=entire("1,234",Locale.US), de=entire("1,234",Locale.GERMANY);
check(us.compareTo(new BigDecimal("1234"))==0); check(de.compareTo(new BigDecimal("1.234"))==0);
System.out.println("same_text=1,234; US="+us.toPlainString()+"; DE="+de.toPlainString());
BigDecimal extra=entire("12.345",Locale.US), negative=entire("-1.00",Locale.US);
check(!amountRule(extra)); check(!amountRule(negative)); check(amountRule(entire("12.50",Locale.US)));
System.out.println("complete_12.345=parsed_business_rejected; complete_-1.00=parsed_business_rejected");
check(amountRule(entire("10000",Locale.US)));
check(!amountRule(entire("10000.01",Locale.US)));
System.out.println("amount_upper_bound: 10000=accepted; 10000.01=rejected");
boolean nanRejected=false,emptyRejected=false;
try { entire("NaN",Locale.US); } catch(IllegalArgumentException e) { nanRejected=true; }
try { entire("",Locale.US); } catch(IllegalArgumentException e) { emptyRejected=true; }
check(nanRejected); check(emptyRejected);
System.out.println("NaN=reject_non_BigDecimal; empty=reject");
System.out.println("PASS checks="+checks);
}
}
怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 WholeNumberParseDemo.java;编译成功后,再执行 java -cp . WholeNumberParseDemo。两条命令分别回车。2026-10-18 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。Windows 固定内存文本;US/Germany两种locale,前缀/全量、特殊值/空输入与自定义金额规则;JDK17/21各14项断言。未测全部locale、严格分组语法、并发及真实账务。
运行后对照结果
prefix=12.50; index=5/8; error=-1
whole_input=reject_suffix
same_text=1,234; US=1234; DE=1.234
complete_12.345=parsed_business_rejected; complete_-1.00=parsed_business_rejected
amount_upper_bound: 10000=accepted; 10000.01=rejected
NaN=reject_non_BigDecimal; empty=reject
PASS checks=14继续对照:同样读完,也可能不是你允许的金额写法
页面中的12条预设在US和Germany下各运行一次,共24格。完整生成器如下;它与实际JSON记录一并放入下载包。两版JDK的输出逐项相同。
US下的12,34会读成1234并通过本例规则,说明完整消费不等于严格验证分组。1E3读成1E+3,scale为−3,也会通过;-0.00的数值归为零,非负检查不能保留原来的负号。
因此先决定产品是否允许指数写法、尾随零、分组与负零,再决定检查原始文本还是解析后的数值。这里的金额规则只是可解释的小例子,不能替你定义所有文本要求。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.math.BigDecimal;
import java.text.*;
import java.util.Locale;
public class ParseMatrixDemo {
static String quote(String value) {
return "\""+value.replace("\\","\\\\").replace("\"","\\\"")
.replace("\r","\\r").replace("\n","\\n").replace("\t","\\t")+"\"";
}
static boolean amountRule(BigDecimal value) {
return value.signum()>=0 && value.scale()<=2 && value.compareTo(new BigDecimal("10000"))<=0;
}
public static void main(String[] args) {
String[] inputs={"12.50abc","1,234","1.234","NaN",""," 12.50","12.50 ","12,34","12.500","10000.01","-0.00","1E3"};
Locale[] locales={Locale.US,Locale.GERMANY};
System.out.println("[");
int row=0;
for(Locale locale:locales) {
for(String input:inputs) {
DecimalFormat formatter=(DecimalFormat)NumberFormat.getNumberInstance(locale);
formatter.setParseBigDecimal(true);
ParsePosition position=new ParsePosition(0);
Number number=formatter.parse(input,position);
boolean full=number instanceof BigDecimal && position.getIndex()==input.length() && position.getErrorIndex()<0;
boolean accepted=full && amountRule((BigDecimal)number);
String type=number==null ? "null" : quote(number.getClass().getSimpleName());
String value=number==null ? "null" : quote(number.toString());
String scale=number instanceof BigDecimal decimal ? Integer.toString(decimal.scale()) : "null";
if(row++>0) System.out.println(",");
System.out.print(" {\"input\":"+quote(input)+",\"locale\":"+quote(locale.toLanguageTag())
+",\"actualType\":"+type+",\"value\":"+value+",\"scale\":"+scale
+",\"index\":"+position.getIndex()+",\"length\":"+input.length()+",\"errorIndex\":"+position.getErrorIndex()
+",\"fullFiniteDecimal\":"+full+",\"businessAccepted\":"+accepted+"}");
}
}
System.out.println("\n]");
}
}
怎样运行:在源码所在目录先执行 javac -encoding UTF-8 --release 17 ParseMatrixDemo.java;成功后执行 java -cp . ParseMatrixDemo。2026-10-18 已在 JDK 17.0.20.1 与 21.0.12 独立编译运行,24格输出一致。
运行后对照结果
[
{"input":"12.50abc","locale":"en-US","actualType":"BigDecimal","value":"12.50","scale":2,"index":5,"length":8,"errorIndex":-1,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"1,234","locale":"en-US","actualType":"BigDecimal","value":"1234","scale":0,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true},
{"input":"1.234","locale":"en-US","actualType":"BigDecimal","value":"1.234","scale":3,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":false},
{"input":"NaN","locale":"en-US","actualType":"Double","value":"NaN","scale":null,"index":3,"length":3,"errorIndex":-1,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"","locale":"en-US","actualType":null,"value":null,"scale":null,"index":0,"length":0,"errorIndex":0,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":" 12.50","locale":"en-US","actualType":null,"value":null,"scale":null,"index":0,"length":6,"errorIndex":0,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"12.50 ","locale":"en-US","actualType":"BigDecimal","value":"12.50","scale":2,"index":5,"length":6,"errorIndex":-1,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"12,34","locale":"en-US","actualType":"BigDecimal","value":"1234","scale":0,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true},
{"input":"12.500","locale":"en-US","actualType":"BigDecimal","value":"12.500","scale":3,"index":6,"length":6,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":false},
{"input":"10000.01","locale":"en-US","actualType":"BigDecimal","value":"10000.01","scale":2,"index":8,"length":8,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":false},
{"input":"-0.00","locale":"en-US","actualType":"BigDecimal","value":"0.00","scale":2,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true},
{"input":"1E3","locale":"en-US","actualType":"BigDecimal","value":"1E+3","scale":-3,"index":3,"length":3,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true},
{"input":"12.50abc","locale":"de-DE","actualType":"BigDecimal","value":"1250","scale":0,"index":5,"length":8,"errorIndex":-1,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"1,234","locale":"de-DE","actualType":"BigDecimal","value":"1.234","scale":3,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":false},
{"input":"1.234","locale":"de-DE","actualType":"BigDecimal","value":"1234","scale":0,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true},
{"input":"NaN","locale":"de-DE","actualType":"Double","value":"NaN","scale":null,"index":3,"length":3,"errorIndex":-1,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"","locale":"de-DE","actualType":null,"value":null,"scale":null,"index":0,"length":0,"errorIndex":0,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":" 12.50","locale":"de-DE","actualType":null,"value":null,"scale":null,"index":0,"length":6,"errorIndex":0,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"12.50 ","locale":"de-DE","actualType":"BigDecimal","value":"1250","scale":0,"index":5,"length":6,"errorIndex":-1,"fullFiniteDecimal":false,"businessAccepted":false},
{"input":"12,34","locale":"de-DE","actualType":"BigDecimal","value":"12.34","scale":2,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true},
{"input":"12.500","locale":"de-DE","actualType":"BigDecimal","value":"12500","scale":0,"index":6,"length":6,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":false},
{"input":"10000.01","locale":"de-DE","actualType":"BigDecimal","value":"1000001","scale":0,"index":8,"length":8,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":false},
{"input":"-0.00","locale":"de-DE","actualType":"BigDecimal","value":"0","scale":0,"index":5,"length":5,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true},
{"input":"1E3","locale":"de-DE","actualType":"BigDecimal","value":"1E+3","scale":-3,"index":3,"length":3,"errorIndex":-1,"fullFiniteDecimal":true,"businessAccepted":true}
]记住这三点
- 数字解析成功可以仅代表一个前缀成功。
- 完整消费、locale 与业务约束要分别确定。
- 请求
BigDecimal后仍应核对实际返回类型。
别把例子用过头
- 只依赖
parse(String)抛不抛异常来判定整段输入。 - 使用服务器默认地区解释来源不明的数字。
- 把全量消费误当成严格的分组语法验证或金额校验。
自己试一小步
给批量导入设计明确的空白、负零和尾随零规则,你会接受哪些输入?
- 先书写输入约定,明确 locale、空白、位数、范围以及是否允许归一化。
- 加入至少八组输入,分别保存原文、解析结果、停止位置和业务结果。
- 对每组先预测,再写断言;不要用捕获所有异常掩盖程序错误。
- 提交输入契约、程序、实际输出和运行版本。
做到这些,就算完成
- 前缀成功与完整输入成功分别记录。
- 两位小数规则是否针对表示或数值已说清。
- 没有把
DecimalFormat视作所有金融或交换格式的通用校验器。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- DecimalFormat.parse:消费范围与返回类型 (新标签页)↗
支持前缀解析及
BigDecimal/特殊值边界。 - ParsePosition:已读位置与错误位置 (新标签页)↗
支持独立记录 index 与
errorIndex。 - NumberFormat:数字格式与解析入口 (新标签页)↗
支持显式选择 locale 和解析方法的用途。