[rigong's attack on the big factory series] don't jump into the interview trap of ArrayList

Posted by fredley on Tue, 04 Jan 2022 23:37:36 +0100

background

Yesterday, Xiao Feng received an interview call from a company. One of the interview questions was a little interesting. I'll share it with you here. The interview question is how to delete the specified element in ArrayList. At first glance, it sounds like a simple question, but if you don't actually step on the pit, you can easily fall into the trap of the interviewer. Let's analyze it together.

problem analysis

Full of doubts

When Xiao Feng heard the interview question, he thought, what is this surface examiner, how to ask such a simple question, and thought that a for loop plus equal judgment and then delete would be finished? But on second thought, no, there must be a trap in it, otherwise I wouldn't ask such a seemingly simple question. Xiao Feng suddenly remembered that he had encountered this problem when writing code before. He also deleted the specified element in the ArrayList, but threw an exception when he directly remove d the element of the for loop. The interviewer's trap is estimated to be here. Xiaofeng secretly rejoiced and found the trap buried by the interviewer.

Xiaofeng recalled the test situation of the day, and the code was desensitized. At the beginning, he wanted to delete the specified elements in the ArrayList. Xiao Feng wrote the following code heartily and confidently. He clicked the Run code button. As a result, he was embarrassed and threw exceptions.

public class TestListMain {

    public static void main(String[] args) {

        List<String> result = new ArrayList<>();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");

        for (String s : result) {
            if ("b".equals(s)) {
                result.remove("b");
            }
        }

    }
}

Copy code

A big red exception will come out immediately. OMG, how can this happen? I feel that there is no problem with the code. Hurry to see what exceptions are thrown and where they are thrown. You can see that an exception of ConcurrentModificationException is thrown, and it is thrown in a detection method in the Itr class. What's going on? We didn't have this Itr code in our original code, so we couldn't figure it out.

restore justice

Since we can't find out from the source code analysis, let's look at the content of the class file compiled by the source code. After all, the class file is the code really executed by the JVM. I don't know if I don't see it. I'm surprised to see that JDK used to play like this. I see. The for each statement in our original code is actually executed by an iterator after compilation.

public class TestListMain {
    public TestListMain() {
    }

    public static void main(String[] args) {
        List<String> result = new ArrayList();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");
        //Create iterator
        Iterator var2 = result.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            if ("b".equals(s)) {
                result.remove("b");
            }
        }

    }
}

Copy code

Itr, an internal class iterator created by ArrayList, then the for each loop is transformed into an iterator plus a while loop. The original for each loop is sold for dog meat.

  public Iterator<E> iterator() {
        return new Itr();
    }

Copy code

Itr, an internal class iterator, judges whether the iterator has content by judging hasNext(), and the next() method obtains the content in the iterator.

 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() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        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];
        }
     ...
     
 }

Copy code

The general process is as follows:

The real exception throwing place is this detection method. When modCount and expectedModCount are not equal, exceptions are thrown directly. Let's see what modCount and expectedModCount are respectively. Here, modCount represents the modification times of ArrayList, while expectedModCount represents the modification times of iterator. When creating Itr iterator, modCount is assigned to expectedModCount. Therefore, in this example, modCount and expectedModCount are 4 at the beginning (four String elements are added). However, after obtaining the b element, the ArrayList performs the remove operation, so the modCount is accumulated to 5. Therefore, inconsistencies occur during inspection, which eventually leads to exceptions. Here we find the reason for throwing exceptions. The loop uses the iterator to loop, but the operation element uses the ArrayList operation. Therefore, the iterator finds that the element has been modified during the loop, so it throws an exception.

Let's think about it again. Why is there such a test? What role does this exception play? Let's start with the description of the comment of ConcurrentModificationException. The simple understanding is that one thread is not allowed to modify the collection and another thread is not allowed to iterate on the basis of the collection. Once this situation is detected, an exception will be thrown through the fast fail mechanism to prevent the following unknowable situation.

/**
 ***
 * For example, it is not generally permissible for one thread to modify a Collection
 * while another thread is iterating over it.  In general, the results of the
 * iteration are undefined under these circumstances.  Some Iterator
 * implementations (including those of all the general purpose collection implementations
 * provided by the JRE) may choose to throw this exception if this behavior is
 * detected.  Iterators that do this are known as <i>fail-fast</i> iterators,
 * as they fail quickly and cleanly, rather that risking arbitrary,
 * non-deterministic behavior at an undetermined time in the future.
 ***
**/
public class ConcurrentModificationException extends RuntimeException {
    ...
}

Copy code

Review the whole process

How to delete correctly

Since the reason for throwing the exception is that the iterator is recycled, and deleting the use of ArrayList causes the detection to fail. Then we will use iterators for recycling and iterators for deletion, so as to ensure consistency. Of course, we can also use the API provided in List.

public class TestListMain {

    public static void main(String[] args) {

        List<String> result = new ArrayList<>();
        result.add("a");
        result.add("b");
        result.add("c");
        result.add("d");

       Iterator<String> iterator = list.iterator();
 
		while (iterator .hasNext()) {
			String str = iterator.next();
			if ("b".equals(str)) {
				iterator.remove();
			}
    }
}

Copy code

summary

This paper mainly analyzes the source code of the exceptions in the element deletion of ArrayList in the for loop, which is also a common interview trap question during the interview. The interviewer examines the candidate's mastery of the JDK source code through such a seemingly simple question. Xiao Feng's interview continues. We look forward to what kind of trap interview questions he will encounter next time.

Topics: Java Back-end Interview