Tear the bottom of the ArrayList by hand to thoroughly analyze the source code

Keywords: Java less

Overview of ArrayList

Hello, I'm going to talk about ArrayList today. When it comes to ArrayList, many people know that the bottom of it is implemented using arrays. Threads are not safe. When it comes to its features, it will say that search is fast and increase or decrease slowly, because interview questions are all reciprocal.Let's talk about its underlying source today.

ArrayList is more precisely implemented by dynamic arrays, where the dynamic word is used to fully characterize it.

Second, ArrayList is not thread-safe, so it's more efficient, but is it absolute?The answer is No.

ArrayList underlying source

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

private static final long serialVersionUID = 8683452581122892189L;

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

transient Object[] elementData; // non-private to simplify nested class access

private int size;

}

(1) ArrayList inherits the AbstractList Abstract class, implements RandomAccess, Cloneable, Serializable interfaces, and enables RandomAccess to provide fast access.

(2) Cloneable is actually a markup interface, which must be implemented, then the clone method in the Object is overridden in the class, and then the clone method is called through the class to clone successfully. If this interface is not implemented, the CloneNotSupportedException exception will be thrown.

(3) Serializable is a serialization interface that supports serialization and deserialization.

(4) DEFAULT_CAPACITY is the size of the default initialization set for ArrayList.

(5) EMPTY_ELEMENTDATA is an empty object array used for shared empty array instances of empty instances.

(6) DEFAULTCAPACITY_EMPTY_ELEMENTDATA is used when creating a collection using the default constructor

(7) Array objects used by elementData to hold current data.

(8) size is the size of the collection.

(9) When the elements in a collection exceed the specified length of the array, the array will be expanded, which is the reason why ArrayList storage is slow, especially when the amount of data is large, each expansion will consume more and more time.

ArrayList construction method source code

ArrayList(int initialCapacity)
public ArrayList(int initialCapacity) {

    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    }
}

(1) The constructor is simple. It directly determines the size of the incoming value. If it is greater than zero, it directly starts with an array object of that length and assigns it to elementData. If it is equal to zero, it assigns the empty array object EMPTY_ELEMENTDATA to elementData. Otherwise, it throws an exception directly.

(2) This constructor is typically used when initializing a collection of large amounts of data.

ArrayList()
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

(1) Assign the DEFAULTCAPACITY_EMPTY_ELEMENTDATA empty array object to elementData

