B4J Question Deleting items from a Map collection gives exception

aminoacid

Active Member
Licensed User
I am really stumped by this. I have a map collection and I am trying to selectively delete items from the map using "map.remove". I keep on getting a "java.util.NoSuchElementException".

I can reproduced the problem using a very simple example code snippet shown below:

B4X:
Sub AppStart (Args() As String)
    Dim m As Map
    m.Initialize
      
    m.Put("1","XXX")
    m.Put("2","XX")
    m.Put("3","X")
    m.Put("4","XXXX")
    m.Put("5","XXXX")
  
    For Each s As String In m.Keys
         log(s)
         m.Remove(s)
    Next

    Log(m.Size)
  
End Sub


Here is the Log. As you can see it looks like keys 1,3 and 5 are deleted but then the code crashes after that.

B4X:
1
3
5
main._appstart (java line: 70)
java.util.NoSuchElementException
    at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:721)
    at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:752)
    at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:750)
    at anywheresoftware.b4a.objects.collections.Map$MyMap.getEntry(Map.java:219)
    at anywheresoftware.b4a.objects.collections.Map$MyMap.getKey(Map.java:196)
    at anywheresoftware.b4a.objects.collections.Map.GetKeyAt(Map.java:95)
    at anywheresoftware.b4a.objects.collections.Map$IterableMap.Get(Map.java:160)
    at b4j.example.main._appstart(main.java:70)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:90)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:77)
    at b4j.example.main.main(main.java:29)


I realize that I can use "m.clear" to accomplish the same result but that's not what I want to do. I want to selectively remove items from the map based on some condition with the possibility of ALL items being removed:

For Each s As String In m.Keys
if <some condition is true> then m.Remove(s) <--- could possibly be true for the entire list
Next

I also tried a traditional For-Next loop with the same results.

Question is ... does anyone know what is wrong with the code? And if it cannot be done this way, how would one delete selected items (and potentially ALL items) in a map collection using a loop and without having to use "map.clear"
 

davidvidasoft

Member
Licensed User
The problems is this: as you remove items from the map the ´m.Keys´ iterable list starts losing its keys and then a weird interaction happens.

You could store the keys in a separate list and iterate this list to remove the object in the map you want. Something like this:
B4X:
Sub AppStart (Form1 As Form, Args() As String)
    Dim m As Map
    Dim l As List
    m.Initialize
    l.Initialize
  
    m.Put("1","XXX")
    l.Add("1")
    m.Put("2","XX")
    l.Add("2")
    m.Put("3","X")
    l.Add("3")
    m.Put("4","XXXX")
    l.Add("4")
    m.Put("5","XXXX")
    l.Add("5")
  
    For Each s As String In l
        Log(s)
        m.Remove(s)
    Next

    Log(m.Size)

End Sub

Notice that it's done this way to avoid referencing the same ´keys´ object.
 
Upvote 0

aminoacid

Active Member
Licensed User
Yes, my work-around was using an array to store the keys and then using the array to delete the items in the map in a manner similar to what you suggest. I was hoping for a more elegant solution and just could not figure out why this was happening when the code looks perfectly logical. The alternative sure is messy!!
Anyway you clarified it for me.
Thank you.
 
Upvote 0

Daestrum

Well-Known Member
Licensed User
An alternative using javaobject and adding one line
B4X:
 ' uses javaobject
...
Dim m As Map
 m.Initialize
 m.put("1","one")
 m.put("2","two")
 m.put("3","three")
 m.put("4","four")
 m.put("5","five")
 Dim keys() As Object = asJO(m).RunMethodJO("keySet",Null).RunMethodJO("toArray",Null)
 For Each s As Object In keys
  Log(m.Remove(s))
 Next
...

Sub asJO(o As JavaObject)As JavaObject
 Return o
End Sub
 
Upvote 0

aminoacid

Active Member
Licensed User
Thank you for the suggestion. Unfortunately I need this to be cross platform. The application will be ported over to B4A and B4I
 
Upvote 0

OliverA

Expert
Licensed User
Yeah, when iterating through a map like that and deleting elements, you are pulling the rug out from under iterable list. In other words, beneath the hood, B4J is creating a
B4X:
for i = 0 to m.size -1
loop.
So first, i = 0 and the iterator gives you m.GetKeyAt(i) where i = 0 which gives you the 1.
You then delete the first element, and then internally the map shuffles everything down (m.GetKeyAt(0) is now "2", m.GetKeyAt(1) is "3" etc.)
Now i = 1, and m.GetKeyAt(i) where i = 1 gives you the 3
Now i = 2, and m.GetKeyAt(i) where i = 2 gives you the 5
now i = 3, and m.GetKeyAt(i) bomb's out since there is no such index position, and only m.GetKeyAt(0) with a value of "2" and m.GetKeyAt(1) with a value of "4" are left.
But this is a very special case (deleting all elements while iterating through it using the iterator)

Now what you can do is the following:

B4X:
for i = m.size - 1 to 0 step -1
   'if m.GetValueAt(i) meets some condition then m.remove(m.GetKeyAt(i))
next
This way you start removing from the top and as you work your way down, you will not retouch a previously touched element (or skip one).

Hope this makes some sense...
 
Last edited:
Upvote 0

aminoacid

Active Member
Licensed User
Yes, I just found that out in B4i so I reverted back to davidvidasoft's suggestion (above) of using a temporary list to hold the keys to be deleted. The code ended up being just like you have above. Thanks all for your very useful suggestions.
 
Upvote 0
Top