Search code examples
arrayslistgroovyiterationconcurrentmodification

Groovy, collating list causes concurrentmodification exception


Still learning the ropes with Groovy, this problem has tripped me up since last night. Not sure why it's throwing concurrentmod exception...(Java 1.6, Groovy 1.8.4)

I have a list of keys... [1,2,3,4,5,6,7,8,9,10,11,12,13]

I collate the list using a custom function partitionList(keys,3) I got from here (can't use java.List.Collate, not on 1.8.6)

Now I've got a list of lists... [[0,1,2],[3,4,5],[6,7,8],[9,10,11],[12,13]]

If the number of sub-lists created is odd, I remove the last sub list [12,13] and redistribute its keys amongst the other sub lists starting in order, creating...

[[0,1,2,12],[3,4,5,13],[6,7,8],[9,10,11]]

The exception occurs when iterating the values of the last sub list. Not sure why since I'm iterating a list and changing an entirely different list in that loop...

UPDATE:

Interesting....if I don't use the paritionList() function, replacing def keyRanges = partitionList( keys, 3) with an explicit list of lists... def keyRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11],[12,13]] the problem disappears. So I'm lead to believe the partitionList() function is doing something that's causing the exception

class CollateListTest {

    static main(args) {    
        def keys = (0..13).toList()

        //partition the keys into a list of lists 
        def keyRanges = partitionList( keys, 3)
        println 'Key range sets...'
        for( keyRange in keyRanges)
            println keyRange

        //if odd number of partitions, 
        //remove last sub list and redistribute values over other lists
        if( (keyRanges.size() % 2) != 0){
            def lastKeyRange = keyRanges.remove( keyRanges.size() - 1 )
            println 'removed range: ' + lastKeyRange

                    // ** EXCEPTION HERE **         
            lastKeyRange.eachWithIndex{ k, index ->
                println 'adding: ' + k
                keyRanges[ index % keyRanges.size()].add( k)
            }
        }
    }

    //from stackoverflow.com/questions/2924395/
    static def partitionList(list, size) {
        def partitions = []
        int partitionCount = list.size() / size

        partitionCount.times { partitionNumber ->
            def start = partitionNumber * size
            def end = start + size - 1
            partitions << list[start..end]
        }

        if (list.size() % size) partitions << list[partitionCount * size..-1]
        return partitions
    }
}

Solution

  • The partitionList method you're using splits up the list with List.getAt(Range). This returns a view into the original list, but doesn't copy the data, so any modifications to the sublist also affect the original.

    This means that lastKeyRange shares data with keyRanges, and that adding to one of the sublists indirectly affects the sublist you are iterating over. Rather than modifying the sublist, just create a new one. For example:

    if( (keyRanges.size() % 2) != 0){
        def lastKeyRange = keyRanges.remove( keyRanges.size() - 1 )
        println 'removed range: ' + lastKeyRange
    
        lastKeyRange.eachWithIndex{ k, index ->
            println 'adding: ' + k
            def newRange = []
            newRange.addAll(keyRanges[ index % keyRanges.size()])
            newRange.add(k)
            keyRanges[ index % keyRanges.size()] = newRange
    
        }
    }