Analysis of ArrayList implementation of Qianfeng Chongqing Java learning and sharing -- common operations

The previous article [ArrayList implementation analysis (I) - object creation] mainly introduced the implementation principles of three methods of ArrayList object creation. The following focuses on the implementation principles of common operations provided by ArrayList.

Add element
ArrayList mainly provides two kinds of methods for adding elements: adding a single element and adding multiple elements:

The following two methods are to add a single element

//Append a new element at the end of ArrayList e

public boolean add(E e)

//Add a new element at the position pointed to by the index index, and the element at the index position in the original List and its subsequent elements move to the right

public void add(int index, E element)

Let's look at the implementation logic of add (E):

public boolean add(E e) {

    // Judge whether the elementData array needs to be expanded, and add 1 to the modCount field

    ensureCapacityInternal(size + 1);  

    //Add the new value E to the last bit of the elementData array

    elementData[size++] = e;

    return true;

}
First, you need to call ensureCapacityInternal to determine whether the elementData array needs to be expanded. The specific implementation is as follows:

private void ensureCapacityInternal(int minCapacity) {

    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));

}
The first constructor means to create an empty ArrayList, the second constructor means to create an ArrayList with an initial size of initialCapacity, and the third constructor means to use a collection object to create an ArrayList object.

No matter which constructor is called, elementData is used internally to point to the data elements stored in ArrayList.

ArrayList add update element
The ArrayList list provides the add method to add elements. The add method has four overloaded methods:

public boolean add(E e)

public void add(int index, E element)

public boolean addAll(Collection<? extends E> c)

public boolean addAll(int index, Collection<? extends E> c)
These four overloaded methods are divided into two categories: one is to add elements to the end of the ArrayList queue, and the other is to add elements to the index position specified by the queue. First, let's look at the specific implementation of the first add method:

public boolean add(E e) {

    // Judge whether the elementData array needs to be expanded, and add 1 to the modCount field

    ensureCapacityInternal(size + 1);  

    //Add the new value E to the last bit of the elementData array

    elementData[size++] = e;

    return true;

}

The implementation of ensureCapacityInternal method is as follows:

private void ensureCapacityInternal(int minCapacity) {

    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));

}

private static int calculateCapacity(Object[] elementData, int minCapacity) {

   //If elementData is empty, the default value and the maximum value of minCapacity are returned

    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

        return Math.max(DEFAULT_CAPACITY, minCapacity);

    }

    return minCapacity;

}

private void ensureExplicitCapacity(int minCapacity) {

    modCount++;

    // overflow-conscious code

    if (minCapacity - elementData.length > 0)

        grow(minCapacity);

}
As can be seen from the above source code, first execute the calculateCapacity method to obtain the size of the list after adding new elements, and then execute ensureExplicitCapacity to expand the array size. Here, you need to add one to modCount and record the number of changes of the list object. Then judge whether the size of the current array can accommodate new data. If not, you need to align the array

elementData for capacity expansion. Let's focus on the implementation principle of grow th:

private void grow(int minCapacity) {

    // Overflow conscious code note that the following may overflow

    int oldCapacity = elementData.length;

    int newCapacity = oldCapacity + (oldCapacity >> 1);

    if (newCapacity - minCapacity < 0)

        newCapacity = minCapacity;

    if (newCapacity - MAX_ARRAY_SIZE > 0)

        newCapacity = hugeCapacity(minCapacity);

    elementData = Arrays.copyOf(elementData, newCapacity);

}
Algorithm for expanding the array: first, calculate the size of the new array for the first time through the old array (newCapacity) = old array size + old array size / 2. When the expanded newCapacity is still less than minCapacity, execute newCapacity=minCapacity to make the size of the new array equal to minCapacity. If newCapacity is greater than MAX_ARRAY_SIZE, execute hugeCapacity to find the largest newCapacity, then use Arrays.copy to copy the old array data into the new array. The size of the new array is newCapacity, use elementData to point to the new array, and finally complete the array expansion.

Note: MAX_ARRAY_SIZE is the maximum allocable size of the array. Some VM S reserve an 8-byte array header for the array, so MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8

Next, the logic of public void add(int index, E element) is basically the same as that of the add method. The add method is to add elements to the end of the queue. This method is to add elements to the specified index position, and move all the data in the original index back one position. The specific implementation is as follows:

public void add(int index, E element) {

    //Judge the validity of the index

    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  

    //Move all elements after index backward

    System.arraycopy(elementData, index, elementData, index + 1,

                     size - index);

    elementData[index] = element;

    size++;

}
rangeCheckForAdd will judge the validity of the index. Although ArrayList is a collection object that can change dynamically, it does not allow new data to be inserted where the size of ArrayList exceeds.

private void rangeCheckForAdd(int index) {

    if (index > size || index < 0)

        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

}
The ensureCapacityInternal method is implemented as described above to expand the data capacity. Then call System.arraycopy to move all the data after index in the extended elementData to the right, and finally assign the new original to index position.

