Directory
- Iterator
- Itr.hasnext and Itr.next implementations
- The second-to-last element of a special
- How to avoid the pit
All say ArrayList in the Foreach loop, can not add elements, and can not remove elements, may throw an exception, then we will analyze its specific implementation. My present environment is Java8.
There is the following section of code:
public class TestForEachList extends BaseTests { @Test public void testForeach() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); for (String s : list) { } }}
The code is simple, a ArrayList adds 3 elements, a foreach loop, and nothing is done. So how does foreach come into being, the brute force method to look at, compile and change the class, with javap -c TestForEachList
the view class file bytecode, as follows:
Javap-c testforeachlist warning:binary file testforeachlist contains collection.list.TestForEachListCompiled from " Testforeachlist.java "public class Collection.list.TestForEachList extends Com.ferret.BaseTests {public Collection.list.TestForEachList (); code:0: Aload_0 1:invokespecial #1//Method com/ferret/basetests. " <init> ":() V 4:return public void Testforeach (); code:0: New #2//class java/util/arraylist 3:dup 4:invokespecial #3 Method java/util/arraylist. " <init> ":() V 7:astore_1 8:aload_1 9:ldc #4//String 1 11:invok Einterface #5, 2//Interfacemethod Java/util/list.add: (ljava/lang/object;) Z 16:pop 17:aload_1 18:LDC #6//String 2 20:invokeinterface #5, 2//Interfacemethod Java/uti L/list.add: (ljava/lang/object;) Z 25:pop 26:aload_1 27:LDC #7//String 3 29:invokeinterface #5, 2//Inte Rfacemethod Java/util/list.add: (ljava/lang/object;) Z 34:pop 35:aload_1 36:invokeinterface #8, 1 Interfacemethod java/util/list.iterator: () ljava/util/iterator; 41:astore_2 42:aload_2 43:invokeinterface #9, 1//Interfacemethod Java/util/iterator.hasnext: () Z 48:ifeq 51:aload_2 52:invokeinterface #10, 1//Interfacemethod Java/util/iterat Or.next: () Ljava/lang/object; 57:checkcast #11//class java/lang/string 60:astore_3 61:goto 64:retur N
Can barely read, is called List.iterator
, and then returns the result according to iterator's Hasnext method to determine if there is a next one, based on the next method to take the next element.
But is the experience is not good, we are modern, so with some modern means, directly with idea open the class file automatically decompile, get the following content:
public class TestForEachList extends BaseTests { public TestForEachList() { } @Test public void testForeach() { List<String> list = new ArrayList(); list.add("1"); list.add("2"); list.add("3"); String var3; for(Iterator var2 = list.iterator(); var2.hasNext(); var3 = (String)var2.next()) { ; } }}
Experience a lot better, then compare the above bytecode file, yes
for(Iterator var2 = list.iterator(); var2.hasNext(); var3 = (String)var2.next()) { ; }
This is the real implementation of the foreach off the syntactic sugar shell.
Let's take a look at how these three methods are implemented:
Iterator
The iterator implementation of ArrayList is as follows:
public Iterator<E> iterator() { return new Itr();}private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; //省略部分实现}
Itr
is the inner class in the ArrayList, so list.iterator()
the function is to return an Itr
object assignment to var2
, after the call var2.hasNext()
, var2.next()
is the implementation of ITR.
It is also worth mentioning that expectedModCount
this variable record is assigned to modCount
, modCount
is a field of ArrayList's parent class abstractlist, the meaning of this field is the number of times the list structure has changed, This is usually triggered by an add or remove, which causes the number of elements to change modCount++
.
Next look at itr.hasNext()``var2.next()
the implementation.
Itr.hasnext and Itr.next implementations
Hasnext is simple.
public boolean hasNext() { return cursor != size; }
The current index is not equal to size, which means that the size is the field of the outer class ArrayList, representing the number of elements.
Look at Next implementation:
public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; }final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
Next method The first step checkForComodification()
, what does it do? If an modCount != expectedModCount
exception is thrown ConcurrentModificationException
. What is Modcount? The number of elements in the external class ArrayList; What is Expectedmodcount? Initializes the internal class ITR when the number of elements in the outer class changes.
Therefore, if you do an add or remove operation in foreach, the program will cause an exception ConcurrentModificationException
. There are two examples to follow:
@Test(expected = ConcurrentModificationException.class) public void testListForeachRemoveThrow() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); for (String s : list) { list.remove(s); } } @Test(expected = ConcurrentModificationException.class) public void testListForeachAddThrow() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); for (String s : list) { list.add(s); } }
The unit test ran, and all was thrown ConcurrentModificationException
.
checkForComodification()
After the code is relatively simple here is not analyzed.
The second-to-last element of a special
Here, let's go through the general process:
- Gets the
Itr
object assigned to VAR2
- Judge Hasnext, that is, the
cursor != size
current iteration element subscript is not equal to the number of list, then return true to continue iterating;
- Next take out the iteration elements
- Checkforcomodification (), judging
modCount != expectedModCount
that the number of elements changed is not equal to the initialization of the internal class ITR when the number of changes in the element, that is, during the iteration has been modified to throw ConcurrentModificationException
.
- If checked through cursor++
Consider the following scenario: What happens when you remove the second-to-last element? The code is as follows:
@Testpublic void testListForeachRemoveBack2NotThrow() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); for (String s : list) { System.out.println(s); if ("2".equals(s)) { list.remove(s); } }}
Guess what would throw an exception? The answer is in the negative. The output is:
1
2
Found 3 less output. Analyze
After the second-to-last element "2" remove, the list of the size-1 changed to 2, and at this time ITR in the next method to remove the element "2" after the addition of 1, the value becomes 2, resulting in the next Judgment Hasnext, Cursor==size, Hasnext returns false, eventually the last element is not output.
How to avoid the pit
Remove or add has a pit in foreach,
- An exception is thrown when you do an operation (remove, add, and so on) that causes the number of elements to change in foreach
ConcurrentModificationException
- When you remove the second element in a foreach, it causes the last element to not be traversed
So how do we avoid it? We can use Fori without using foreach, the following code:
@Test public void testListForiMiss() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); list.remove(i); } }
It is obvious that the above is a false demonstration , the output is as follows:
1
3
The reason is very simple, the original element 1 is removed, the back of the forward copy, 2 to the original 1 position (subscript 0), 3 to the original position of 2 (subscript 1), size from 3 to 2,i+1=1, output list.get (1) became 3, 2 was omitted.
The following is the correct demonstration:
method One, or fori, the position before the move minus back on the line, remove after i--:
@Test public void testListForiRight() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); list.remove(i); i--; //位置前挪了减回去就行了 } }
method Two, without the ArrayList remove method, with the ITR own definition of the Remove method , the code is as follows:
@Test public void testIteratorRemove() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); Iterator<String> itr = list.iterator(); while (itr.hasNext()) { String s = itr.next(); System.out.println(s); itr.remove(); } }
Why is ITR's own definition of remove not an error? Look at the Source:
public void remove() { if (lastRet < 0) throw new IllegalStateException(); //依然有校验数量是否变更 checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; //但是变更之后重新赋值了,又相等了 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
There are still checkForComodification()
checks, but after seeing that they are re-assigned, they are equally.
OK, the above is the whole content. Describes the pit of the list remove in foreach and how to avoid it.
Analysis of the problem of remove from Java ArrayList in foreach