Fixes an issue in HashMap iteration.
HashMap's iterator would skip the last key of keys that collided
on hashcode if the previous key was removed using Iterator#remove.
If keys collide on their hashcodes the last added
key for a certain hashcode was skipped in iteration.
Change-Id: I7affad4d70b18a808948c02836dcdfbb9648c414
diff --git a/user/super/com/google/gwt/emul/java/util/InternalHashCodeMap.java b/user/super/com/google/gwt/emul/java/util/InternalHashCodeMap.java
index 6050ef4..19a0011 100644
--- a/user/super/com/google/gwt/emul/java/util/InternalHashCodeMap.java
+++ b/user/super/com/google/gwt/emul/java/util/InternalHashCodeMap.java
@@ -143,13 +143,11 @@
public void remove() {
checkState(lastEntry != null);
+ boolean isLastChain = itemIndex == lastChain.length;
InternalHashCodeMap.this.remove(lastEntry.getKey());
- // If we are sill in the same chain, our itemIndex just jumped an item. We can fix that
- // by decrementing the itemIndex. However there is an exception: if there is only one
- // item, the whole chain is simply dropped not the item. If we decrement in that case, as
- // the item is not drop from the chain, we will end up returning the same item twice.
- if (chain == lastChain && chain.length != 1) {
+ // If this was not the last item in the chain, our itemIndex just jumped an item.
+ if (!isLastChain) {
itemIndex--;
}
diff --git a/user/test/com/google/gwt/emultest/java/util/HashMapTest.java b/user/test/com/google/gwt/emultest/java/util/HashMapTest.java
index a4f4dac..28f232f 100644
--- a/user/test/com/google/gwt/emultest/java/util/HashMapTest.java
+++ b/user/test/com/google/gwt/emultest/java/util/HashMapTest.java
@@ -1,12 +1,12 @@
/*
* Copyright 2008 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -102,7 +102,7 @@
/**
* Check the state of a newly constructed, empty HashMap.
- *
+ *
* @param hashMap
*/
private static void checkEmptyHashMapAssumptions(HashMap<?, ?> hashMap) {
@@ -757,6 +757,81 @@
return new HashMap();
}
+ // see: https://github.com/gwtproject/gwt/issues/9184
+ public void testIterationWithCollidingHashCodes() {
+
+ Object firstKey = createObjectWithHashCode(1);
+ Object secondKey = createObjectWithHashCode(1);
+
+ // make sure we actually have a hash collision for this test
+ assertEquals(firstKey.hashCode(), secondKey.hashCode());
+
+ HashMap<Object, String> testMap = new HashMap<>();
+ testMap.put(firstKey, "one");
+ testMap.put(secondKey, "two");
+
+ Iterator<Object> it = testMap.keySet().iterator();
+
+ assertTrue("new iterator should have next", it.hasNext());
+ Object keyFromMap1 = it.next();
+
+ it.remove();
+
+ assertTrue("iterator should have next after first removal", it.hasNext());
+ Object keyFromMap2 = it.next();
+
+ it.remove();
+ assertFalse(it.hasNext());
+
+ // since iteration order is not defined for HashMap we need to make this test work with
+ // both outcomes of the iteration
+ if ((firstKey == keyFromMap1 && secondKey == keyFromMap2)
+ || (firstKey == keyFromMap2 && secondKey == keyFromMap1)) {
+ return;
+ }
+ fail("Wrong keys returned in iteration");
+ }
+
+ // see: https://github.com/gwtproject/gwt/issues/9184
+ public void testIterationWithCollidingHashCodes_FullIteration() {
+
+ List<Object> keys = new ArrayList<>();
+ HashMap<Object, String> testMap = new HashMap<>();
+ for (int i = 0; i < 100; i++) {
+ Object key = createObjectWithHashCode((i + 10) / 10);
+ keys.add(key);
+ testMap.put(key, "" + i);
+ }
+
+ int expectedSize = keys.size();
+
+ for (Iterator<Object> it = testMap.keySet().iterator(); it.hasNext(); expectedSize--) {
+ Object key = it.next();
+ assertTrue("Unexpected key was returned", keys.contains(key));
+ keys.remove(key);
+ it.remove();
+ assertEquals("Map size", expectedSize - 1, testMap.size());
+ }
+
+ assertEquals("expected size", 0, expectedSize);
+ assertTrue(testMap.isEmpty());
+ assertTrue(keys.isEmpty());
+ }
+
+ private Object createObjectWithHashCode(final int hashCode) {
+ return new Object() {
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return "" + hashCode;
+ }
+ };
+ }
+
private void iterateThrough(final HashMap<?, ?> expected) {
Iterator<?> iter = expected.entrySet().iterator();
for (int i = 0; i < expected.size(); i++) {