1. 概述

有时我们需要忽略数值的具体类型或类来比较它们。当数据格式不统一,且数值可能在不同场景下使用时,这种比较方式特别有用。

本文将探讨如何比较基本类型和不同类的数值(如IntegerLongFloat等),以及如何比较浮点数与整数。

2. 比较不同的类

2.1. 比较整数基本类型

Java 提供了多种表示整数的基本类型。为简化说明,我们主要讨论 intlongdouble 使用基本类型时,可以直接比较数值是否相等:

@ValueSource(strings = {"1", "2", "3", "4", "5"})
@ParameterizedTest
void givenSameNumbersButDifferentPrimitives_WhenCheckEquality_ThenTheyEqual(String number) {
    int integerNumber = Integer.parseInt(number);
    long longNumber = Long.parseLong(number);
    assertEquals(longNumber, integerNumber);
}

⚠️ 但这种方法在溢出时不可靠。以下测试会正确识别数值不相等:

@ValueSource(strings = {"1", "2", "3", "4", "5"})
@ParameterizedTest
void givenSameNumbersButDifferentPrimitivesWithIntegerOverflow_WhenCheckEquality_ThenTheyNotEqual(String number) {
    int integerNumber = Integer.MAX_VALUE + Integer.parseInt(number);
    long longNumber = Integer.MAX_VALUE + Long.parseLong(number);
    assertNotEquals(longNumber, integerNumber);
}

但如果两个值同时溢出,可能导致错误结果。 虽然不容易踩坑,但以下场景仍需警惕:

@Test
void givenSameNumbersButDifferentPrimitivesWithLongOverflow_WhenCheckEquality_ThenTheyEqual() {
    long longValue = BigInteger.valueOf(Long.MAX_VALUE)
      .add(BigInteger.ONE)
      .multiply(BigInteger.TWO).longValue();
    int integerValue = BigInteger.valueOf(Long.MAX_VALUE)
      .add(BigInteger.ONE).intValue();
    assertThat(longValue).isEqualTo(integerValue);
}

此测试会认为两数相等,尽管其中一个实际是另一个的两倍。 这种方法只适用于小数值且无溢出风险的情况。

2.2. 比较整数和浮点基本类型

比较整数与浮点数时情况类似:

@ValueSource(strings = {"1", "2", "3", "4", "5"})
@ParameterizedTest
void givenSameNumbersButDifferentPrimitivesTypes_WhenCheckEquality_ThenTheyEqual(String number) {
    int integerNumber = Integer.parseInt(number);
    double doubleNumber = Double.parseDouble(number);
    assertEquals(doubleNumber, integerNumber);
}

这是因为 int 会被自动提升为 doublefloat 但只要数值存在微小差异,比较结果就符合预期:

@ValueSource(strings = {"1", "2", "3", "4", "5"})
@ParameterizedTest
void givenDifferentNumbersButDifferentPrimitivesTypes_WhenCheckEquality_ThenTheyNotEqual(String number) {
    int integerNumber = Integer.parseInt(number);
    double doubleNumber = Double.parseDouble(number) + 0.0000000000001;
    assertNotEquals(doubleNumber, integerNumber);
}

⚠️ 但精度和溢出问题依然存在。 即使比较同类型数值,结果也可能不可靠:

@Test
void givenSameNumbersButDifferentPrimitivesWithDoubleOverflow_WhenCheckEquality_ThenTheyEqual() {
    double firstDoubleValue = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.valueOf(42)).doubleValue();
    double secondDoubleValue = BigDecimal.valueOf(Double.MAX_VALUE).doubleValue();
    assertEquals(firstDoubleValue, secondDoubleValue);
}

以下场景更直观:用浮点数表示百分比(1=100%)与整数表示百分比(30)的比较:

@Test
void givenSameNumbersWithDoubleRoundingErrors_WhenCheckEquality_ThenTheyNotEqual() {
    double doubleValue = 0.3 / 0.1; // 结果为 2.9999999999999996
    int integerValue = 30 / 10;      // 结果为 3
    assertNotEquals(doubleValue, integerValue);
}

因此,基本类型比较不可靠,尤其涉及浮点运算时。

3. 比较包装类

使用包装类时,结果与基本类型不同:

@ValueSource(strings = {"1", "2", "3", "4", "5"})
@ParameterizedTest
void givenSameNumbersButWrapperTypes_WhenCheckEquality_ThenTheyNotEqual(String number) {
    Float floatNumber = Float.valueOf(number);
    Integer integerNumber = Integer.valueOf(number);
    assertNotEquals(floatNumber, integerNumber);
}

