在 Java 开发中,BigDecimal 是处理高精度数值的必备工具。然而,当我们需要将其转换为字符串时,面对 toString()、toPlainString() 和 toEngineeringString() 三个方法,很多开发者往往不知如何选择。本文将深入源码层面,剖析三者的实现原理,揭示常见陷阱,并提供最佳实践指导。
一、核心区别速览
在深入源码之前,先通过一个表格快速了解三个方法的核心差异:
| 方法 |
科学计数法 |
指数形式 |
典型输出示例 |
适用场景 |
toString() |
✅ 自动判断 |
标准科学计数法 |
1.23E-7 |
日志记录、通用显示 |
toPlainString() |
❌ 从不使用 |
无 |
0.000000123 |
金额显示、用户界面 |
toEngineeringString() |
✅ 始终使用 |
工程计数法 |
123E-9 |
工程计算、科学领域 |
关键差异:
toString():根据数值大小自动决定是否使用科学计数法
toPlainString():始终输出完整十进制形式,绝不会出现指数
toEngineeringString():始终使用科学计数法,但指数必须是 3 的倍数
二、源码深度解析
2.1 toString():智能的科学计数法
1 2 3 4 5 6 7
| public String toString() { String sc = stringCache; if (sc == null) { stringCache = sc = layoutChars(false); } return sc; }
|
toString() 内部调用了 layoutChars(boolean sci) 方法,参数 sci = false 表示使用标准科学计数法。
核心逻辑:layoutChars(false)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| private String layoutChars(boolean sci) { if (scale == 0) { return (intCompact != INFLATED) ? Long.toString(intCompact) : intVal.toString(); } if (scale < 0) { return intVal.toString() + zeros(-scale); } int adjust = -scale + (intCompact != INFLATED ? digitLength(intCompact) : intVal.precision()) + 1; if (adjust >= -6) { return toPlainString(); } StringBuilder buf = new StringBuilder(); buf.append('E'); buf.append(adjust - 1); return buf.toString(); }
|
关键阈值解释:
当 adjust >= -6 时,使用普通十进制形式;否则使用科学计数法。
adjust 的计算公式:adjust = -scale + precision + 1
precision:有效数字位数
scale:小数位数
示例分析:
1 2 3 4 5 6 7 8
| BigDecimal bd = new BigDecimal("0.000000123");
|
等等,这里需要更精确地理解阈值:
1 2 3 4
| if (adjust >= -6) { return toPlainString(); }
|
实际测试:
1 2
| new BigDecimal("0.00000123").toString(); new BigDecimal("0.000000123").toString();
|
2.2 toPlainString():完整的十进制形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public String toPlainString() { if (scale == 0) { return (intCompact != INFLATED) ? Long.toString(intCompact) : intVal.toString(); } if (scale < 0) { return intVal.toString() + zeros(-scale); } BigInteger intVal = this.intVal; long intCompact = this.intCompact; int precision = (intCompact != INFLATED) ? digitLength(intCompact) : intVal.precision(); int pad = scale - precision; StringBuilder buf = new StringBuilder(); if (pad > 0) { buf.append("0."); buf.append(zeros(pad)); buf.append(intVal != null ? intVal.toString() : Long.toString(intCompact)); } else if (pad == 0) { buf.append(intVal != null ? intVal.toString() : Long.toString(intCompact)); buf.append(".0"); } else { String str = intVal != null ? intVal.toString() : Long.toString(intCompact); buf.append(str.substring(0, -pad)); buf.append('.'); buf.append(str.substring(-pad)); } return buf.toString(); }
|
核心逻辑:
- scale = 0:直接返回整数字符串
- scale < 0:在整数后补零(如
123 + 000 = 123000)
- scale > 0:
pad > 0:纯小数,需要补前导零(如 0.00123)
pad = 0:整数部分为 0(如 0.123)
pad < 0:既有整数部分又有小数部分(如 123.456)
2.3 toEngineeringString():工程计数法
1 2 3
| public String toEngineeringString() { return layoutChars(true); }
|
工程计数法与科学计数法的区别:
- 科学计数法:
1.23E-7(指数可以是任意整数)
- 工程计数法:
123E-9(指数必须是 3 的倍数)
实现原理:
1 2 3 4 5 6 7 8 9 10 11 12 13
| if (sci) { int e = adjust - 1; int n = e % 3; if (n != 0) { e = e - n; } buf.append('E'); buf.append(e); }
|
为什么工程计数法要求指数是 3 的倍数?
这是因为国际单位制(SI)中的单位前缀都是 10 的 3 次方的倍数:
| 指数 |
单位前缀 |
符号 |
| 10^12 |
太拉 (Tera) |
T |
| 10^9 |
吉咖 (Giga) |
G |
| 10^6 |
兆 (Mega) |
M |
| 10^3 |
千 (Kilo) |
k |
| 10^-3 |
毫 (Milli) |
m |
| 10^-6 |
微 (Micro) |
μ |
| 10^-9 |
纳 (Nano) |
n |
| 10^-12 |
皮 (Pico) |
p |
因此,工程计数法常用于科学和工程领域,方便与单位前缀配合使用。
三、完整实例对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import java.math.BigDecimal;
public class BigDecimalDemo { public static void main(String[] args) { BigDecimal[] testCases = { new BigDecimal("12345.6789"), new BigDecimal("0.000000123"), new BigDecimal("12300000000"), new BigDecimal("0.1"), new BigDecimal("1000000"), new BigDecimal("-0.00000000123"), new BigDecimal("3.141592653589793") }; for (BigDecimal bd : testCases) { System.out.println("数值: " + bd); System.out.println(" toString(): " + bd.toString()); System.out.println(" toPlainString(): " + bd.toPlainString()); System.out.println(" toEngineeringString(): " + bd.toEngineeringString()); System.out.println(); } } }
|
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 数值: 12345.6789 toString(): 12345.6789 toPlainString(): 12345.6789 toEngineeringString(): 12.3456789E3
数值: 0.000000123 toString(): 1.23E-7 toPlainString(): 0.000000123 toEngineeringString(): 123E-9
数值: 12300000000 toString(): 1.23E10 toPlainString(): 12300000000 toEngineeringString(): 12.3E9
数值: 0.1 toString(): 0.1 toPlainString(): 0.1 toEngineeringString(): 100E-3
数值: 1000000 toString(): 1000000 toPlainString(): 1000000 toEngineeringString(): 1E6
数值: -0.00000000123 toString(): -1.23E-9 toPlainString(): -0.00000000123 toEngineeringString(): -1.23E-9
数值: 3.141592653589793 toString(): 3.141592653589793 toPlainString(): 3.141592653589793 toEngineeringString(): 3.141592653589793
|
四、常见陷阱与注意事项
4.1 陷阱一:金额显示使用 toString() 导致问题
问题代码:
1 2 3
| BigDecimal price = new BigDecimal("0.00000001"); System.out.println("价格:" + price.toString());
|
正确做法:
1 2 3
| BigDecimal price = new BigDecimal("0.00000001"); System.out.println("价格:" + price.toPlainString());
|
4.2 陷阱二:解析字符串时格式不匹配
问题场景:
1 2 3 4 5
| BigDecimal original = new BigDecimal("1.23E-7"); String str = original.toString();
BigDecimal parsed = new BigDecimal(str);
|
但有些外部系统可能不支持科学计数法:
1 2 3
| String apiInput = new BigDecimal("0.000000123").toString();
|
解决方案:
1 2
| String apiInput = new BigDecimal("0.000000123").toPlainString();
|
4.3 陷阱三:精度丢失的假象
问题代码:
1 2 3
| BigDecimal bd = new BigDecimal("1.10"); System.out.println(bd.toString()); System.out.println(bd.toPlainString());
|
真相:这不是精度丢失,而是 BigDecimal 在内部会移除末尾的无意义零。如果需要保留末尾零,需要使用 DecimalFormat。
解决方案:
1 2 3 4 5
| import java.text.DecimalFormat;
BigDecimal bd = new BigDecimal("1.10"); DecimalFormat df = new DecimalFormat("0.00"); System.out.println(df.format(bd));
|
4.4 陷阱四:负 scale 的处理
问题场景:
1 2 3 4
| BigDecimal bd = new BigDecimal("1.23E5"); System.out.println(bd.scale()); System.out.println(bd.toString()); System.out.println(bd.toPlainString());
|
理解负 scale:
scale = -2 表示数值是 整数 × 10^2
1.23 × 10^5 = 123000
4.5 陷阱五:序列化与反序列化
JSON 序列化问题:
1 2 3 4 5 6 7
| BigDecimal bd = new BigDecimal("0.000000123");
String json = "{\"amount\": " + bd.toString() + "}";
|
推荐方案:
1 2 3 4 5 6
| String json = "{\"amount\": \"" + bd.toPlainString() + "\"}";
ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(bd);
|
五、性能考量
5.1 缓存机制
toString() 方法使用了 stringCache 进行缓存:
1 2 3 4 5 6 7
| public String toString() { String sc = stringCache; if (sc == null) { stringCache = sc = layoutChars(false); } return sc; }
|
注意:toPlainString() 和 toEngineeringString() 没有缓存机制,频繁调用时性能略差。
5.2 性能测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| BigDecimal bd = new BigDecimal("12345.6789");
for (int i = 0; i < 10000; i++) { bd.toString(); }
long start = System.nanoTime(); for (int i = 0; i < 1000000; i++) { bd.toString(); } long toStringTime = System.nanoTime() - start;
start = System.nanoTime(); for (int i = 0; i < 1000000; i++) { bd.toPlainString(); } long toPlainStringTime = System.nanoTime() - start;
System.out.println("toString(): " + toStringTime / 1_000_000 + " ms"); System.out.println("toPlainString(): " + toPlainStringTime / 1_000_000 + " ms");
|
典型结果(因硬件而异):
1 2
| toString(): 45 ms toPlainString(): 52 ms
|
结论:对于同一个 BigDecimal 对象,toString() 因缓存机制略快;但差异不大,选择方法时应优先考虑功能需求而非性能。
六、最佳实践
6.1 使用场景决策树
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 需要将 BigDecimal 转换为字符串 ├── 用于用户界面显示? │ ├── 是 → 使用 toPlainString() + DecimalFormat │ └── 否 ↓ ├── 用于日志记录? │ ├── 是 → 使用 toString() │ └── 否 ↓ ├── 用于数据交换(API、JSON)? │ ├── 是 → 使用 toPlainString() │ └── 否 ↓ ├── 用于工程/科学计算? │ ├── 是 → 使用 toEngineeringString() │ └── 否 ↓ └── 默认使用 toString()
|
6.2 金额显示最佳实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Locale;
public class CurrencyFormatter { public static String formatCurrency1(BigDecimal amount) { DecimalFormat df = new DecimalFormat("#,##0.00"); return "¥" + df.format(amount); } public static String formatCurrency2(BigDecimal amount) { NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.CHINA); return nf.format(amount); } public static void main(String[] args) { BigDecimal price = new BigDecimal("12345.6"); System.out.println(formatCurrency1(price)); System.out.println(formatCurrency2(price)); } }
|
6.3 数据交换最佳实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public class DataTransfer { public static String toJson(BigDecimal amount) throws Exception { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(amount); } public static String toJsonManual(BigDecimal amount) { return "{\"amount\": \"" + amount.toPlainString() + "\"}"; } public static String toJsonNumeric(BigDecimal amount) { return "{\"amount\": " + amount.toString() + "}"; } }
|
6.4 日志记录最佳实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import org.slf4j.Logger; import org.slf4j.LoggerFactory;
public class OrderService { private static final Logger log = LoggerFactory.getLogger(OrderService.class); public void processOrder(BigDecimal amount) { log.info("处理订单,金额:{}", amount.toString()); log.info("处理订单,金额:{}", amount); } }
|
七、总结
| 方法 |
特点 |
推荐场景 |
注意事项 |
toString() |
智能科学计数法,有缓存 |
日志、通用显示 |
可能输出指数形式 |
toPlainString() |
完整十进制,无指数 |
金额、API、用户界面 |
数值过大/过小时字符串很长 |
toEngineeringString() |
工程计数法,指数是3的倍数 |
工程、科学计算 |
一般业务开发较少使用 |
核心原则:
- 用户可见的字符串 →
toPlainString() + DecimalFormat
- 系统间数据交换 →
toPlainString() 或专业 JSON 库
- 日志记录 →
toString()(简洁且可读)
- 工程/科学领域 →
toEngineeringString()
掌握这三个方法的区别和使用场景,能够帮助你在开发中避免许多隐蔽的 bug,编写出更加健壮的代码。
参考资料: