【问题标题】:HashMaps with Comparable keys not working as expected具有 Comparable 键的 HashMap 无法按预期工作
【发布时间】: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

标签: java hashmap


【解决方案1】:

从 Java 8 开始,HashMap 添加了一项优化,以处理键为 Comparable 时的冲突:

为了改善影响,当键为java.lang.Comparable时,该类可能会使用键之间的比较顺序来帮助打破平局。

如果您的键的自然顺序与 equals() 不一致,则可能会发生任何奇怪的事情,在这种情况下,地图似乎正试图使用​​自然顺序来跟踪键。

【讨论】:

  • Bang on target 我也在使用 java 8... :) 非常感谢 chrylis
  • 改善什么影响?这是否意味着他们做了比破坏HashMap 更糟糕的事情?
  • 如果一个类型可以根据compareTo 相同而与equals 不一样,那你没关系,只要它们与equals 相同,那么它们也与compareTo 相同。标准示例是BigDecimal1.01.00equals 不同,但与compareTo 相同。
【解决方案2】:

我复制了你意想不到的结果。然后我从Person 中删除了Comparable,所有检查都通过了。然后我恢复了Comparable,切换到OpenJDK 7,所有的检查都通过了。可以肯定的是,我切换回 Oracle Java 8,再次得到了你意想不到的结果。显然,Oracle 在 Java 8 中使用 HashMap 做了一些“聪明”的事情。继续努力,Oracle。

【讨论】:

  • 为了向后兼容和编写一次/到处运行 :-(
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-21
  • 1970-01-01
  • 1970-01-01
  • 2013-12-23
  • 2014-12-09
相关资源
最近更新 更多