我们面临着自Java 8以来HashMap行为方式的奇怪问题.
当HashMap键实现Comparable接口但是compareTo实现与equals不一致时HashMaps:
他们应该成长得更大
它们包含几个相等元素的实例
附加到这些元素的值可能不同
get(key)结果取决于使用哪个键(即使键根据equals方法相等).
我创建了一个小测试来重现问题(见下文).测试总是通过Java 7(可能还有以前的版本).Java 8中的测试总是失败(除非我从类中删除了Comparable接口).
我不确定这是多么可以修复,如果没有可能在javadoc中显式下划线,那么如果要在哈希集合中使用对象,则必须与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 HashMappersonToAgeMap; 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 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 map, int age) { for (char name = MIN_NAME; name <= MAX_NAME; name++) { map.put(new Person(name, age), age); } } int countAllPeopleUsingAge(Map 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 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 { 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 + "]"; } // compareTo is inconsistent with equals which should be OK in // non-sorted collections @Override public int compareTo(Person other) { return this.age - other.age; } } }
Marco13.. 5
该HashMap的文件说:
为了改善影响,当键是Comparable时,此类可以使用键之间的比较顺序来帮助打破关系.
因此,如果使用Comparable
具有不一致比较顺序的元素,则必须期望像这样的奇怪行为.
HashMap
Java 8 的实现说明中也明确提到了这种行为:
/* * Implementation notes. * ... * Tree bins (i.e., bins whose elements are all TreeNodes) are * ordered primarily by hashCode, but in the case of ties, if two * elements are of the same "class C implements Comparable", * type then their compareTo method is used for ordering. (We * conservatively check generic types via reflection to validate * this -- see method comparableClassFor). ...
这是在OpenJDK的以下更改中引入的:http://hg.openjdk.java.net/jdk8/jdk8/jdk/diff/d62c911aebbb/src/share/classes/java/util/HashMap.java#l1.73
该HashMap的文件说:
为了改善影响,当键是Comparable时,此类可以使用键之间的比较顺序来帮助打破关系.
因此,如果使用Comparable
具有不一致比较顺序的元素,则必须期望像这样的奇怪行为.
HashMap
Java 8 的实现说明中也明确提到了这种行为:
/* * Implementation notes. * ... * Tree bins (i.e., bins whose elements are all TreeNodes) are * ordered primarily by hashCode, but in the case of ties, if two * elements are of the same "class C implements Comparable", * type then their compareTo method is used for ordering. (We * conservatively check generic types via reflection to validate * this -- see method comparableClassFor). ...
这是在OpenJDK的以下更改中引入的:http://hg.openjdk.java.net/jdk8/jdk8/jdk/diff/d62c911aebbb/src/share/classes/java/util/HashMap.java#l1.73