3

Why does the following occur? Shouldn't both work?

List<String> items = data;
for( String id : items ) {
    List<String> otherItems = otherData;        

    // 1.   addAll()
    //Causes ConcurrentModificationException
    items.addAll(otherItems);

    // 2.   .add()
    //Doesn't cause exceptions
    for( String otherId : otherItems ) {
        items.add(otherId);
    }
}

Is it because add() adds to the collection Items, but addAll() creates a new collection thus modifying Items to be a different instance of List?

Edit items and otherItems are of concrete type ArrayList<String>.

Saran
  • 3,845
  • 3
  • 37
  • 59
FreakyDan
  • 559
  • 1
  • 7
  • 24
  • 4
    What concrete type is `Items`? – Mureinik Oct 03 '14 at 18:23
  • 1
    Because you have an iterator open on items and items doesn't support concurrent modification. Why you're allowed a single add is the real question. Either way, what you're doing here probably isn't what you want. – David Ehrmann Oct 03 '14 at 18:24

3 Answers3

6

Neither operation is proper, since it modifies the collection while iterating it.

Examining the implementation of ArrayList shows that calling either add or addAll should successfully throw the ConcurrentModificationException on the next loop iteration. The fact that it isn't doing that for add means that either there is an obscure bug in the ArrayList class for the particular version of Java you have; or (more likely) otherItems is empty, so in the second case, you aren't actually calling add at all.

I'm sure otherItems must be empty, because if adding to the Items list "worked" in the way you want, then it would grow every time around the loop, causing it to loop indefinitely until dying with an OutOfMemoryError.

Boann
  • 48,794
  • 16
  • 117
  • 146
  • 1
    You're right, otherItems was never being populated. It does fail if there is data in it, good catch! – FreakyDan Oct 03 '14 at 18:44
  • @FreakyDan - which means the 'otherItems' would never have caused the CME either. Your code in your question is messed up. `addAll()` of an empty collection would not cause a CME. – rolfl Oct 03 '14 at 18:52
  • @FreakyDan - actually, you may have found a bug in Java... hmmm. I am editing my answer. – rolfl Oct 03 '14 at 18:54
  • 1
    @rolfl `addAll` of an empty collection does cause the CME. See the ArrayList code: `addAll` unconditionally calls `ensureCapacityInternal`, which unconditionally calls `ensureExplicitCapacity`, which unconditionally does `modCount++`. – Boann Oct 03 '14 at 18:55
  • I see that side effect now. That is a bug (in ArrayList). Interesting. – rolfl Oct 03 '14 at 18:56
4

The for loop you have here

for( String id : Items ) {

is logically the same as:

for(Iterator<String> it = Items.iterator(); it.hasNext();) {
    String id = it.next();

    ....
}

Now, if you modify the list an iterator iterates over, in the middle of iterating, you get the ConcurrentModificationException. From the Javadoc for ArrayList:

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException.

So, the addAll() is modifying Items, and causing the iterator to fail.

Your only solution is to either:

  1. don't do the addAll()
  2. change to the loop with the iterator, and do:

    for (String other : otherItems) {
        it.add(other);
    }
    

    In other words, do the add via the iterator, and avoid the ConcurrentModification....

Now, about why add() works, and addAll() does not? I believe you are probably just seeing something like the add version when there are no other items in the iterator, or the values being added are emtpry, Perhaps a bug in the implementation of Items. It should be throwing a CME, and the fact that it is not throwing one, means there is a bug, not in your code, but in the collection.

They should both fail!

BUT: You have subsequently found that the addAll() was adding an empty collection. An empty addAll() should not be causeing a CME... and, as @Boann points out, this is a bug in the implementation of ArrayList.

I have put together the following test to prove things out:

private static List<String> buildData() {
    return new ArrayList<>(Arrays.asList("Hello", "World"));
}

public static void testThings(List<String> data, List<String> addall, List<String> add) {
    System.out.printf("Using %s addAll %s and add %s%n", data, addall, add);

    try {
        for (String s : data) {
            if (addall != null) {
                data.addAll(addall);
            }
            if (add != null) {
                for (String a : add) {
                    data.add(a);
                }
            }
        }
        System.out.println("OK: " + data);
    } catch (Exception e) {
        System.out.println("Fail: " + e.getClass() + " -> " + e.getMessage());
    }
}

public static void main(String[] args) {

    String[] hw = {"Hello", "World"};

    testThings(buildData(), Arrays.asList(hw), null);
    testThings(buildData(), null, Arrays.asList(hw));
    testThings(new ArrayList<>(), Arrays.asList(hw), null);
    testThings(new ArrayList<>(), null, Arrays.asList(hw));
    testThings(buildData(), new ArrayList<>(), null);
    testThings(buildData(), null, new ArrayList<>());
    testThings(new ArrayList<>(), new ArrayList<>(), null);
    testThings(new ArrayList<>(), null, new ArrayList<>());
}

This produces the results:

Using [Hello, World] addAll [Hello, World] and add null
Fail: class java.util.ConcurrentModificationException -> null
Using [Hello, World] addAll null and add [Hello, World]
Fail: class java.util.ConcurrentModificationException -> null
Using [] addAll [Hello, World] and add null
OK: []
Using [] addAll null and add [Hello, World]
OK: []
Using [Hello, World] addAll [] and add null
Fail: class java.util.ConcurrentModificationException -> null
Using [Hello, World] addAll null and add []
OK: [Hello, World]
Using [] addAll [] and add null
OK: []
Using [] addAll null and add []
OK: []

Note the two lines:

Using [Hello, World] addAll [] and add null
Fail: class java.util.ConcurrentModificationException -> null
Using [Hello, World] addAll null and add []
OK: [Hello, World]

Adding an empty addAll causes a CME, yet that does not structurally modify the list. This is a bug in ArrayList.

rolfl
  • 17,539
  • 7
  • 42
  • 76
  • Interesting, I always thought it was intentional that you could add to the end. I always viewed it as you can add to a chain, but you can't rearrange it. So in my inexperienced mind both should have worked. – FreakyDan Oct 03 '14 at 18:31
  • 1
    I like this "Now, if you modify the list an iterator iterates over, in the middle of iterating, you get the ConcurrentModificationException." do you have any resource for that so I can read more? – Kick Buttowski Oct 03 '14 at 18:31
  • 2
    @KickButtowski - The [JavaDoc for ArrayList](http://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html): *The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException.* – rolfl Oct 03 '14 at 18:34
  • The specification is loose as to whether the CME should be thrown on the `nasNext()`, `next()` or both calls. Java libraries only throw on `next()` and not `hasNext()`, which some people consider to be a bug... as that is not 'fail fast'. – rolfl Oct 03 '14 at 18:36
  • Thank you for linking that, my assumption is that ArrayList.add() works similarly to 'iterator's own [add method]' while .addAll() doesn't. – FreakyDan Oct 03 '14 at 18:39
0

I agree with rolfl -> in both cases CME should be thrown...

Most probable answer:

addAll() is a first invocation, it throws Exception, and second piece of code is simply never reached -> you have forgotten to comment first part out and therefore think that add()-part is exception-free code ;)

cichystefan
  • 328
  • 2
  • 10