1. 引言
分数是另一种表示数字的方式,由分子和分母组成。例如分数 3/5 可以理解为"5份中的3份",其值等同于小数 0.6。本教程将探讨在 Java 中将小数转换为分数的几种方法。
2. 使用乘以10的幂次方
一种简单粗暴的转换方法是:将小数乘以10的幂次方,然后用结果直接作为分子分母构建分数。
以下是实现代码:
String convertDecimalToFractionUsingMultiplyingWithPowerOf10(double decimal) {
String decimalStr = String.valueOf(decimal);
int decimalPlaces = decimalStr.length() - decimalStr.indexOf('.') - 1;
long denominator = (long) Math.pow(10, decimalPlaces);
long numerator = (long) (decimal * denominator);
return numerator + "/" + denominator;
}
核心步骤:
- 通过计算小数点位置确定小数位数
- 分母 = 10^小数位数
- 分子 = 原始小数 × 分母
- 用斜杠拼接分子分母返回
测试用例验证:
assertEquals("5/10", convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.5));
assertEquals("1/10", convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.1));
assertEquals("6/10", convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.6));
assertEquals("85/100", convertDecimalToFractionUsingMultiplyingWithPowerOf10(0.85));
assertEquals("125/100", convertDecimalToFractionUsingMultiplyingWithPowerOf10(1.25));
assertEquals("1333333333/1000000000", convertDecimalToFractionUsingMultiplyingWithPowerOf10(1.333333333));
⚠️ 这种方法虽然简单,但有个明显缺陷:生成的分数可能不是最简形式。比如 0.5 会得到 5/10,实际应该简化为 1/2。
3. 使用最大公约数(GCD)
更健壮的方法是用最大公约数(GCD)对分数进行约分。GCD 是能同时整除分子和分母的最大整数。
首先实现 GCD 计算方法(欧几里得算法):
long gcd(long a, long b) {
if (b == 0) {
return a;
} else {
return gcd(b, a % b);
}
}
算法逻辑:
- 当 b=0 时,GCD 就是 a
- 否则递归计算 gcd(b, a % b)
然后实现带约分的转换方法:
String convertDecimalToFractionUsingGCD(double decimal) {
String decimalStr = String.valueOf(decimal);
int decimalPlaces = decimalStr.length() - decimalStr.indexOf('.') - 1;
long denominator = (long) Math.pow(10, decimalPlaces);
long numerator = (long) (decimal * denominator);
long gcd = gcd(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
return numerator + "/" + denominator;
}
关键改进:
- 前两步与之前相同(计算分母和分子)
- 计算分子分母的 GCD
- 用 GCD 同时约分分子和分母
- 返回最简分数
测试用例对比:
assertEquals("1/2", convertDecimalToFractionUsingGCD(0.5)); // 约分后
assertEquals("1/10", convertDecimalToFractionUsingGCD(0.1));
assertEquals("3/5", convertDecimalToFractionUsingGCD(0.6)); // 约分后
assertEquals("17/20", convertDecimalToFractionUsingGCD(0.85)); // 约分后
assertEquals("5/4", convertDecimalToFractionUsingGCD(1.25)); // 约分后
assertEquals("1333333333/1000000000", convertDecimalToFractionUsingGCD(1.333333333));
⚠️ 虽然这种方法能保证最简分数,但处理超大数时性能可能下降,因为递归计算取模运算对大数开销较大。
4. 使用 Apache Commons Math 库
最后介绍利用 Apache Commons Math 库的现成方案:
String convertDecimalToFractionUsingApacheCommonsMath(double decimal) {
Fraction fraction = new Fraction(decimal);
return fraction.toString();
}
实现原理:
- 直接用小数构造
Fraction
对象 - 调用
toString()
获取分数字符串
测试结果(注意格式稍有不同):
assertEquals("1 / 2", convertDecimalToFractionUsingApacheCommonsMath(0.5));
assertEquals("1 / 10", convertDecimalToFractionUsingApacheCommonsMath(0.1));
assertEquals("3 / 5", convertDecimalToFractionUsingApacheCommonsMath(0.6));
assertEquals("17 / 20", convertDecimalToFractionUsingApacheCommonsMath(0.85));
assertEquals("5 / 4", convertDecimalToFractionUsingApacheCommonsMath(1.25));
assertEquals("4 / 3", convertDecimalToFractionUsingApacheCommonsMath(1.333333333)); // 注意这里不同!
✅ 这是最省事的方案,但需要引入第三方依赖。
5. 处理循环小数
观察发现:前两种方法与 Commons Math 对 1.333333333 的处理结果不同。这是因为循环小数的处理机制差异。
循环小数是指小数部分有无限重复的数字序列,如 1.333... 中的 "3" 无限循环。
首先实现循环序列检测:
String extractRepeatingDecimal(String fractionalPart) {
int length = fractionalPart.length();
for (int i = 1; i <= length / 2; i++) {
String sub = fractionalPart.substring(0, i);
boolean repeating = true;
for (int j = i; j + i <= length; j += i) {
if (!fractionalPart.substring(j, j + i).equals(sub)) {
repeating = false;
break;
}
}
if (repeating) {
return sub;
}
}
return "";
}
然后改进 GCD 方法支持循环小数:
String convertDecimalToFractionUsingGCDRepeating(double decimal) {
String decimalStr = String.valueOf(decimal);
int indexOfDot = decimalStr.indexOf('.');
String afterDot = decimalStr.substring(indexOfDot + 1);
String repeatingNumber = extractRepeatingDecimal(afterDot);
if (repeatingNumber == "") {
return convertDecimalToFractionUsingGCD(decimal);
} else {
// 处理循环小数
int n = repeatingNumber.length();
int repeatingValue = Integer.parseInt(repeatingNumber);
int integerPart = Integer.parseInt(decimalStr.substring(0, indexOfDot));
int denominator = (int) Math.pow(10, n) - 1;
int numerator = repeatingValue + (integerPart * denominator);
int gcd = gcd(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
return numerator + "/" + denominator;
}
}
循环小数转换公式:
n
= 循环节长度分母 = 10^n - 1
分子 = 循环节值 + 整数部分 × 分母
测试用例验证:
assertEquals("1/2", convertDecimalToFractionUsingGCDRepeating(0.5));
assertEquals("17/20", convertDecimalToFractionUsingGCDRepeating(0.85));
assertEquals("5/4", convertDecimalToFractionUsingGCDRepeating(1.25));
assertEquals("4/3", convertDecimalToFractionUsingGCDRepeating(1.333333333)); // 正确处理循环
assertEquals("7/9", convertDecimalToFractionUsingGCDRepeating(0.777777));
以 1.333... 为例:
- 检测到循环节 "3"(长度 n=1)
- 分母 = 10^1 - 1 = 9
- 分子 = 3 + (1 × 9) = 12
- 原始分数 12/9 → GCD约分后得到 4/3
⚠️ 当前实现无法正确处理混合循环小数(如 0.1123123123 这种非循环+循环的组合)。
6. 总结
本文探讨了 Java 中小数转分数的四种方法:
- 乘以10的幂次方:简单直接但可能不约分
- GCD约分法:保证最简形式,性能适中
- Apache Commons Math:最省事的第三方方案
- 循环小数增强版:专门处理无限循环情况
对于大多数场景,GCD 约分法是最佳平衡点——既保证结果最简,又无需引入外部依赖。
示例源码已上传至 GitHub