ArrayList(Collection c)
public ArrayList(Collection<? extends E> c) {

    elementData = c.toArray();

    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

There are two main things to do here:
(1) Convert the set c into an array and assign it to the elementData array object.

(2) Then determine if the size and the value are equal and not equal to 0, then perform the assignment of the data and reassign it to the array object elementData, otherwise assign the empty array object to the elementData directly.

Method Source Analysis for ArrayList

add() method
public boolean add(E e) {
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}

(1) Perform the ensureCapacityInternal method to determine whether the original array object needs to be expanded.

(2) Add the e object to the elementData array object.

Next, let's look at the source code for the ensureCapacityInternal method.

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

The ensureExplicitCapacity method and the calculateCapacity method were called in ensureCapacityInternal. Let's look at the calculateCapacity method

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

    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

(1) The main task here is to calculate the size of the capacity, first determine if the elementData array object has an initialization size, if not, take the larger of DEFAULT_CAPACITY or minCapacit as the size of the capacity, or minCapacity as the size of the capacity if it has already been initialized.

Next, look at the source code for ensureExplicitCapacity:

private void ensureExplicitCapacity(int minCapacity) {

    modCount++;

    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

(1) Executing modCount increases by itself, modCount is the number of times the current list structure has been modified.

(2) Judge that if minCapacity is greater than elementData.length, perform the expansion; otherwise, exit the method directly and add elements.

Next, let's look at the source code for the group method:

private void grow(int minCapacity) {

    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);
}

(1) Here you get the length of the original elementData and assign it to a variable, oldCapacity, then expand the original length by 1.5 times and pay oldCapacity.

(2) To determine if minCapacity is greater than newCapacity, then assign minCapacity to newCapacity. Why?Because after the previous layer method has been parsed, minCapacity is the minimum length allowed after expansion, that is, the minimum length of data actually stored. If you have a smaller length after expansion than minCapacity, you can only use minCapacity as the length of the container.

(3) Then determine if the new length of the container, newCapacity, is greater than the maximum length allowed by the container, MAX_ARRAY_SIZE, and set the expanded length to the maximum available length.

(4) Copy, expand, and construct a new array.

Next, let's look at the source of the hugeCapacity called by the group method:

private static int hugeCapacity(int minCapacity) {

    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();

    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

(1) To directly determine if minCapacity is less than zero, throw an exception, and then compare if the minimum length allowed by the container is greater than MAX_ARRAY_SIZE, then assign the maximum value of Integer to minCapacity as the maximum length of the container.

add(int index, E element) method
public void add(int index, E element) {

    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!

    System.arraycopy(elementData, index, elementData, index + 1,size - index);

    elementData[index] = element;
    size++;
}

(1) There are three main things to do here. The first is to determine if the subscript is out of bounds and throw the IndexOutOfBoundsException exception if it is.

(2) Then it is to determine whether expansion is necessary. This method, like the one above, has already been said and will not be repeated.

(3) Finally, after index of the array object, the object is moved one bit backward to add the element to the specified location.

Next let's look at the source code for rangeCheckForAdd

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

(1) Direct judgment of index > size or index < 0 throws the array subscript out of bounds exception when it is established.

addAll(Collection c) method
public boolean addAll(Collection<? extends E> c) {
    return addAll(this.size, c);
}

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

    rangeCheckForAdd(index);

    int cSize = c.size();

    if (cSize==0)
        return false;

    checkForComodification();

    parent.addAll(parentOffset + index, c);

    this.modCount = parent.modCount;

    this.size += cSize;
    return true;
}

private void checkForComodification() {
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
}

(1) In the addAll (Collection c) method, addAll(this.size, c) is called directly. The first thing in addAll(this.size, c) is to determine if the subscript is out of bounds.

(2) Then determine if the size of c is greater than 0 and return false if it is equal to 0.

(3) Check whether the number of modifications is equal, or throw the ConcurrentModificationException exception directly if they are not equal. This error occurs when we loop a list with an iterator and add/delete elements in it using the list method.

(4) Insert elements into the array, assign modCount the number of modifications, and add one size to the final size

(5) In the add operation, first determine if the subscript is out of bounds and needs expansion. If expansion is needed, copy the array, default expansion is half. If half expansion is not enough, use the target size as capacity after expansion, and then set the corresponding subscript element value.

get() method
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

E elementData(int index) {
    return (E) elementData[index];
}

(1) This is a simple and direct way to determine if the subscript is out of bounds, throw an exception if it is out of bounds, and return the element value at the specified index position.

set() method
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

(1) First determine if the value is out of bounds, then take out the old value at the original index position, set the new value element to the index position, and finally return the old value oldValue.

remove() method
public E remove(int index) {
    rangeCheck(index);

    modCount++;

    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);

    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

(1) Determine if it is out of bounds, then add the modCount value of the number of modifications to 1 and get the old value of the original index position.

(2) Then calculate how many elements are behind the index position, move the element one bit forward after the index position, and return the old value.

remove(Object o) method
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

(1)This method of deleting objects is relatively simple. First, determine if the o object is a null object, then iterate through the elements in the set for null, whether there is a null value, and then perform deletion. The method of deleting the specified object is fastRemove. The principle is to calculate the number of elements after the index position, then move the elements after the index one bit forward, and finally assign the last one to null.Value.

(2) If the O object is not a null object and the logic is the same, why write it separately?It's simple, because it calls the o.equals(elementData[index] method to make a judgment, and if it's null, it doesn't report a null pointer exception.

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

private class Itr implements Iterator<E> {
    int cursor; 
    int lastRet = -1;
    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];
    }

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

(1) Several properties in the iterator are important, int cursor is the index of the next element to be returned, int lastRet = -1 is the index of the last element returned, defaulting to -1, which is the case where none exists.

(2) The hasNext method determines whether the next element exists and whether the following subscript is the size of the array.

(3) The next method takes the next element, first calls the checkForComodification method to check if the number of modifications is consistent, then defines the subscript of the next element to determine if the subscript is greater than the number of elements contained in the ArrayList, throws the NoSuchElementException(No such element exception) Exception, then get the elementData data object in ArrayList, and judge the subscript again. If this judgment is inconsistent, it means that the array has been modified, cursor +1 is pointed to the subscript of the next element, lastRet is defined as the subscript of the returned element, and the corresponding value of the subscript is returned.

(4) remove removes the current element, first determines if the subscript lastRet of the last element is less than 0, then does not exist, throws an exception, then calls checkForComodification to determine if the modifications are consistent, then calls the remove method of ArrayList, and finally updates the values of cursor, lastRet, expectedModCount.

Posted by suresh_nsnguys on Wed, 26 Feb 2020 08:23:05 -0800