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"
欢迎任何使这些断言简单的建议。
如果要使用contains
或equals
,则需要在Student
的equals
方法中进行舍入处理。
不过,我建议使用合适的断言库,例如 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) 不要断言 double
与 equals(expected, actual)
。
为了断言双精度值,单元测试库在断言中提供了第三个参数来指定允许的增量,例如:
public static void assertEquals(double expected, double actual, double delta)
在 JUnit 5 中(JUnit 4 有类似的东西)。
或赞成BigDecimal
到double/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
中定义的值完全相同(即不多不少,顺序完全相同)。
假设我有这个 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"
欢迎任何使这些断言简单的建议。
如果要使用contains
或equals
,则需要在Student
的equals
方法中进行舍入处理。
不过,我建议使用合适的断言库,例如 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) 不要断言 double
与 equals(expected, actual)
。
为了断言双精度值,单元测试库在断言中提供了第三个参数来指定允许的增量,例如:
public static void assertEquals(double expected, double actual, double delta)
在 JUnit 5 中(JUnit 4 有类似的东西)。
或赞成BigDecimal
到double/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
中定义的值完全相同(即不多不少,顺序完全相同)。