Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

motlin
Copy link
Contributor

@motlin motlin commented Jun 3, 2024

No description provided.

{
this.put(entry.getKey(), entry.getValue());
}
map.forEach(this::put);
Copy link
Contributor Author

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;
Copy link
Contributor Author

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());
Copy link
Contributor Author

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());
}

Copy link
Contributor Author

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");
Copy link
Contributor Author

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
Copy link
Contributor Author

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");
Copy link
Contributor Author

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);
Copy link
Contributor Author

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);
Copy link
Contributor Author

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);
}
Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant