news 2026/10/13 5:20:30

Java数字转中文:工具类实现、金额大写与面试题解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java数字转中文:工具类实现、金额大写与面试题解析

把 1234 转成一千二百三十四,看起来就是个填空题。真要在 Java 里把阿拉伯数字转中文数字,你马上会撞上 1001 到底该读“一千零一”还是“一千零零一”、10000000 和 100000000 中间要补几个零、以及金额大写里 10 必须写成“壹拾”而不是“拾”这种细节。我最早写这个工具,是在一个报表导出模块里,甲方要求把所有统计数字转成中文放在 Word 正文里,当时我以为写个数组映射就够了,结果一测 100010000 这种数字直接傻眼。后来陆续在几个项目里又干了三次类似的事,才把这套逻辑彻底理顺。

这篇文章就把我自己沉淀下来的方案完整拆一遍:先分清你要的是哪种“中文数字”,再讲透中文数字的读数规则,然后给出可直接抄的整数转换工具类,最后扩展到财务场景的金额大写,附带我踩过的坑和排查思路。适合正准备写工具类的 Java 开发,也适合把这道题当面试题准备的朋友。

1. 先搞清楚你到底需要哪种“中文数字”

我第一次写这个功能时犯的错,就是上来就写代码。实际工作中“阿拉伯数字转中文数字”至少有三类完全不同的需求,规则、边界、验收标准都不一样。

1.1 场景一:报表、大屏展示的小写中文数字

这类场景最常见:导出的 Excel、Word 或 PDF 里,把“12345”显示成“一万二千三百四十五”;大屏上把某个统计值转成“三千二百一十万”。特点是只要读数自然、不带歧义,不需要考虑金额合规,也不用管“壹贰叁”那套大写。

这种场景对“一十”的省略比较宽容。100000 读“十万”或者“一十万”都能接受,1010 读“一千零一十”也是通的。但连续零的合并必须做好,10001 不能读成“一万零零一”,更不能读成“一零零零一”。大多数第三方工具、网上的 demo,目标都是这一类。

1.2 场景二:财务、合同里的金额大写

这一类是另一个物种。发票、合同、报销单里要写“壹佰贰拾叁元肆角伍分”,数字必须用大写,单位必须齐全,10 必须写作“壹拾”,不能省成“拾”。财务上对大小写金额一致性有严格要求,如果你只是拿普通中文数字的映射表去替换,很容易产出“拾万元整”这种不合规的结果。

更关键的是,金额有角分。0.50 元要写成“零元伍角整”?不对,严格说是“零元伍角”,如果没有角且有分,比如 0.05,要写“零元零伍分”。这些尾巴逻辑,报表场景根本用不到,但财务场景一条都不能少。

1.3 场景三:面试题里的隐藏考点

这是 Java 面试里出现频率相当高的“基础题”,热词里的 java面试题、java基础、java八股文都经常带它。面试官通常不会只问你“会不会转换”,而是会追问:

  • 100001 你是怎么处理连续零的?
  • 如果数字超出 long 范围怎么办?
  • 金额大写为什么不能直接用普通中文数字?
  • StringBuilder 拼接和 String 拼接在循环里的区别是什么?

所以这篇文章里的代码我会尽量写出工程可用级别,而不是面试背答案级别。面试官看到你能把边界情况和扩展方案讲清楚,印象分会比背一段网上的代码好很多。

2. 中文数字的规则,比你想的复杂一点

先别急着写代码,把规则梳理清楚,后面所有代码都是这堆规则的翻译。中文数字大约有三层规则:数字映射、零的读法、分段与单位。第三层里还藏着一个“一十”的省略问题。

2.1 数字映射只是第一步

数字映射非常简单:0 到 9 对应“零一二三四五六七八九”,单位对应“十百千万亿”。这是最底层的砖,但只做映射远远不够,因为中文数字不是简单的“每个阿拉伯数字替换成汉字”。比如 1001,如果你一位一位替换,会得到“一零零一”,而正确读法是“一千零一”。为什么?因为 1001 的 1 占了千位,中间的 0 只读一个,并且末尾的 0 不读。

