Tricky NullPointerException in IntObjectMap.forEachKeyValue
magicprinc opened this issue · comments
If you remove key 0 (int; aka EMPTY_KEY) in forEachKeyValue
- you will enjoy NullPointerException
@Test public void testBugRemoveNPE (){
try {
var m = IntObjectMaps.mutable.of(0, 'a');
m.forEachKeyValue((k,v)->m.removeKey(k));
fail();
} catch (NullPointerException e){
//assertEquals("Cannot read field \"containsOneKey\" because \"this.sentinelValues\" is null", e.getMessage());
}
}
Problem is here:
org.eclipse.collections.impl.map.mutable.primitive.IntObjectHashMap#forEachKeyValue
if (this.sentinelValues.containsOneKey)
after removing key 0 aka EMPTY_KEY - sentinelValues can be null
The problem is easy to fix:
if (this.sentinelValues != null && this.sentinelValues.containsZeroKey)
{
procedure.value(EMPTY_KEY, this.sentinelValues.zeroValue);
}
if (this.sentinelValues != null && this.sentinelValues.containsOneKey)
{
procedure.value(REMOVED_KEY, this.sentinelValues.oneValue);
}
While the above is true, I don't believe we support map modifications within forEachKeyValue
. remove
is "safe" by accident (and not safe in the case of the zero key). put
would cause all manner of problems.
Perhaps that needs to be better documented.
Additionally, we could add removeIf
to primitive maps (I'm assuming this is how you ran into this, as the code sample to reproduce this is not likely your real code).
It is absolutely safe (even if by accident). Only this case with key==0 is a problem.
I didn't want to call removeKey
in forEachKeyValue
in the first place, BUT
keyValuesView
iterator doesn't support remove
(throws exception) and it was harder to tell how safe to call removeKey
from this iterator...
Real code (migrated from JDK Map) looks like this:
void verify (@Nonnull MutableIntObjectMap<byte[]> m){
m.forEachKeyValue((ie,b)->{
int iel = b.length;//actual ie len
Integer ielMin = IE_LEN_MIN.get(ie);
if (ielMin != null && iel < ielMin) {
m.removeKey(ie);
}
Integer ielMax = IE_LEN_MAX.get(ie);
if (ielMax != null && iel > ielMax) {
m.removeKey(ie);
}
});
}
I use EC only few hours. Probably there is a more idiomatic way to do this.
PS: BUT bug with NPE is easy to fix. So, why keep it lying there ;-) Other primitive key Map implementations have it probably too.
The above code fits well within a removeIf
implementation.
We have two choices here:
- Document
forEachKeyValue
as not allowing modifications. This allows us to change the map implementation internals much easier. For example, if we implement auto-shrinking,remove
will no longer be safe, even by accident. Additionally, addremoveIf
. Maybe implementkeyValuesView.iterator().remove()
. - Document
forEachKeyValue
as allowingremove
(but notput
!) and test all scenarios with this and fix various issues.
Relying on an accident, instead of an intent, is neither good for your code nor for the library.
I would choose both :-)
- implement keyValuesView.iterator().remove() - is a very good thing!
- Document forEachKeyValue as allowing remove (but not put!) and test all scenarios with this and fix various issues. And fix it (no more NPE), obviously!
I've just checked: this pattern is everywhere
if (this.sentinelValues != null)
{
if (this.sentinelValues.containsZeroKey)
{
procedure.value(EMPTY_KEY);
}
if (this.sentinelValues.containsOneKey)// a place to hit your head
{
procedure.value(REMOVED_KEY);
}
}
in forEachKey, forEachValue, etc...
So they all have this exactly same problem.
And it means: it is really hard to remove from primitive Maps...