Tear the bottom layer of ArrayList by hand and thoroughly analyze the source code

Posted by skippence on Sun, 30 Jan 2022 07:58:04 +0100

Recommended reading:

ArrayList overview

Hello, everyone. Today, let's introduce ArrayList. When it comes to ArrayList, many people know that its bottom layer is implemented by array, and the thread is unsafe. When it comes to its characteristics, they will say that it is fast to find and slow to add and delete, because everyone recites the interview questions. Let's talk about its underlying source code today.

ArrayList is more accurately implemented by dynamic arrays. The use of dynamic words here is to fully reflect its characteristics.

Moreover, ArrayList is not thread safe, so it is more efficient, but is this absolute? The answer is No.

ArrayList underlying source code

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 and implements RandomAccess, Cloneable and Serializable interfaces. RandomAccess enables it to access quickly.

(2) Cloneable is actually a tag interface. Only after implementing this interface, rewrite the clone method in the Object in the class, and then call the clone method through the class can cloning succeed. If this interface is not implemented, a clonnotsupportedexception (cloning is not supported) exception will be thrown.

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

(4) DEFAULT_ Capability is the default initialization set size of ArrayList.

(5) EMPTY_ELEMENTDATA is an empty object array, which is used to share empty array instances of empty instances.

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

(7) elementData is an array object used to store the current data.

(8) size is the size of the collection.

(9) When the elements in the set exceed the specified length of the array, the array will be expanded. The expansion operation is the reason for the slow storage operation of ArrayList. 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 very simple. It directly judges the size of the value passed in. If it is greater than zero, it directly initializes an array object of this length and assigns it to elementData. If it is equal to zero, empty the empty array object_ elementData is assigned to elementData. Otherwise, an exception is thrown directly.

(2) This constructor is generally used when initializing a collection with a large amount of data.

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

(1) Set defaultcapability_ EMPTY_ elementData null array object assigned 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;
    }
}

Two main things have been done here:
(1) First convert the set c into an array, and then assign it to the elementData array object.

(2) Then judge whether the size and are equal and not equal to 0. If yes, assign the data and re assign it to the array object elementData. Otherwise, assign the empty array object to elementData directly.

Source code analysis of ArrayList method

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

(1) Execute the ensureCapacityInternal method to judge whether the original array object needs to be expanded.

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

Next, let's take a look at the source code of the ensureCapacityInternal method.

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

In ensureCapacityInternal, we call the ensureExplicitCapacity method and the calculateCapacity method. 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 capacity. First judge whether the elementData array object has an initialization size. If not, take default_ The larger of capacity or minCapacity is the capacity. If it has been initialized, minCapacity is the capacity.

Next, let's take a look at the source code of ensureExplicitCapacity:

private void ensureExplicitCapacity(int minCapacity) {

    modCount++;

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

(1) Execute the auto increment of modCount. modCount is the number of times the current list structure has been modified.

(2) Judge if minCapacity is greater than elementdata Length will be expanded. Otherwise, exit this method directly and add elements.

Next, let's look at the source code of the grow 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, first get the length of the original data elementData and assign it to a variable oldCapacity, then expand the original length by 1.5 times and pay it to oldCapacity.

(2) Judge whether minCapacity is greater than newCapacity. If it is established, assign minCapacity to newCapacity. Why do you do this? Because from the analysis of the previous layer by layer method, minCapacity is the minimum length allowed after capacity expansion, that is, the minimum length of the actual stored data. If the length after capacity expansion is smaller than minCapacity, you can only use minCapacity as the length of the container.

(3) Then judge whether the new length newCapacity of the container is greater than the maximum length Max allowed by the container_ ARRAY_ Size, set the expansion length to the maximum available length.

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

Next, let's take a look at the source code of hugeCapacity called by the grow th 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) Directly judge whether minCapacity is less than zero, throw an exception, and then compare whether the minimum length allowed by the container is greater than MAX_ARRAY_SIZE, if true, 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 judge whether the subscript is out of bounds. If so, an IndexOutOfBoundsException exception will be thrown.

(2) Then judge whether to expand the capacity. This method is the same as the above. It has been said and will not be repeated.

(3) Finally, move the object after executing the array object index one bit back and add the element to the specified position.

Next, let's take a look at the source code of rangeCheckForAdd

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

(1) Directly determine the index > size or index < 0 condition, and throw the array subscript out of bounds exception directly when it is true.

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) addAll(Collection c) method directly calls addAll(this.size, c). The first thing in addAll(this.size, c) is to judge whether the subscript is out of bounds.

