-
Notifications
You must be signed in to change notification settings - Fork 613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix inconsistencies in unmodifiable maps. #1617
base: master
Are you sure you want to change the base?
Conversation
{ | ||
this.put(entry.getKey(), entry.getValue()); | ||
} | ||
map.forEach(this::put); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change fixes assertions in HashBiMapNoIteratorTest, which now inherits more tests/coverage from MapTestCase.
{ | ||
throw new UnsupportedOperationException("Cannot mutate " + this.getClass().getSimpleName()); | ||
} | ||
return result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementations like these are debatable, but are done consistently elsewhere like in UnmodifiableMutableMap. The trait-based testing approach is applying identical assertions to all the unmodifiable map wrappers and exposing differences like these.
@Override | ||
public V putIfAbsent(K key, V value) | ||
{ | ||
throw new UnsupportedOperationException("Cannot call putIfAbsent() on " + this.getClass().getSimpleName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that other composite methods like putIfAbsent and the compute* methods all consistently throw, regardless of if the map would be mutated. These overrides make sure that the exception is consistently UnsupportedOperationException and that any lambdas passed into the compute* methods are never invoked.
{ | ||
throw new UnsupportedOperationException("Cannot call replaceAll() on " + this.getClass().getSimpleName()); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still rely on abstract classes a lot, rather than an interface hierarchy with default methods, leading to lots of duplication. Cleaning that up is a bigger task.
@@ -770,7 +770,7 @@ public static <K, V> void forEachKeyValue(Map<K, V> map, Procedure2<? super K, ? | |||
{ | |||
if (map == null) | |||
{ | |||
throw new IllegalArgumentException("Cannot perform a forEachKeyValue on null"); | |||
throw new NullPointerException("Cannot perform a forEachKeyValue on null"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most implementations of putAll() already throw NPE when the argument is null, but a few delegate to this method and throw IllegalArgumentException.
@@ -34,7 +35,7 @@ | |||
import static org.junit.Assert.assertThrows; | |||
import static org.junit.Assert.assertTrue; | |||
|
|||
public interface MapIterableTestCase extends RichIterableWithDuplicatesTestCase | |||
public interface MapIterableTestCase extends RichIterableWithDuplicatesTestCase, MapTestCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a tricky detail. Every MapIterable in Eclipse Collections implements Map, even the immutable ones. That means we ought to be running tests against methods like Map.put() for every MapIterable. This is the core of the change that led to discovering so many inconsistencies between Mutable, Immutable, and Unmodifiable maps.
@Test | ||
default void Map_put() | ||
{ | ||
Map<Integer, String> map = (Map<Integer, String>) this.newWithKeysValues(3, "Three", 2, "Two", 1, "One"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newWithKeysValues() returns Object in these tests, to allow for Map and MapIterable to have the same assertions. The result of the method call is cast to Map.
|
||
<K, V> Map<K, V> newWithKeysValues(Object... elements); | ||
// Returns Object to allow subclasses to return either Map or MapIterable, and be cast to Map either way | ||
<K, V> Object newWithKeysValues(Object... elements); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's where we return Object to allow testing of Map and MapIterable together. The generics are funny here. Deleting them leads to the overriding methods not being recognized as overrides.
{ | ||
assertNull(result.put(random.nextDouble(), each)); | ||
} | ||
return Collections.unmodifiableMap(result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an important test to ensure consistency across implementations. This runs the same assertions against Collections.unmodifiableMap(new HashMap<>())
return null; | ||
})); | ||
Assert.assertEquals(this.newWithKeysValues(1, "1", 2, "2", 3, "3"), map); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These assertions are pulled up and now apply to all MutableMapIterables, not just MutableMaps. This led to finding some inconsistencies in BiMaps.
b674cee
to
62d2e24
Compare
8968fab
to
e374155
Compare
45caeb7
to
7975b90
Compare
7975b90
to
b1b96aa
Compare
No description provided.