1. 项目缘起与整体设计思路中文数字和阿拉伯数字之间的转换看起来是个小需求但真正动手写过的人都知道这东西的坑远比想象中多。我最早接触这个需求是在一个票据打印模块里用户输入“叁万伍仟元整”系统要自动转成35000去做金额校验反过来生成合同文本时又要把“12800”转成“壹万贰仟捌佰”这种大写形式。一开始觉得随便写个switch-case就能搞定结果越写越发现边界情况层出不穷——零的处理、万和亿的进位、连续零的合并、纯小数、负数、甚至“两”和“二”的混用每一个都能让你调试到怀疑人生。这个项目的核心目标很明确提供一套健壮、可复用的Java工具类实现中文数字含小写“一二三”和大写“壹贰叁”与阿拉伯数字之间的双向转换。它解决的核心问题是在金融、合同、票据、报表等场景中数字需要在两种表示法之间精确、无歧义地转换且必须处理好中文数字特有的分段进位规则。适合有一定Java基础、需要在实际业务中处理数字格式化的开发者参考也适合想深入理解中文数字体系结构的学习者。我选择用纯Java实现不依赖任何第三方库原因有三一是这类转换逻辑本身不复杂引入依赖反而增加维护成本二是不同业务对“零”的处理、对大写小写的偏好不同自研更容易定制三是这种工具类往往要嵌入到各种老系统里零依赖意味着最高的兼容性。整体设计上我把它拆成两个核心方法chineseToArabic和arabicToChinese前者负责解析后者负责生成内部再细分处理整数部分和小数部分。注意中文数字转换最大的陷阱不是算法本身而是业务语义。比如“一万零一”到底是10001还是1001这取决于你对“零”的解析策略。我的方案是严格按照中文读数习惯来确保转换结果符合人的直觉。2. 中文数字的体系结构与核心难点拆解2.1 中文数字的分段进位逻辑中文数字和阿拉伯数字最大的区别在于阿拉伯数字是位置记数法每一位的值由它的位置决定而中文数字是分段记数法每四位为一个小节小节内部用“十百千”表示小节之间用“万亿”连接。理解这一点是写好转换器的前提。举个例子123456789。按中文习惯读作“一亿二千三百四十五万六千七百八十九”。拆开看就是三个小节1亿级、2345万级、6789个级。每个小节内部独立处理然后拼上对应的单位。这个结构决定了我的解析算法必须采用“分段处理再合并”的策略而不是逐字符扫描。具体来说我定义了四个级别的单位个级无单位、万级万、亿级亿、万亿级万亿。为什么没有“兆”因为在日常业务中超过万亿的数字极少出现而且“兆”在不同语境下含义不同有时指百万有时指万亿容易引起歧义。所以我设定了一个上限支持到万亿级别超出则抛出异常或返回错误提示。2.2 零的处理最容易被低估的难点零的处理是中文数字转换中最容易出错的地方。阿拉伯数字里零就是个占位符10001中间三个零清清楚楚。但中文里“一万零一”只用一个“零”就带过了。这就带来两个问题解析时一个“零”可能代表多个零生成时多个零要合并成一个“零”。我的处理策略是这样的解析时遇到“零”就标记当前小节需要补零但不立即确定补几个零而是等到整个小节解析完根据小节内数字的位置来推算。生成时则遍历每个小节如果小节内部有断档比如千位有值、百位为零、十位有值就在断档处插入一个“零”。这里有个细节小节末尾的零不读。比如1200读作“一千二百”不读“一千二百零零”。但1201要读作“一千二百零一”。这个规则看似简单实现时却需要仔细判断零后面是否还有非零数字。2.3 大写与小写的映射关系中文数字有两套体系小写一二三四五六七八九十和大写壹贰叁肆伍陆柒捌玖拾。金融场景必须用大写因为大写不易篡改。两套体系的核心映射关系是一一对应的但有几个特殊点需要注意。“两”和“二”的混用是个典型问题。口语中我们说“两百”但书面大写必须写“贰佰”。我的方案是解析时同时接受“两”和“二”都映射为2生成时统一输出“贰”或“二”不输出“两”因为“两”不是规范的大写形式。另外“十”的处理也有讲究。10可以读作“十”也可以读作“一十”。在中文习惯里单独一个“十”开头的数字通常省略“一”比如“十五”而不是“一十五”。但如果是“一百一十”那个“一十”就不能省略。我的生成逻辑是如果“十”位于最高位且前面没有其他数字就省略“一”否则保留。阿拉伯数字小写中文大写中文备注10十拾最高位省略“一”110一百一十壹佰壹拾非最高位保留“一”200二百贰佰不用“两百”1001一千零一壹仟零壹中间一个零10001一万零一壹万零壹跨小节一个零3. 阿拉伯数字转中文的完整实现3.1 整数部分的转换流程先讲阿拉伯转中文因为这是生成方向逻辑相对直观。我的实现思路是把数字按四位一组从低位到高位切分每组独立转换成中文再拼上对应的节权单位。假设输入是100200300。从低位切分0300、0020、1。注意这里要补足四位方便处理。然后从高位到低位处理第一组是1对应亿级转换结果是“一亿”第二组是0020对应万级转换结果是“二十万”第三组是0300对应个级转换结果是“零三百”。拼起来就是“一亿零二十万零三百”不对这里就暴露了一个问题小节之间的零怎么处理正确的读法是“一亿零二十万零三百”。但我的切分方式导致第二组是0020转换时前面的零被忽略了只输出“二十万”。那“一亿”和“二十万”之间的零从哪来答案是当高位小节的值不为零且低位小节的值不为零但低位小节的首位千位为零时需要补一个“零”。这个规则同样适用于万级和个级之间。所以完整的拼接逻辑是从高到低遍历每个小节如果当前小节有值先判断是否需要补零前一个小节有值且当前小节小于1000然后输出小节内容和节权单位。如果当前小节为零则标记需要补零但不输出。3.2 小数部分的处理小数部分相对简单但有个坑中文里没有“点”的规范大写形式。金融场景通常不写小数或者用“角分”表示。我的方案是提供两种模式一种用“点”连接如“三点一四”一种用“角分”表示如“三元一角四分”。默认用“点”模式因为通用性更强。小数部分的转换就是逐位映射3.14转成“三点一四”。注意零也要读出来3.05是“三点零五”不能省略中间的零。这一点和整数部分不同整数部分的零可以合并小数部分的零必须逐位读出。3.3 代码实现与关键注释public class ChineseNumberConverter { private static final String[] DIGITS {零, 一, 二, 三, 四, 五, 六, 七, 八, 九}; private static final String[] UPPER_DIGITS {零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖}; private static final String[] UNITS {, 十, 百, 千}; private static final String[] UPPER_UNITS {, 拾, 佰, 仟}; private static final String[] SECTIONS {, 万, 亿, 万亿}; private static final String[] UPPER_SECTIONS {, 万, 亿, 万亿}; public static String arabicToChinese(long number, boolean upperCase) { if (number 0) { return upperCase ? 零 : 零; } boolean negative number 0; if (negative) { number -number; } String[] digits upperCase ? UPPER_DIGITS : DIGITS; String[] units upperCase ? UPPER_UNITS : UNITS; String[] sections upperCase ? UPPER_SECTIONS : SECTIONS; StringBuilder result new StringBuilder(); int sectionIndex 0; boolean needZero false; while (number 0) { int section (int) (number % 10000); if (section 0) { needZero true; } else { if (needZero result.length() 0) { result.insert(0, digits[0]); } String sectionStr convertSection(section, digits, units); result.insert(0, sectionStr sections[sectionIndex]); needZero section 1000; } number / 10000; sectionIndex; } if (negative) { result.insert(0, 负); } return result.toString(); } private static String convertSection(int section, String[] digits, String[] units) { StringBuilder sb new StringBuilder(); int unitIndex 0; boolean zeroFlag false; while (section 0) { int digit section % 10; if (digit 0) { if (!zeroFlag sb.length() 0) { zeroFlag true; } } else { if (zeroFlag) { sb.insert(0, digits[0]); zeroFlag false; } sb.insert(0, digits[digit] units[unitIndex]); } section / 10; unitIndex; } return sb.toString(); } }这段代码的核心在convertSection方法里。它逐位处理一个小节用zeroFlag标记是否遇到了零。如果遇到零且后面还有非零数字就补一个“零”。注意sb.insert(0, ...)是从头部插入因为我是从低位往高位处理的。实操心得needZero这个变量的逻辑我调了三次才对。最初我是在小节为零时直接插入“零”结果“一亿零一万”变成了“一亿零一万零”末尾多了个零。后来改成用标记位等下一个非零小节出现时再补零才解决了问题。4. 中文数字转阿拉伯数字的解析策略4.1 解析器的整体架构反向转换比正向转换难得多因为中文数字的写法太灵活了。同一個数字可能有多种写法“一千二百”和“一千二百零”都有人写“十五”和“一十五”也都对。我的解析器采用“分段解析再合并”的策略和生成方向对称。具体流程是先把输入字符串按“亿”、“万”切分成若干小节每个小节内部再按“千”、“百”、“十”解析出数值最后乘以对应的节权系数相加。这个思路听起来简单但切分的时候要小心“万亿”是一个单位不能先按“万”切再按“亿”切否则会切错。我的做法是用正则表达式先匹配出所有节权单位的位置然后从右往左切分。为什么从右往左因为中文数字的节权是从小到大的“个”在右边“亿”在左边。从右往左处理可以保证每次切出来的都是完整的小节。4.2 小节内部的数值解析小节内部的解析是逐字符扫描。维护一个当前数字currentDigit和一个累加值sectionValue。遇到数字字符就更新currentDigit遇到单位十百千就用currentDigit * unitValue累加到sectionValue然后重置currentDigit。这里有个特殊情况“十”开头的小节。比如“十五”第一个字符就是单位“十”没有前面的数字。我的处理是如果遇到单位时currentDigit为零即没有前置数字就默认当前数字为1。这样“十五”就解析成1*10515。还有一个坑“零”在小节内部怎么处理我的策略是直接跳过因为零不贡献数值只影响读数。但要注意“零”不能出现在单位后面比如“十零”是非法写法遇到这种情况我选择抛出异常让调用方知道输入有问题。4.3 节权单位的处理与合并节权单位的处理是解析器最容易出错的地方。我定义了一个映射表万对应10000亿对应100000000万亿对应1000000000000。解析时每遇到一个节权单位就把当前累积的sectionValue乘以对应的系数加到总结果里然后重置sectionValue。但这里有个顺序问题如果输入是“一亿二千三百四十五万六千七百八十九”我从左往右扫描先遇到“亿”此时sectionValue是1乘以1亿得到1亿加到总结果。然后继续扫描“二千三百四十五万”累积到“万”时sectionValue是2345乘以1万得到2345万加到总结果。最后扫描“六千七百八十九”没有节权单位直接加到总结果。这样就能正确得到123456789。如果是从右往左切分呢那就先切出“六千七百八十九”再切出“二千三百四十五万”再切出“一亿”然后分别解析再合并。两种方式都可以我选择了从左往右扫描因为代码更简洁。输入解析过程结果十五单位“十”无前置数字默认11*10515一千零一一千1000零跳过一11001一万零一一万10000零跳过一110001一亿零一万一亿100000000零跳过一万100001000100004.4 代码实现与异常处理public static long chineseToArabic(String chinese) { if (chinese null || chinese.isEmpty()) { throw new IllegalArgumentException(输入不能为空); } // 统一处理大写和小写 chinese normalize(chinese); // 处理负数 boolean negative false; if (chinese.startsWith(负)) { negative true; chinese chinese.substring(1); } // 按节权切分 long result 0; long sectionValue 0; long currentDigit 0; for (int i 0; i chinese.length(); i) { char c chinese.charAt(i); int digit charToDigit(c); if (digit 0) { currentDigit digit; } else { long unit charToUnit(c); if (unit 0) { if (unit 10000) { // 节权单位 sectionValue (sectionValue currentDigit) * unit; result sectionValue; sectionValue 0; currentDigit 0; } else { // 小节内单位 if (currentDigit 0 unit 10) { currentDigit 1; } sectionValue currentDigit * unit; currentDigit 0; } } // 零直接跳过 } } result sectionValue currentDigit; return negative ? -result : result; }这段代码里charToDigit和charToUnit是两个辅助方法负责字符到数值的映射。normalize方法把大写字符统一转成小写这样后续逻辑只需要处理一套字符集。注意节权单位的处理逻辑我改了好几版。最初我是把sectionValue和currentDigit分开累加结果“一亿二千万”这种输入算错了。后来改成遇到节权单位时先把currentDigit并入sectionValue再乘以节权系数才正确。5. 常见问题与排查技巧实录5.1 零的解析异常问题现象输入“一千零零一”期望得到1001实际得到1001正确但输入“一千零一零”却得到了1010。排查思路零后面如果还有数字那个数字应该是个位。但“一千零一零”这种写法本身就不规范中文里不会这么读。我的处理是如果零后面还有数字且该数字后面没有单位就把它当作个位处理。但更稳妥的做法是严格校验输入格式遇到连续零或零结尾的情况直接报错。解决方案在解析前加一层格式校验用正则表达式检查输入是否符合中文数字的规范写法。不合法的输入直接抛出异常避免产生歧义结果。5.2 大写金额的“整”字处理问题现象金融场景中金额后面常跟“整”字如“叁万伍仟元整”。解析时“整”字会干扰。排查思路“整”不是数字字符也不是单位字符我的解析器遇到它会跳过。但“元”和“角分”也会出现需要一并处理。解决方案在normalize方法里增加一个过滤步骤把“整”、“元”、“角”、“分”、“圆”等业务相关字符剔除只保留纯数字和单位字符。这样解析器就不需要关心业务后缀了。5.3 超大数字的溢出问题问题现象输入“九万亿”时long类型能容纳但输入“九亿亿”就溢出了。排查思路long的最大值是约922亿亿理论上能容纳“九亿亿”。但我的节权单位只定义到“万亿”遇到“亿亿”这种写法无法解析。解决方案明确文档说明支持的范围是到“万亿”级别超出范围抛出ArithmeticException。如果业务确实需要更大数字建议改用BigInteger但那样代码复杂度会上升需要权衡。问题类型典型输入错误结果修正方案连续零一千零零一1001正确但依赖容错格式校验拒绝非法输入零结尾一千零1000正确允许但生成时不会输出末尾零业务后缀叁万伍仟元整解析失败过滤非数字字符超大数字九亿亿溢出限制范围或改用BigInteger两二混用两百解析失败normalize时把“两”转“二”5.4 性能优化建议这个工具类本身计算量不大单次转换在微秒级别。但如果要在循环里批量处理几十万条数据还是有几个优化点一是把字符到数字的映射从switch-case改成数组查表减少分支预测开销二是避免在循环里创建StringBuilder可以复用三是如果输入格式固定可以预编译正则表达式。我在一个报表导出场景里实测过优化前处理10万条数据需要约800毫秒优化后降到约200毫秒。提升主要来自数组查表和减少对象创建。实操心得不要过早优化。这个工具类在大多数场景下性能都够用除非你真的在循环里调用几十万次否则没必要折腾。我见过有人为了省几毫秒把代码写得晦涩难懂维护成本远大于性能收益。6. 扩展场景与实用建议6.1 金额大写的特殊规则金融场景的金额大写和普通中文数字有几个区别。第一必须用大写字符壹贰叁第二金额到“元”为止后面要加“整”字第三有角分时“元”后面不加“整”直接跟“角分”。比如12800.50要写成“壹万贰仟捌佰元伍角”。我的工具类提供了一个toMoneyFormat方法在标准转换基础上增加这些业务规则。实现时要注意角分部分如果为零只输出“元整”如果角为零分不为零输出“元零X分”如果角分都不为零输出“元X角Y分”。6.2 与前端交互的注意事项如果这个工具类要暴露给前端调用有几个点要提前约定好。一是输入格式前端传过来的中文数字可能包含空格、全角字符、甚至繁体字后端要做归一化处理。二是返回格式建议统一返回long或BigDecimal不要返回字符串避免前端再做一次解析。三是异常处理非法输入要返回明确的错误码和提示信息方便前端展示。我在一个项目里就遇到过前端传“壹万 贰仟”中间带空格的后端直接解析失败。后来在normalize里加了空格过滤才解决。所以提前约定输入规范很重要但后端做容错也必不可少。6.3 单元测试的覆盖要点这种工具类的测试用例要覆盖以下几类基本数字0到9、整十整百整千、带零的数字中间零、末尾零、连续零、跨小节数字万级、亿级、负数、小数、大写小写、非法输入。我大概写了80多个测试用例其中一半是边界情况。特别建议测试“一”和“十”的组合比如10、11、110、1010这几个数字的读写规则最容易出错。还有“零”的各种位置10001、10010、10100、11000每个都要验证。6.4 后续扩展方向这个工具类目前只支持整数和简单小数。如果要做更复杂的场景可以考虑几个方向一是支持分数和百分数比如“百分之五十”转成0.5二是支持序数词比如“第一”转成1三是支持中文数字的模糊匹配比如用户输入“一万多”时返回一个范围而不是精确值。不过这些扩展都要谨慎因为中文的模糊表达太多强行解析容易产生歧义。我的建议是核心的精确转换做扎实模糊场景交给业务层去处理工具类保持单一职责。最后分享一个小技巧如果你在调试时不确定转换结果对不对可以先把阿拉伯数字转成中文再把中文转回阿拉伯数字看看是否和原值一致。这个往返测试能发现大部分逻辑错误。我在开发过程中就是靠这个方法定位了好几个零处理的bug。
阅读完成 · 觉得有帮助?