[Learning Notes-Java Collection-1] ArrayList Source Analysis

Posted by scottfossum on Thu, 08 Aug 2019 18:20:28 +0200

brief introduction

ArrayList is a List implemented as an array, which has the ability to dynamically expand compared to arrays, so it can also be called a dynamic array.

Inheritance System

  • ArrayList implements List, RandomAccess, Cloneable, java.io.Serializable and other interfaces.
  • ArrayList implements List and provides basic add, delete, traverse operations.
  • ArrayList implements RandomAccess, providing the ability to access randomly.
  • ArrayList implements Cloneable and can be cloned.
  • ArrayList implements Serializable and can be serialized.

Source Code Analysis

/**
 * The default capacity, which is 10, is the default capacity when created through the new ArrayList().
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * An empty array, if used when the incoming capacity is 0, is created with the new ArrayList(0).
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * Empty array, used when passing in capacity, restarted to default capacity size when adding the first element
 * This is created with the new ArrayList() using this empty array.
 * The difference from EMPTY_ELEMENTDATA is that when the first element is added, using this empty array initializes to DEFAULT_CAPACITY (10) elements.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * Array storing elements
 * Where elements are actually stored, transient s are used to not serialize this field.
 */
transient Object[] elementData; // non-private to simplify nested class access

/**
 * Number of elements in the collection
 * The number of elements actually stored, not the length of the elementData array.
 */
private int size;

ArrayList(int initialCapacity) construction method

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // If the initial capacity passed in is greater than 0, create a new array storage element
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // If the initial incoming capacity equals 0, use the empty array EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // Throw an exception if the initial capacity passed in is less than 0
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

ArrayList() construction method