所以转换的本质是对“数位”的翻译,不是对“数字字符”的翻译。每个数字不仅要转成汉字,还要带上它所在的位权单位。

2.2 零的处理:连续零合并、末尾零省略

零是整个转换里最容易翻车的地方。中文读数对零有三条核心规则:

  • 一组数字中间的零要读出来,比如 1001 是“一千零一”,这里的 0 要读。
  • 连续的多个零只读一个,比如 10001 是“一万零一”,不是“一万零零一”,中间四个 0 只合成一个“零”。
  • 末尾的零不读,比如 1000 是“一千”,最后三个 0 都不读。

这三条规则看起来简单,但和分段单位叠加之后,段与段之间的零就变得非常棘手。比如 100100000,按四位一组拆是 1 0010 0000,正确读法是“一亿零一十万”(这里按保留“一十”的读法),亿和万之间要补一个零;而 110000000 拆成 1 1000 0000,读“一亿一千万”,中间不需要零。同样是亿段后面接万段,一个有零一个没零,区别就在万段的值是否小于 1000,以及中间有没有整段为 0 的空档。

这块我当时调试了很久,最后得出的可靠方案是:分段之后用一个 pendingZero 标志记录是否遇到了全零的段,再配合“当前段数值是否小于 1000”来判断是否要补零。

2.3 四位分段与“万亿亿”扩展

中文读数是四位一断的,跟英文的三位一断完全不同。个十百千是一段,往上“万”一段,“亿”一段,再往上“万亿”“亿亿”……每四位数属于同一个段,段内用“千百十”做位权,段间用“万亿亿”做衔接。

很多人写转换工具只支持到亿,long 一超过 1 亿就乱了。而实际上 Java 的 long 最大值是 9223372036854775807,要完整支持它,至少得准备“亿亿、万亿、亿、万”四个段。我在自己的工具里就是按四个段做的分组:

9223372036854775807 分组后是: [922] [3372] [0368] [5477] [5807] 对应单位:亿亿 万亿 亿 万 (个位段无单位)

有人习惯把“亿亿”叫“兆”,但“兆”在中文语境下歧义太严重,计算机里提到兆都是 10^6,大数读法里又有人说是 10^12。为了避免争论,干脆用复合单位“亿亿”,语义最直观,代码注释里写清楚就行。

2.4 “一十”到底能不能省略

这是个小细节,但坑过不少人。比如 10 读“十”还是“一十”?100000 读“十万”还是“一十万”?口语里两种都有人讲,但更规范的书面读法是“十”和“十万”。而在金额大写里,必须写作“壹拾”“壹拾万”,省掉“壹”是不合规的。

所以工具类里最好把这个行为做成参数:普通中文数字默认省略最高段的“一十”,金额转换时关闭省略。比如 10 在普通场景输出“十”,在金额场景输出“一十”,再映射成“壹拾”。这个开关很小,但能同时满足两类需求。

3. 通用整数转换:一份可以抄作业的 Java 工具类

规则讲完,开始写代码。我直接给出一个完整可运行的版本,支持负数、支持 long 全范围、连续零正确合并、最高段十位自动省略“一”。

3.1 设计思路:从高位到低位分段

整体思路分三步:先把 long 按四位一组塞进数组,数组下标对应“亿亿、万亿、亿、万、个位”;然后从高位往低位遍历,跳过全 0 段,遇到非 0 段就把段内转换成“千百十”表达;最后把段单位和段之间的零补上。为什么要从高位往低位?因为零的补位方向是从高往低看的,正向遍历用 pendingZero 判断最直观。如果从低位往高位拼,补零逻辑要多绕一层,我第一版就是这么写的,回头改 bug 时后悔没一开始就用正向。

3.2 完整代码