尽管 FloatInteger 数值相同,但比较结果为不相等。 即使比较 IntegerLong 也是如此:

@ValueSource(strings = {"1", "2", "3", "4", "5"})
@ParameterizedTest
void givenSameNumbersButDifferentWrappers_WhenCheckEquality_ThenTheyNotEqual(String number) {
    Integer integerNumber = Integer.valueOf(number);
    Long longNumber = Long.valueOf(number);
    assertNotEquals(longNumber, integerNumber);
}

核心问题在于比较 Number 层次结构中的不同类。 大多数类的 equals() 方法会先检查类型是否相同,例如 Long 的实现:

public boolean equals(Object obj) {
    if (obj instanceof Long) {
        return value == ((Long)obj).longValue();
    }
    return false;
}

这种设计避免了传递性问题,但也导致不同表示形式的数值无法直接比较。

4. BigDecimal

BigDecimal 是解决精度问题的最佳选择。我们分两种情况测试:相同精度和不同精度:

static Stream<Arguments> numbersWithDifferentScaleProvider() {
    return Stream.of(
      Arguments.of("0", "0.0"), Arguments.of("1", "1.0"),
      Arguments.of("2", "2.0"), Arguments.of("3", "3.0"),
      Arguments.of("4", "4.0"), Arguments.of("5", "5.0"),
      Arguments.of("6", "6.0"), Arguments.of("7", "7.0")
    );
}
static Stream<Arguments> numbersWithSameScaleProvider() {
    return Stream.of(
      Arguments.of("0", "0"), Arguments.of("1", "1"),
      Arguments.of("2", "2"), Arguments.of("3", "3"),
      Arguments.of("4", "4"), Arguments.of("5", "5"),
      Arguments.of("6", "6"), Arguments.of("7", "7")
    );
}

相同精度的比较符合预期:

@MethodSource("numbersWithSameScaleProvider")
@ParameterizedTest
void givenBigDecimalsWithSameScale_WhenCheckEquality_ThenTheyEqual(String firstNumber, String secondNumber) {
    BigDecimal firstBigDecimal = new BigDecimal(firstNumber);
    BigDecimal secondBigDecimal = new BigDecimal(secondNumber);
    assertEquals(firstBigDecimal, secondBigDecimal);
}

但不同精度时结果不同:

@MethodSource("numbersWithDifferentScaleProvider")
@ParameterizedTest
void givenBigDecimalsWithDifferentScale_WhenCheckEquality_ThenTheyNotEqual(String firstNumber, String secondNumber) {
    BigDecimal firstBigDecimal = new BigDecimal(firstNumber);
    BigDecimal secondBigDecimal = new BigDecimal(secondNumber);
    assertNotEquals(firstBigDecimal, secondBigDecimal);
}

BigDecimal 认为 11.0 不相等! 因为 equals() 会比较精度(scale),即使只是末尾零不同。

解决方案:使用 compareTo() 方法忽略精度差异:

@MethodSource("numbersWithDifferentScaleProvider")
@ParameterizedTest
void givenBigDecimalsWithDifferentScale_WhenCompare_ThenTheyEqual(String firstNumber, String secondNumber) {
    BigDecimal firstBigDecimal = new BigDecimal(firstNumber);
    BigDecimal secondBigDecimal = new BigDecimal(secondNumber);
    assertEquals(0, firstBigDecimal.compareTo(secondBigDecimal));
}

BigDecimal 是最佳选择,但需注意 equals()compareTo() 的行为差异。

5. AssertJ

使用 AssertJ 可简化断言代码:

@MethodSource("numbersWithDifferentScaleProvider")
@ParameterizedTest
void givenBigDecimalsWithDifferentScale_WhenCompareWithAssertJ_ThenTheyEqual(String firstNumber, String secondNumber) {
    BigDecimal firstBigDecimal = new BigDecimal(firstNumber);
    BigDecimal secondBigDecimal = new BigDecimal(secondNumber);
    assertThat(firstBigDecimal).isEqualByComparingTo(secondBigDecimal);
}

此方法更简洁,也支持自定义比较器处理复杂逻辑。

6. 结论

最佳实践总结:

  1. 基本类型比较:仅适用于小数值且无溢出风险
  2. 包装类比较:不同类直接比较始终返回 false
  3. **BigDecimal**:
    • 使用 compareTo() 忽略精度差异
    • 避免踩坑 equals() 的精度检查
  4. AssertJ:推荐使用 isEqualByComparingTo() 简化代码

核心原则:忽略类型比较数值时,BigDecimal + compareTo() 是最可靠的方案。

所有示例代码可在 GitHub 获取。


原始标题:Compare the Numbers of Different Types | Baeldung