【发布时间】:2015-12-16 06:12:08
【问题描述】:
我们正面临 HashMap 行为方式的奇怪问题。
当HashMap键实现Comparable接口但compareTo实现与equals不一致时HashMaps:
- 长得比它们应该长的大得多
- 它们包含多个相等元素的实例
- 附加到这些元素的值可能不同
- get(key) 结果取决于使用哪个键(即使根据 equals 方法,键相等)。
我创建了一个小测试来重现该问题(见下文)。
import java.util.HashMap;
import java.util.Map;
public class HashMapTest {
private static final char MIN_NAME = 'A';
private static final char MAX_NAME = 'K';
private static final int EXPECTED_NUMBER_OF_ELEMENTS = MAX_NAME - MIN_NAME + 1;
private HashMap<Person, Integer> personToAgeMap;
HashMapTest(){
personToAgeMap = new HashMap();
}
public static void main(String[] args){
HashMapTest objHashMap = new HashMapTest();
System.out.println("Initial Size of Map: " + objHashMap.getPersonToAgeMap().size());
objHashMap.whenOverridingEqualElements_thenSizeOfTheMapIsStable();
objHashMap.whenGettingElementUsingPersonOfAge1_thenOverridenValuesAreReturned();
objHashMap.whenGettingElementUsingPersonOfAge100_thenOverridenValuesAreReturned();
objHashMap.whenGettingElementUsingPersonOfAge50_thenOverridenValuesAreReturned();
objHashMap.whenGettingElementUsingPersonOfAgeMinus1_thenOverridenValuesAreReturned();
}
public HashMap<Person, Integer> getPersonToAgeMap(){
return personToAgeMap;
}
public void whenOverridingEqualElements_thenSizeOfTheMapIsStable() {
System.out.println("Adding elements with age 1..");
putAllPeopleWithAge(personToAgeMap, 1);
System.out.println(personToAgeMap);
System.out.println("Expected Number Of elements: " + EXPECTED_NUMBER_OF_ELEMENTS+ "\nActual Number of elements: "+personToAgeMap.size());
System.out.println();
System.out.println("Overwriting map, with value 100..");
putAllPeopleWithAge(personToAgeMap, 100);
System.out.println(personToAgeMap);
System.out.println("Expected Number Of elements: " + EXPECTED_NUMBER_OF_ELEMENTS+ "\nActual Number of elements: "+personToAgeMap.size());
System.out.println();
}
public void whenGettingElementUsingPersonOfAge1_thenOverridenValuesAreReturned() {
useAgeToCheckAllHashMapValuesAre(1, 100);
}
public void whenGettingElementUsingPersonOfAge100_thenOverridenValuesAreReturned() {
useAgeToCheckAllHashMapValuesAre(100, 100);
}
public void whenGettingElementUsingPersonOfAge50_thenOverridenValuesAreReturned() {
useAgeToCheckAllHashMapValuesAre(50, 100);
}
public void whenGettingElementUsingPersonOfAgeMinus1_thenOverridenValuesAreReturned() {
useAgeToCheckAllHashMapValuesAre(-10, 100);
}
private void useAgeToCheckAllHashMapValuesAre(int age, Integer expectedValue) {
System.out.println("Checking the values corresponding to age = " + age);
StringBuilder sb = new StringBuilder();
int count = countAllPeopleUsingAge(personToAgeMap, age);
System.out.println("Count of People with age " + age+" =" + count);
if (EXPECTED_NUMBER_OF_ELEMENTS != count) {
sb.append("Size of the map ").append(" is wrong: ")
.append("expected <").append(EXPECTED_NUMBER_OF_ELEMENTS).append("> actual <").append(count).append(">.\n");
}
for (char name = MIN_NAME; name <= MAX_NAME; name++) {
Person key = new Person(name, age);
Integer value = personToAgeMap.get(key);
if (!expectedValue.equals(value)) {
sb.append("Unexpected value for ").append(key).append(": ")
.append("expected <").append(expectedValue).append("> actual <").append(value).append(">.\n");
}
}
if (sb.length() > 0) {
System.out.println(sb.toString());
}
}
void putAllPeopleWithAge(Map<Person, Integer> map, int age) {
for (char name = MIN_NAME; name <= MAX_NAME; name++) {
map.put(new Person(name, age), age);
}
}
int countAllPeopleUsingAge(Map<Person, Integer> map, int age) {
int counter = 0;
for (char name = MIN_NAME; name <= MAX_NAME; name++) {
if (map.containsKey(new Person(name, age))) {
counter++;
}
}
return counter;
}
String getAllPeopleUsingAge(Map<Person, Integer> map, int age) {
StringBuilder sb = new StringBuilder();
for (char name = MIN_NAME; name <= MAX_NAME; name++) {
Person key = new Person(name, age);
sb.append(key).append('=').append(map.get(key)).append('\n');
}
return sb.toString();
}
class Person implements Comparable<Person> {
char name;
int age;
public Person(char name, int age) {
this.name = name;
this.age = age;
}
//Making sure all elements end up in the very same bucket
//Nothing wrong with it except performance...
@Override
public int hashCode() {
return 0;
}
//equals is only by name
@Override
public boolean equals(Object other) {
Person otherPerson = (Person)other;
return this.name == otherPerson.name;
}
public String toString() {
return name + "[age=" + age + "]";
}
//comparing by age
//NOTE: compareTo is inconsistent with equals which should be OK in non-sorted collections
//https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html
@Override
public int compareTo(Person other) {
return this.age - other.age;
}
}
}
预期输出
Initial Size of Map: 0
Adding elements with age 1..
{K[age=1]=1, J[age=1]=1, I[age=1]=1, H[age=1]=1, G[age=1]=1, F[age=1]=1, E[age=1]=1, D[age=1]=1, C[age=1]=1, B[age=1]=1, A[age=1]=1}
Expected Number Of elements: 11
Actual Number of elements: 11
Overwriting map, with value 100..
{K[age=1]=100, J[age=1]=100, I[age=1]=100, H[age=1]=100, G[age=1]=100, F[age=1]=100, E[age=1]=100, D[age=1]=100, C[age=1]=100, B[age=1]=100, A[age=1]=100}
Expected Number Of elements: 11
Actual Number of elements: 11
Checking the values corresponding to age = 1
Count of People with age 1 =11
Checking the values corresponding to age = 100
Count of People with age 100 =11
Checking the values corresponding to age = 50
Count of People with age 50 =11
Checking the values corresponding to age = -10
Count of People with age -10 =11
实际输出
Initial Size of Map: 0
Adding elements with age 1..
{I[age=1]=1, A[age=1]=1, B[age=1]=1, C[age=1]=1, D[age=1]=1, E[age=1]=1, F[age=1]=1, G[age=1]=1, H[age=1]=1, J[age=1]=1, K[age=1]=1}
Expected Number Of elements: 11
Actual Number of elements: 11
Overwriting map, with value 100..
{I[age=1]=100, A[age=1]=1, B[age=1]=100, C[age=1]=1, D[age=1]=100, A[age=100]=100, C[age=100]=100, E[age=100]=100, H[age=100]=100, J[age=100]=100, K[age=100]=100, F[age=100]=100, G[age=100]=100, E[age=1]=1, F[age=1]=1, G[age=1]=1, H[age=1]=1, J[age=1]=1, K[age=1]=1}
Expected Number Of elements: 11
Actual Number of elements: 19
Checking the values corresponding to age = 1
Count of People with age 1 =11
Unexpected value for E[age=1]: expected <100> actual <1>.
Unexpected value for F[age=1]: expected <100> actual <1>.
Unexpected value for G[age=1]: expected <100> actual <1>.
Unexpected value for J[age=1]: expected <100> actual <1>.
Unexpected value for K[age=1]: expected <100> actual <1>.
Checking the values corresponding to age = 100
Count of People with age 100 =10
Size of the map is wrong: expected <11> actual <10>.
Unexpected value for B[age=100]: expected <100> actual <null>.
Checking the values corresponding to age = 50
Count of People with age 50 =5
Size of the map is wrong: expected <11> actual <5>.
Unexpected value for B[age=50]: expected <100> actual <null>.
Unexpected value for E[age=50]: expected <100> actual <null>.
Unexpected value for F[age=50]: expected <100> actual <null>.
Unexpected value for G[age=50]: expected <100> actual <null>.
Unexpected value for J[age=50]: expected <100> actual <null>.
Unexpected value for K[age=50]: expected <100> actual <null>.
Checking the values corresponding to age = -10
Count of People with age -10 =4
Size of the map is wrong: expected <11> actual <4>.
Unexpected value for A[age=-10]: expected <100> actual <1>.
Unexpected value for B[age=-10]: expected <100> actual <null>.
Unexpected value for C[age=-10]: expected <100> actual <1>.
Unexpected value for D[age=-10]: expected <100> actual <null>.
Unexpected value for E[age=-10]: expected <100> actual <1>.
Unexpected value for F[age=-10]: expected <100> actual <null>.
Unexpected value for G[age=-10]: expected <100> actual <null>.
Unexpected value for H[age=-10]: expected <100> actual <null>.
Unexpected value for J[age=-10]: expected <100> actual <null>.
Unexpected value for K[age=-10]: expected <100> actual <null>.
【问题讨论】:
-
compareTo implementation is inconsistent with equals如果你不正确地实现boolean compareTo(Object o),你可能会冒着在后台尝试基于 compareTo 排序并最终排序不正确/重复集合中的元素的风险 -
Comparable 应该与 Equals 和 Hashcode 一致...如果它们不一致,这是一种不好的做法,并且可能导致奇怪的结果。如果您必须将您的对象与另一个值进行比较,请改用 Comparator。
-
另请注意,通常不正确的接口实现可能会导致使用该接口的事物产生不正确的结果
-
//注意:compareTo 与 equals 不一致,这在未排序的集合中应该是可以的 //docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html
-
你能创建一个 MCVE 吗? (Minimal,完整且可验证的示例,强调“Minimal”,您可以进行比此更短的测试)stackoverflow.com/help/mcve