import java.math.BigDecimal; import java.math.RoundingMode; public class NumberToChineseUtil { private static final String[] DIGITS = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九" }; private static final String[] UNITS = {"千", "百", "十", ""}; // 从高到低。Long.MAX_VALUE = 9223372036854775807,需要“亿亿”段 private static final String[] SECTION_UNITS = {"亿亿", "万亿", "亿", "万", ""}; private NumberToChineseUtil() { } /** * 阿拉伯数字转中文数字,普通展示场景。 * 10 -> 十,100000 -> 十万 */ public static String toChinese(long num) { return toChinese(num, true); } /** * 阿拉伯数字转中文数字。 * * @param num 目标数字 * @param omitHighestTen true 表示最高段十位省略“一”,比如十万、十二 * false 表示保留,比如一十万、一十二 */ private static String toChinese(long num, boolean omitHighestTen) { if (num == 0) { return "零"; } if (num < 0) { return "负" + toChinese(-num, omitHighestTen); } long[] sections = new long[SECTION_UNITS.length]; for (int i = sections.length - 1; i >= 0 && num > 0; i--) { sections[i] = num % 10000; num /= 10000; } StringBuilder result = new StringBuilder(); boolean isHighestSection = true; boolean pendingZero = false; for (int i = 0; i < sections.length; i++) { long section = sections[i]; if (section == 0) { pendingZero = true; continue; } // 段与段之间补零: // 1. 中间出现过整段为0,必须补 // 2. 当前段带前导0,比如 0100,也要补 if (result.length() > 0 && (pendingZero || section < 1000)) { result.append("零"); } String sectionStr = convertSection((int) section); // 只有最高有效段才允许省略“一十” if (isHighestSection && omitHighestTen && section >= 10 && section < 20 && sectionStr.startsWith("一十")) { sectionStr = sectionStr.substring(1); } result.append(sectionStr).append(SECTION_UNITS[i]); pendingZero = false; isHighestSection = false; } return result.toString(); } /** * 转换 0~9999 之间的段内数字。 * 1001 -> 一千零一,1010 -> 一千零一十,1100 -> 一千一百 */ private static String convertSection(int section) { int[] digits = new int[4]; for (int i = 3; i >= 0; i--) { digits[i] = section % 10; section /= 10; } StringBuilder sb = new StringBuilder(); boolean zeroFlag = false; for (int i = 0; i < 4; i++) { if (digits[i] == 0) { zeroFlag = true; } else { if (sb.length() > 0 && zeroFlag) { sb.append("零"); } sb.append(DIGITS[digits[i]]).append(UNITS[i]); zeroFlag = false; } } return sb.toString(); } }

这块代码不长,但基本把 long 全范围覆盖了。实测 9223372036854775807 会输出“九百二十二亿亿三千三百七十二万亿零三百六十八亿五千四百七十七万五千八百零七”,长是长了点,但分段和零位都正确。

3.3 关键代码逐段拆解

先看分段这一步。sections 数组长度是 5,从低位开始num % 10000依次填入,最后 sections[0] 就是最高段,sections[4] 是个位段。整个过程等价于把数字按四位一组拆开。

再看补零逻辑。pendingZero记录从上一个非零段到当前段之间,是否出现过整个段都是 0 的情况。如果出现过,说明中间有断层,必须在段前补一个零。同时,即便没有整段为 0,当前段自身的值小于 1000,也说明这段四位里有前导零,比如 0100 万,那么前面也要补零。两个条件用||连接,覆盖了段间零的两种来源。

convertSection里最核心的是 zeroFlag。它只在数字为 0 时置为 true,之后遇到非零数字时,如果之前有过零,就补一个“零”,然后立刻把 zeroFlag 重置。这样连续多个 0 就只会产生一个“零”,而且段末尾的 0 因为后面没有非零数字,根本不会触发。1001 走一遍:千位 1 直接拼接“一千”,百位 0 置 zeroFlag,十位 0 保持 zeroFlag,个位 1 发现 zeroFlag 为 true 就补零,得到“一千零一”。

3.4 测试用例:直接对着看

我建议你把下面这组用例直接扔进单元测试,这是我调这个工具时积累的最小回归集。

输入数值期望输出说明
0零边界
10十最高段“一十”省略
14十四同上
100一百普通百位
101一百零一段内零
1001一千零一连续零合并
1010一千零一十零不在末尾
10000一万整万
10001一万零一段间补零
10010一万零一十段间补零
100000十万万段十位省略
100100000一亿零一十万亿段后补零
110000000一亿一千万无零
100000001一亿零一中间整段为 0
-1234负一千二百三十四负数

数字 100100000 这段,按照“保留一十”的默认读法是“一亿零一十万”。如果你习惯读“一亿零十万”,把 omitHighestTen 的逻辑去掉一部分就能得到,但金融场景千万别这么干。

4. 扩展:金额大写转换,直接支撑合同场景

日常报表只要小写中文数字,但财务场景需要的是另一套输出。我把它做成一个独立方法,内部复用上面的转换逻辑,只是换了大写映射、关掉“一十”省略、再加上“元角分”和“整”。

4.1 大写转换和普通中文数字的差异

差异主要有三点:

  • 数字映射换成壹贰叁肆伍陆柒捌玖。
  • 单位换成拾佰仟万亿。
  • 十位前面必须有“壹”,不允许省略。
  • 整数部分结束要加“元”,没有角分要加“整”。

其中“壹拾”是硬性要求。合同上写“拾万元整”会被人挑毛病,正确写法是“壹拾万元整”。所以复用普通转换时必须传入 omitHighestTen=false,让 100000 先转成“一十万”,再映射成“壹拾万”。

4.2 代码实现

public class AmountToChineseUtil { private static final String[] UPPER_DIGITS = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; private AmountToChineseUtil() { } /** * 金额转中文大写,支持角分。 * 123.45 -> 壹佰贰拾叁元肆角伍分 * 100.00 -> 壹佰元整 * 0.05 -> 零元零伍分 */ public static String toChineseAmount(BigDecimal amount) { if (amount == null) { throw new IllegalArgumentException("金额不能为空"); } amount = amount.setScale(2, RoundingMode.HALF_UP); long yuanPart = amount.longValue(); int centPart = amount.subtract(BigDecimal.valueOf(yuanPart)) .movePointRight(2).intValue(); int jiao = centPart / 10; int fen = centPart % 10; StringBuilder sb = new StringBuilder(); if (yuanPart == 0) { sb.append("零元"); } else { // 第二个参数 false:金额大写不允许省略“壹拾” String intCn = NumberToChineseUtil.toChineseForAmount(yuanPart); sb.append(toUpper(intCn)).append("元"); } if (jiao == 0 && fen == 0) { sb.append("整"); } else { if (jiao != 0) { sb.append(UPPER_DIGITS[jiao]).append("角"); } else if (fen != 0) { // 角为 0 但分不为 0,需要补一个零 sb.append("零"); } if (fen != 0) { sb.append(UPPER_DIGITS[fen]).append("分"); } } return sb.toString(); } private static String toUpper(String chinese) { StringBuilder sb = new StringBuilder(chinese.length() * 2); for (int i = 0; i < chinese.length(); i++) { char c = chinese.charAt(i); switch (c) { case '零': sb.append("零"); break; case '一': sb.append("壹"); break; case '二': sb.append("贰"); break; case '三': sb.append("叁"); break; case '四': sb.append("肆"); break; case '五': sb.append("伍"); break; case '六': sb.append("陆"); break; case '七': sb.append("柒"); break; case '八': sb.append("捌"); break; case '九': sb.append("玖"); break; case '十': sb.append("拾"); break; case '百': sb.append("佰"); break; case '千': sb.append("仟"); break; case '万': sb.append("万"); break; case '亿': sb.append("亿"); break; case '负': sb.append("负"); break; default: sb.append(c); } } return sb.toString(); } }

我在 NumberToChineseUtil 里需要暴露一个给金额用的方法,因为普通toChinese(long)会省略“壹拾”,而金额不能省。可以直接把之前的私有方法改成包级可见,名字就叫toChineseForAmount:

static String toChineseForAmount(long num) { return toChinese(num, false); }

4.3 “整”和“零”的边界

这个方法是整个金额转换里最容易出错的部分。没有角分,结尾必须加“整”,这是票据习惯。有角分,结尾就不要“整”,而是按“X角Y分”或“零Y分”输出。角位为 0 但分位不为 0 时,要在分前面补一个“零”,比如 5.05 是“伍元零伍分”,不能写成“伍元伍分”。0.50 和 0.05 的区别也必须由这套逻辑体现出来,前者是“零元伍角”,后者是“零元零伍分”。

我之前踩过的一个坑是用 double 直接运算金额。0.1 加 0.2 在 double 里都不是干净值,金额字段用了 double 后,subtract出来经常是 0.29999999999999998,再 movePointRight 取整就会得到错误的角分。后来统一改成 BigDecimal 并在入口处 setScale(2),这个坑才算填平。

5. 常见问题与排查技巧实录

写这类工具,真正花时间的不是主流程,而是边界情况的排查。我把这几年攒下的问题整理成一份速查表,每条后面附结论和原因,方便你遇到同类问题直接对号入座。

5.1 连续多个零输出又长又乱

症状:10001 输出“一万零零一”或“一零零零一”。

原因:一位位硬拼,没有做零的合并。解决方案是段内用 zeroFlag、段间用 pendingZero 双重控制。记住一个核心判断:只有“当前已经有内容、并且刚才遇到过零”时才补一个“零”,补完立即重置标志。

5.2 十位到底该不该省略“一”

症状:100000 输出“一十万”,你觉得不对;或者合同里把“壹拾万”写成了“拾万”。

这不是 bug,是场景没分清。普通展示场景,100000 写成“一十万”也不算大错,但读起来啰嗦,所以默认省略。金额大写场景必须保留“壹拾”。解决方案就是给转换方法加布尔参数,而不是写两套逻辑。

5.3 金额用 double 导致角分错乱

症状:金额 9.99 转大写变成“玖元玖角玖分”偶尔变成“玖元玖角玖分”,但某些小数值会多出一分或少一分。

原因:double 的二进制浮点表示无法精确表达 0.1、0.2 这类十进制小数。金额相关字段一律用 BigDecimal,并且统一 setScale(2, RoundingMode.HALF_UP),在入口就规范化。

5.4 负数、零、long 溢出

负数我在工具里直接输出“负中文数字”,比如 -1234 转“负一千二百三十四”。这个行为可以根据业务决定,如果业务上不允许负数,应当在入口直接参数校验抛异常。

零单独返回“零”,不能走循环,否则 0 会返回空字符串。long 溢出属于输入校验问题,我用五位段数组天然覆盖了 Long.MAX_VALUE,所以不需要额外判断;如果你按网上常见的三段式(亿、万、个)实现,超过 99999999999999 就会出错,记得加范围校验。

5.5 StringBuilder 比 String 好用在哪

热词里单独出现了 java stringbuilder,这里多说一句。字符串循环拼接时 JVM 会反复创建新的 String 对象,代码写result = result + section虽然简洁,但数据量大时性能很差,可能触发频繁 GC。用 StringBuilder 的 append 是在同一个可变对象上操作,性能和内存都稳定得多。这个工具类虽然每次都 new 一个 StringBuilder,但因为转换频次低,完全没问题;倒是如果你在一个大循环里调用十万次转换,建议把方法设计成无状态静态方法,方便未来做缓存。

问题症状根因解决方案
连续零一万零零一没有合并零zeroFlag 状态机
十位省略十万 / 一十万混淆场景需求不同布尔开关控制
金额不准0.05 变成伍分double 精度BigDecimal
空结果0 返回空串忽略零边界单独返回“零”
溢出超亿范围乱掉段位不够扩展亿亿段

6. 最后分享一点实际经验

我在真实项目里用这套工具的感觉是:方向比技巧重要。第一次写的时候,我满脑子都是“怎么把数字拆开、怎么映射”,结果卡在零的处理上两天。后来把中文读数的规则一条条列清楚,再回到代码,逻辑一下就顺了。建议你也先写规则清单,再写代码,不要反过来。

另外,这个工具类应该当成工程组件维护,而不是一次性脚本。我通常会在项目的 common 模块里放一个 NumberToChineseUtil,再配一组单元测试,把 0、10、1010、100000001、Long.MAX_VALUE、负数、金额小数值这些边界全都锁死。后面任何人再遇到数字转中文的需求,直接复用就行,不用重新造轮子。

如果后续想继续扩展,可以考虑两个方向:一个是支持 BigDecimal 转中文数字的小数部分,比如 3.14 转“三点一四”,这个在报表里经常遇到;另一个是支持序数场景,比如第一、第二,原理和基数转换不同,但同样值得做。每次扩展都把这些边界用例带上,你会发现自己对“中文数字化”这件事的掌握会越来越系统。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:20:25

数据治理工具推荐避坑指南:这些功能缺失千万别买

企业客服、市场、售后部门每天都在和数据打交道&#xff1a;客户投诉记录、营销活动转化数据、售后工单信息、设备运行日志——这些数据分散在CRM、工单系统、电商后台、售后管理平台等不同系统中。当管理层要求“把上个季度客户投诉最多的产品品类和对应的售后处理时长拉出来”…

作者头像 李华
网站建设 2026/10/13 5:20:24

长沙做GEO优化怎么选?2026本地服务商盘点

在长沙挑选 GEO 优化服务商&#xff0c;企业需要重点考察服务商对 AI 大模型收录逻辑的理解程度、可核验的本地落地案例&#xff0c;同时确认交付物是否可以写入合同、是否支持灵活的合作模式。2026 年长沙市场上 GEO 相关服务供给持续增多&#xff0c;服务商能力参差不齐&…

作者头像 李华
网站建设 2026/10/13 5:20:05

LiteLLM实战:统一多模型接入,构建生产级模型网关的关键配置与排障

过去一段时间&#xff0c;我参与的一个内部AI项目里&#xff0c;几乎每天都得跟不同厂商的模型接口打交道。最初引入开源组件 LiteLLM&#xff0c;只是看中它支持的模型多&#xff0c;后来用顺了才发现&#xff0c;它的价值远不止“能调多个模型”&#xff0c;而是把接入层的复…

作者头像 李华
网站建设 2026/10/13 5:19:46

Oracle啤酒ERP实施:从WIP非标工单到PAC成本的落地路径

简介&#xff1a;这份PPT是Oracle顾问团队为珠江啤酒ERP项目制定的实施方案讲解材料&#xff0c;核心围绕大型制造企业Oracle ERP落地过程中的组织保障、方法论与计划分解展开。内容展示了从实施队伍组建、全球与本地顾问资源池配置&#xff0c;到AIM应用实施方法、PJM项目管理…

作者头像 李华
网站建设 2026/10/13 5:19:32

双框架实战:ThinkPHP+Laravel电影订票系统的并发选座架构

做任何同时涉及两套框架的项目&#xff0c;第一反应大概率是“没必要”。但如果你接手过那种既要完整后台排片管理、又要前台选座并发不低的电影订票系统&#xff0c;就会明白一个朴素道理&#xff1a;工具不是用来比高低的&#xff0c;而是用来把活高效干完的。今天要聊的这套…

作者头像 李华
网站建设 2026/10/13 5:18:56

Python客户管理系统毕业设计:从数据库建模到答辩实战

这两年带过不少做毕设的同学&#xff0c;也帮人改过不少“能跑但答辩一轮游”的项目。聊到客户管理系统&#xff0c;十个里得有六七个选Python去做&#xff0c;但真正能把系统讲清楚、扛住老师追问的&#xff0c;其实不到一半。我印象比较深的是一个学弟&#xff0c;系统功能全…

作者头像 李华