The following describes the method of batch adding elements to implement addAll:

public boolean addAll(Collection<? extends E> c) {

    Object[] a = c.toArray();

    int numNew = a.length;

    ensureCapacityInternal(size + numNew);  // Increments modCount

    System.arraycopy(a, 0, elementData, size, numNew);

    size += numNew;

    return numNew != 0;

}

public boolean addAll(int index, Collection<? extends E> c) {

    rangeCheckForAdd(index);

    Object[] a = c.toArray();

    int numNew = a.length;

    ensureCapacityInternal(size + numNew);  // Increments modCount

    int numMoved = size - index;

    if (numMoved > 0)

        System.arraycopy(elementData, index, elementData, index + numNew,

                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);

    size += numNew;

    return numNew != 0;

}
The implementation method of addAll is similar to that of add. Both of them need to extend the data from securecapacityinternal. If it is addAll (int index, collection <? Extensions E > C), move the element after the index to the right of elementData by numNew size. According to the source code analysis, add(index,e) and addAll(index,c) operations should be avoided as much as possible, because there will be a large number of data movement operations.

The following describes the set(int index, E element) method. The function of this method is to replace the old element at the index position in the ArrayList with the new element element, and return the old element. The implementation is as follows:

public E set(int index, E element) {

    rangeCheck(index);

    E oldValue = elementData(index);

    elementData[index] = element;

    return oldValue;

}
According to the source code, the implementation of set method is relatively simple.

The following describes the sort method, which sorts the list through the passed Comparator object:

public void sort(Comparator<? super E> c) {

    final int expectedModCount = modCount;

    Arrays.sort((E[]) elementData, 0, size, c);

    if (modCount != expectedModCount) {

        throw new ConcurrentModificationException();

    }

    modCount++;

}

In the sort method, first obtain the current modCount value, and then call the Arrays.sort method to sort the original array in the list according to the requirements of the Comparator. Before sort returns, check whether the list has been modified by other threads. If other threads have modified the list, throw the
ConcurrentModificationException exception. Finally, modify modCount.

The following describes the lastIndexOf method and indexOf method. LastIndexOf is the last index value of the current element in the list, and indexOf is the index value of the current element in the list.

public int lastIndexOf(Object o) {

    if (o == null) {

        for (int i = size-1; i >= 0; i--)

            if (elementData[i]==null)

                return i;

    } else {

        for (int i = size-1; i >= 0; i--)

            if (o.equals(elementData[i]))

                return i;

    }

    return -1;

}

public int indexOf(Object o) {

    if (o == null) {

        for (int i = 0; i < size; i++)

            if (elementData[i]==null)

                return i;

    } else {

        for (int i = 0; i < size; i++)

            if (o.equals(elementData[i]))

                return i;

    }

    return -1;

}

From the above source code, if it is the lastIndexOf method, traverse the list from back to front; if it is the indexOf method, traverse the list from front to back.

Another important method is retainAll (collection <? > c). This method is used to delete the elements not contained in c from the current list, that is, to obtain the intersection with c. the specific implementation is as follows:

public boolean retainAll(Collection<?> c) {

    Objects.requireNonNull(c);

    return batchRemove(c, true);

}

private boolean batchRemove(Collection<?> c, boolean complement) {

    final Object[] elementData = this.elementData;

    int r = 0, w = 0;

    boolean modified = false;

    try {

        for (; r < size; r++)

            if (c.contains(elementData[r]) == complement)

                elementData[w++] = elementData[r];

    } finally {

        if (r != size) {

            System.arraycopy(elementData, r,

                             elementData, w,

                             size - r);

            w += size - r;

        }

        if (w != size) {

            for (int i = w; i < size; i++)

                elementData[i] = null;

            modCount += size - w;

            size = w;

            modified = true;

        }

    }

    return modified;

}
It can be seen from the above source code that the implementation of retainAll core is in the batchremove method, which is used in two places in ArrayList: removeAll (collection <? > c) and retainAll (collection <? > c). Batchremove has two parameters. The first parameter c is list and the second parameter is Boolean. When used in removeAll, false is passed in, indicating that all elements in c need to be deleted from the current ArrayList. The batchRemove invoked in the retainAll method is true, which means that the elements that appear in c need to be preserved. The core codes are listed below,

for (; r < size; r++)

            if (c.contains(elementData[r]) == complement)

                elementData[w++] = elementData[r];

The above code is to delete the elementData according to the value of complex, through elementData[w++] = elementData[r]; Place the elements to be retained to the left of the elementData array. The following code is that when the list is operated by other threads, the current ArrayList increases the amount of data and appends the new elements to the reserved data elements.

if (r != size) {

            System.arraycopy(elementData, r,

                             elementData, w,

                             size - r);

            w += size - r;

}
Then assign null to the location of the element to be deleted in elementData. This enables GC to clean up garbage data when appropriate.

Tags: Java

Posted on Sat, 09 Oct 2021 02:33:51 -0400 by weezil