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;
}

核心步骤:

  1. 通过计算小数点位置确定小数位数
  2. 分母 = 10^小数位数
  3. 分子 = 原始小数 × 分母
  4. 用斜杠拼接分子分母返回

测试用例验证:

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;
}

关键改进:

  1. 前两步与之前相同(计算分母和分子)
  2. 计算分子分母的 GCD
  3. 用 GCD 同时约分分子和分母
  4. 返回最简分数

测试用例对比:

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();
}

实现原理:

  1. 直接用小数构造 Fraction 对象
  2. 调用 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... 为例:

  1. 检测到循环节 "3"(长度 n=1)
  2. 分母 = 10^1 - 1 = 9
  3. 分子 = 3 + (1 × 9) = 12
  4. 原始分数 12/9 → GCD约分后得到 4/3

⚠️ 当前实现无法正确处理混合循环小数(如 0.1123123123 这种非循环+循环的组合)。

6. 总结

本文探讨了 Java 中小数转分数的四种方法:

  1. 乘以10的幂次方:简单直接但可能不约分
  2. GCD约分法:保证最简形式,性能适中
  3. Apache Commons Math:最省事的第三方方案
  4. 循环小数增强版:专门处理无限循环情况

对于大多数场景,GCD 约分法是最佳平衡点——既保证结果最简,又无需引入外部依赖。

示例源码已上传至 GitHub


原始标题:Convert Decimal to Fraction in Java