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

Tricky NullPointerException in IntObjectMap.forEachKeyValue #1418

Open
magicprinc opened this issue Dec 29, 2022 · 5 comments
Open

Tricky NullPointerException in IntObjectMap.forEachKeyValue #1418

magicprinc opened this issue Dec 29, 2022 · 5 comments

Comments

@magicprinc
Copy link

magicprinc commented Dec 29, 2022

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);
}
@mohrezaei
Copy link
Member

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).

@magicprinc
Copy link
Author

magicprinc commented Dec 29, 2022

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.

@mohrezaei
Copy link
Member

The above code fits well within a removeIf implementation.

We have two choices here:

  1. 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, add removeIf. Maybe implement keyValuesView.iterator().remove().
  2. Document forEachKeyValue as allowing remove (but not put!) 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.

@magicprinc
Copy link
Author

I would choose both :-)

  1. implement keyValuesView.iterator().remove() - is a very good thing!
  2. 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!

@magicprinc
Copy link
Author

magicprinc commented Dec 29, 2022

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...

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

No branches or pull requests

2 participants