JDK 8 ArrayList source code parsing

Posted by soshea on Sun, 12 May 2019 06:24:37 +0200

[This article is to sort out the summary articles of knowledge and summarize some important points of knowledge that I think are relevant, in order to consolidate memory and technical exchanges and forget criticism and correction. It refers to a lot of previous articles, including pictures are also quoted, if offensive, invasive deletion. ]

0. Storage structure

From the bottom implementation point of view, Array is an array implementation, and unlike an array, its capacity can be changed. When the collection expands, it creates a larger array space and copies the original data into the new array. ArrayList supports fast random access to elements, but insertion and deletion are usually slow because the process is likely to require moving other elements.

 

The 1 Definition

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

2. Static constants

    // Default initial capacity size
    private static final int DEFAULT_CAPACITY = 10;

    // Empty arrays (for empty instances)
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // Shared empty array instances for default size empty instances.
    // We distinguish it from the EMPTY_ELEMENTDATA array to see how much additional capacity is required to add the first element.
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // The theoretical maximum that an array can allocate
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

3 attribute

    /**
     * An array of ArrayList data
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList Number of elements contained
     */
    private int size;

4. Constructor

Mainly used to initialize arrays.

    /**
     * Constructor with initial capacity parameter.
     */
    public ArrayList(int initialCapacity) {
        // Initial Capacity > 0, then directly create the corresponding length of the array
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // Create empty arrays using EMPTY_ELEMENTDATA representation
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Default constructor, denoted by DEFAULTCAPACITY_EMPTY_ELEMENTDATA as an empty array.
     * When the first element is added, an array with a length of 10 is actually allocated.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * In the order in which they are returned by the iterator of the set, a list of elements containing the specified set is constructed.
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        // If the number of specified collection elements is not zero
        if ((size = elementData.length) != 0) {
            // Determine whether c.toArray returns an array of Object type, if not, replicate with Arrays.copyOf
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // Replace empty arrays
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

 

5 Common methods

add method

  1. Ensure that the array can drop the element and expand if the capacity is insufficient.
  2. Appends the specified element to the end of the list
public boolean add(E e) {
        // To judge whether the expansion is needed, the group method is used to expand the capacity if the expansion is needed.
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // Appends the specified element to the end of the list
        elementData[size++] = e;
        return true;
    }

To determine whether expansion is needed, call the grow() method if expansion is needed

    // Get the Minimum Expansion Capacity
    private void ensureCapacityInternal(int minCapacity) {
        // ElementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA indicates that the current array length is the default length of 10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // See if the length of a new node is larger than the default length
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
    
    //Judging whether expansion is needed
    private void ensureExplicitCapacity(int minCapacity) {
        // New additions are structural modifications
        modCount++;
        
        // If it is larger than the current array length, it needs to be expanded
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

Capacity expansion method

  1. To determine the size of the new capacity, first expand to 1.5 times, if not enough, directly use the required capacity;
  2. If the required capacity exceeds the theoretically allocated maximum, the maximum capacity is used.
  3. Copy the array to a new capacity array.
    // ArrayList Expansion Method
    private void grow(int minCapacity) {
        // Old Capacity is the old capacity, and new Capacity is the new capacity.
        int oldCapacity = elementData.length;
        // The new capacity is 1.5 times the rescue capacity.
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // If the new capacity still does not meet the requirements, use minCapacity directly as the new capacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // If the new capacity is greater than the maximum capacity defined by ArrayList, set it to MAX_ARRAY_SIZE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // Copy to a new array using replicated Arrays.copyOf
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

Array replication method

Arrays.copyOf() method is sometimes used in the source code, and System.arraycopy() method is sometimes used for array replication. In fact, the bottom of the Arrays.copyOf() method is to call the System.arraycopy() method, while the System.arraycopy() method is a Native method.

Arrays.copyOf (T [] original, int new Length) method

Copy the specified original array to make it newLength in length.
Valid values in the original array will be included in the new array, and the extra part of the new array will be null.

The underlying implementation calls System.arraycopy(original, 0, copy, 0, Math. min (original. length, new Length);

System.arraycopy(Object src, int srcPos, Object dest, int destPos,int length) method

The original array srcPos is copied from the starting position of the original array srcPos to the destPos position of the destination array dest, and the length of the replication is length.

parameter Explain
src Original array
srcPos Initial position of primitive array
dest target array
destPos Starting position of target array
length Number of array elements to replicate
// Arrays.copy method source code
public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }

// The underlying layer uses the System.arraycopy() method to copy old array data to new arrays
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        // Declare a new array of length newLength
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        //
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

// Is a native method
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

get method

  1. Simply check if index is larger than array length.
  2. Returns the element directly.
    /**
     * Returns the element at the specified location in the ArrayList.
     */
    public E get(int index) {
        // Array boundary checking for index
        rangeCheck(index);
        // Elements that return directly to that location
        return elementData(index);
    }
    
    // Check only if it is larger than the array length
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

remove method

  1. Index boundary checking
  2. Self-Increasing Modifications
  3. Save the elements on index to oldValue
  4. Move all the elements on index one bit forward
  5. Empty the last element to allow the garbage collector to recycle
  6. Return the original value oldValue
// Delete the element at the specified location of the ArrayList
public E remove(int index) {
        // Boundary check
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);
        // Calculate the number of elements that need to be moved
        int numMoved = size - index - 1;
        // Move the element behind index one bit forward 
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // null the last position to help GC
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

remove specifies object methods

  1. There are two kinds of processing logic according to whether the object is empty or not.
  2. If the removal object is empty, the first null object is searched in turn, and then removed.
  3. If the removal object is not empty, it is searched sequentially and the same elements are found for removal.
// Remove the specified object
public boolean remove(Object o) {
        // There are two kinds of processing logic based on whether the object is empty or not.
        // Remove empty objects and find the first null object in turn
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
        // If the object is not empty, the relative elements are searched in turn for removal.
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

// Skipping checks directly removes elements at specified locations, the same logic as remove(int index)
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
    }

 

Best practices

  1. Estimate the required capacity of the scenario and set it to the initial value to prevent multiple scaling and affect performance (especially when the data volume is large).
  2.  

Reference

  1. https://zhuanlan.zhihu.com/p/34443888
  2. Code Out Efficient Java Development Manual

Topics: Java Attribute