(2) Then judge whether the size of c is greater than 0. If it is equal to 0, return false.

(3) Check whether the number of modifications is equal. If not, a concurrent modificationexception (concurrent modification) exception will be thrown directly. That is, this error will occur when we use the iterator to cycle the list and add / delete elements with the list method.

(4) Insert the element into the array, assign the modification times to modCount, and finally increase the size by one

(5) During the add operation, first judge whether the subscript is out of bounds and whether it needs to be expanded. If it needs to be expanded, copy the array and expand by half by default. If the expansion is not enough, use the target size as the 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 very simple. First, judge whether the subscript is out of bounds, throw an exception when it is out of bounds, and finally 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 judge whether it is out of bounds, then take the value of the original index position as oldValue, 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) Judge whether it is out of bounds, and then increase the modification times modCount value by 1, and then obtain the old value of the original index position.

(2) Then calculate the number of elements after the index position, move the element after the index position forward one bit, and finally 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, judge whether the o object is null. If it is null, traverse the elements in the collection. If there is a null value, execute the deletion. The method of deleting the specified object is fastRemove. The principle is to calculate the number of elements after the index position, and then move the elements after the index forward by one bit, Finally, the last bit is assigned a null value.

(2) If the logic executed is the same when the O object is a non null object, why write it separately? It is very simple, because it needs to call the o.equals(elementData[index] method to judge. If it is null, it will not 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) There are several important attributes in the iterator. int cursor is the index of the next element to be returned, and int lastRet = -1 - is the index of the last element to be returned. The default is - 1, which is not the case.

(2) The hasnext method determines whether the next element exists by determining whether the following subscript is the size of the array.

(3) The next method gets the next element. First, call the checkForComodification method to check whether the number of modifications is consistent, then define the subscript of the next element and judge the subscript. If the subscript is greater than the number of elements contained in the ArrayList, throw NoSuchElementException (no such element exception), and then get the elementData data object in the ArrayList, Judge the subscript again. If the judgment is inconsistent this time, it indicates that the array has been modified. Finally, cursor +1 points to the subscript of the next element. Finally, lastRet is defined as the subscript of the returned element, and then the value corresponding to the subscript is returned.

summary

This interview question contains almost all the interview questions and answers he encountered in a year, even including the detailed dialogue and quotations in the interview. It can be said that the details are to the extreme, and even the optimization of resume and how to submit resume are easier to get interview opportunities! It also includes teaching you how to get the push quota of some big factories, such as Alibaba and Tencent!

A celebrity said that success depends on 99% sweat and 1% opportunity, and if you want to get that 1% opportunity, you have to pay 99% sweat first! Only if you persevere towards your goal step by step can you have a chance to succeed!

Success will only be left to those who are prepared! Free access to information: stamp here

mentException (there is no such element exception). Then get the elementData data data object in the ArrayList and judge the subscript again. If the judgment is inconsistent this time, it means that the array has been modified. Finally, point cursor +1 to the subscript of the next element, and finally define lastRet as the subscript of the returned element, and then return the value corresponding to the subscript.

summary

This interview question contains almost all the interview questions and answers he encountered in a year, even including the detailed dialogue and quotations in the interview. It can be said that the details are to the extreme, and even the optimization of resume and how to submit resume are easier to get interview opportunities! It also includes teaching you how to get the push quota of some big factories, such as Alibaba and Tencent!

A celebrity said that success depends on 99% sweat and 1% opportunity, and if you want to get that 1% opportunity, you have to pay 99% sweat first! Only if you persevere towards your goal step by step can you have a chance to succeed!

Success will only be left to those who are prepared! Free access to information: stamp here

Topics: Java Interview Programmer