public ArrayList() {
    // If no initial capacity is passed in, use the empty array DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    // Using this array expands to the default size of 10 when the first element is added
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

ArrayList construction method

/**
* Initialize the elements passed into the collection into the ArrayList
*/
public ArrayList(Collection<? extends E> c) {
    // Set to Array
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // Check that c.toArray() returns an Object [] type, and if not, re-copy it to Object[].class type
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // If c is an empty collection, it is initialized to an empty array EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

c.toArray() here; since the return may not be of type Object[], see the following code:

class MyList extends ArrayList<String> {
    /**
     * Subclass overrides parent class's method and returns can be different
     * But you can only use array types here, not Object
     * Should be a bug in java itself
     */
    @Override
    public String[] toArray() {
        // Write to death directly for convenience
        return new String[]{"1", "2", "3"};
    }
}

Add(E) method

Add elements to the end with an average time complexity of O(1).

public boolean add(E e) {
    // Check if expansion is required
    ensureCapacityInternal(size + 1);
    // Insert the element last
    elementData[size++] = e;
    return true;
}

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

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // If it is an empty array DEFAULTCAPACITY_EMPTY_ELEMENTDATA, it is initialized to the default size of 10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // The new capacity is 1.5 times the old capacity
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // If the new capacity is found to be smaller than the required capacity, the required capacity will prevail
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // If the new capacity exceeds the maximum capacity, use the maximum capacity
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // Copy out a new array with new capacity
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  1. Check if expansion is required;
  2. If elementData equals DEFAULTCAPACITY_EMPTY_ELEMENTDATA, the initialization capacity is DEFAULT_CAPACITY;
  3. The new capacity is 1.5 times the old capacity (oldCapacity + (oldCapacity >> 1). If you add so much capacity and find that it is smaller than what you need, the capacity you need is the one you need.
  4. Create a new capacity array and copy the old array to the new array;

add(int index, E element) method

Add elements to the specified location with an average time complexity of O(n).

public void add(int index, E element) {
    // Check if it is out of bounds
    rangeCheckForAdd(index);
    // Check if expansion is required
    ensureCapacityInternal(size + 1);
    // Move inex and the elements that follow it one place back, and the index position is empty
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // Insert element into index position
    elementData[index] = element;
    // Size increase 1
    size++;
}

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
  1. Check if the index is out of bounds;
  2. Check if expansion is required;
  3. Move the elements one place back after inserting them into the index position;
  4. Place the inserted element at the insertion index position;
  5. Size plus 1;

addAll method

Find the union of two sets.

/**
* Add all elements from collection c to the current ArrayList
*/
public boolean addAll(Collection<? extends E> c) {
    // Convert set c to array
    Object[] a = c.toArray();
    int numNew = a.length;
    // Check if expansion is required
    ensureCapacityInternal(size + numNew);
    // Copy all elements in c to the end of the array
    System.arraycopy(a, 0, elementData, size, numNew);
    // Size increases the size of c
    size += numNew;
    // Returns true if c is not empty, false otherwise
    return numNew != 0;
}

get(int index) method

Gets the element at the specified index location with a time complexity of O(1).

public E get(int index) {
    // Check if it is out of bounds
    rangeCheck(index);
    // Returns the element at the index position of the array
    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. Check if the index is out of bounds, here only check if it is out of bounds, if an IndexOutOfBoundsException exception is thrown over the upper bound, and if an ArrayIndexOutOfBoundsException exception is thrown over the lower bound.
  2. Returns the element at the index position;

remove(int index) method

Deletes the element at the specified index location with a time complexity of O(n).

public E remove(int index) {
    // Check if it is out of bounds
    rangeCheck(index);

    modCount++;
    // Get the element at index position
    E oldValue = elementData(index);

    // If index is not last, move the element after index forward one position
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);

    // Delete the last element to help the GC
    elementData[--size] = null; // clear to let GC do its work

    // Return Old Value
    return oldValue;
}

  1. Check if the index is out of bounds;
  2. Gets the element at the specified index position;
  3. If the last element is not deleted, the other elements move forward by one place.
  4. null the last position for GC recycling;
  5. Returns the deleted element.

As you can see, ArrayList deletes elements without indentation.

remove(Object o) method

Deletes an element of a specified element value with a time complexity of O(n).

public boolean remove(Object o) {
    if (o == null) {
        // Traverse the entire array, find where the element first appears, and delete it quickly
        for (int index = 0; index < size; index++)
            // If the element to be deleted is null, compare it with null, using ==
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // Traverse the entire array, find where the element first appears, and delete it quickly
        for (int index = 0; index < size; index++)
            // If the element to be deleted is not null, compare using the equals() method
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    // There is a missing cross-border check
    modCount++;
    // If index is not last, move the element after index forward one position
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    // Delete the last element to help the GC
    elementData[--size] = null; // clear to let GC do its work
}
  1. Find the first element equal to the specified element value;
  2. Quick deletion;

fastRemove(int index) has fewer operations to check index boundaries than remove(int index), so jdk optimizes performance to the extreme.

retainAll method

Find the intersection of two sets.

public boolean retainAll(Collection<?> c) {
    // Collection c cannot be null
    Objects.requireNonNull(c);
    // Call the bulk delete method, and complement passes in true, indicating deletion of elements not contained in c
    return batchRemove(c, true);
}

/**
* Bulk deletion of elements
* complement Delete elements not contained in c for true
* complement Delete elements contained in c for false
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    // Traversing the array simultaneously using two pointers, read and write
    // Each time the read pointer adds 1, the write pointer adds 1 when it places an element
    // This does not require any additional space, it just needs to operate on the original array
    int r = 0, w = 0;
    boolean modified = false;
    try {
        // Traverse the entire array, and if c contains the element, place it at the write pointer (complement prevails)
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Normally r ends up equal to size unless c.contains() throws an exception
        if (r != size) {
            // If c.contains() throws an exception, copy all unread elements after the write pointer
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // Leave the element after the write pointer empty to help the GC
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            // The new size is equal to the location of the write pointer (because every write pointer is added 1, the new size is exactly equal to the location of the write pointer)
            size = w;
            modified = true;
        }
    }
    // Return true with modifications
    return modified;
}
  1. Traverse the elementData array;
  2. If the element is in c, add it to the w position of the elementData array and move the w position one bit backward;
  3. After traversal, elements before w are common to both, and elements after w are not common to both;
  4. null elements after w for GC recycling;

removeAll

Finds the one-way difference between two sets, keeping only those elements that are not in c in the current set and not those that are not in the current set in c.

public boolean removeAll(Collection<?> c) {
    // Collection c cannot be empty
    Objects.requireNonNull(c);
    // Also call the bulk delete method, when complement passes in false, meaning delete the element contained in c
    return batchRemove(c, false);
}

summary

  1. ArrayList uses arrays to store elements internally. When the length of the arrays is insufficient, the ArrayList will not be scaled by adding half the space each time.
  2. ArrayList supports random access, accessing elements through an index is extremely fast, and the time complexity is O(1);
  3. ArrayList adds elements to the tail very quickly, with an average time complexity of O(1);
  4. ArrayList adds elements to the middle slowly because the average time complexity to move elements is O(n);
  5. ArrayList deletes elements from the tail very quickly with an O(1) time complexity;
  6. ArrayList deletes elements from the middle more slowly because the average time complexity to move elements is O(n);
  7. ArrayList supports union, call addAll (Collection<? Extends E> c) method;
  8. ArrayList supports intersection, call the retainAll (Collection<? Extends E> c) method;
  9. ArrayList supports one-way difference set, call removeAll (Collection<? Extends E> c) method;

Topics: Java less JDK