Java List.contains 具有公差的 double 对象

Java List.contains object with double with tolerance

假设我有这个 class:

public class Student
{
  long studentId;
  String name;
  double gpa;

  // Assume constructor here...
}

我有一个类似的测试:

List<Student> students = getStudents();
Student expectedStudent = new Student(1234, "Peter Smith", 3.89)
Assert(students.contains(expectedStudent)

现在,如果 getStudents() 方法将 Peter 的 GPA 计算为 3.8899999999994,则此测试将失败,因为 3.8899999999994 != 3.89。

我知道我可以对单个 double/float 值进行容差断言,但是有没有一种简单的方法可以使 "contains" 工作,所以我没有单独比较 Student 的每个字段(我将编写许多类似的测试,我将测试的实际 class 将包含更多字段)。

我还需要避免修改有问题的 class(即学生)以添加自定义相等逻辑。

此外,在我的实际 class 中,将有其他双精度值的嵌套列表需要使用公差进行测试,如果我必须单独断言每个字段,这将使断言逻辑更加复杂.

理想情况下,我想说"Tell me if this list contains this student, and for any float/double fields, do the comparison with tolerance of .0001"

欢迎任何使这些断言简单的建议。

如果要使用containsequals,则需要在Studentequals方法中进行舍入处理。

不过,我建议使用合适的断言库,例如 AssertJ。

List.contains() 的行为是根据元素的 equals() 方法定义的。因此,如果您的 Student.equals() 方法比较 gpas 完全相等并且您无法更改它,那么 List.contains() 不是您的目的的可行方法。

并且可能 Student.equals() 不应该 使用公差比较,因为很难看出如何才能使 class 的 hashCode()方法与这样的equals()方法一致。

也许你能做的是写一个替代的,类似于 equals 的方法,比如“matches()”,它包含你的 fuzzy-comparison 逻辑。然后,您可以使用

之类的内容测试符合您标准的学生列表
Assert(students.stream().anyMatch(s -> expectedStudent.matches(s)));

其中有一个隐式迭代,但List.contains()也是如此。

我不是特别熟悉 GPA 的概念,但我想它从来没有使用过超过 2 位小数的精度。 3.8899999999994 GPA 根本没有多大意义,或者至少没有意义。

您实际上面临着人们在存储货币价值时经常面临的相同问题。 3.89 英镑有意义,但 3.88999999 英镑没有意义。已经有大量信息可以处理这个问题。例如,参见 this article

TL;DR:我会将数字存储为整数。所以 3.88 GPA 将存储为 388。当您需要打印该值时,只需除以 100.0。整数不存在与浮点值相同的精度问题,因此您的对象自然会更容易比较。

1) 不要覆盖 equals/hashCode 仅用于单元测试目的

这些方法具有语义,它们的语义没有考虑 class 的所有字段来使测试断言成为可能。

2) 依靠测试库来执行你的断言

Assert(students.contains(expectedStudent)

或者那个(张贴在 John Bollinger 的回答中):

Assert(students.stream().anyMatch(s -> expectedStudent.matches(s)));

在单元测试方面是很好的反模式。
当断言失败时,首先需要知道错误的原因以纠正测试。
依靠布尔值断言列表比较根本不允许这样做。
KISS(保持简单和愚蠢):使用测试 tools/features 断言,不要重新发明轮子,因为这些将在测试失败时提供所需的反馈。

3) 不要断言 doubleequals(expected, actual)

为了断言双精度值,单元测试库在断言中提供了第三个参数来指定允许的增量,例如:

public static void assertEquals(double expected, double actual, double delta) 

在 JUnit 5 中(JUnit 4 有类似的东西)。

或赞成BigDecimaldouble/float更适合这种比较。

但这并不能完全解决您的需求,因为您需要断言实际对象的多个字段。使用循环来做到这一点显然不是一个很好的解决方案。
Matcher 库提供了一种有意义且优雅的方式来解决这个问题。

4) 使用Matcher库对实际List对象的特定属性进行断言

使用 AssertJ:

//GIVEN
...

//WHEN
List<Student> students = getStudents();

//THEN
Assertions.assertThat(students)
           // 0.1 allowed delta for the double value
          .usingComparatorForType(new DoubleComparator(0.1), Double.class) 
          .extracting(Student::getId, Student::getName, Student::getGpa)
          .containsExactly(tuple(1234, "Peter Smith", 3.89),
                           tuple(...),
          );

一些解释(所有这些都是 AssertJ 的特性):

  • usingComparatorForType() 允许为给定类型的元素或其字段设置特定的比较器。

  • DoubleComparator 是一个 AssertJ 比较器,它提供了在双重比较中考虑 epsilon 的便利。

  • extracting 定义要从列表中包含的实例断言的值。

  • containsExactly() 断言提取的值与 Tuple 中定义的值完全相同(即不多不少,顺序完全相同)。