Source code analysis of ArrayList based on jdk1.8

Keywords: less

Source code analysis of ArrayList based on jdk1.8

When we talk about ArrayList, we may immediately think of it as follows: orderly, repeatable, fast search, but slow add and delete, and unsafe thread. But the specific reasons are not very clear, this article will study with you according to these problems. It mainly analyzes the construction method of ArrayList, adding elements, deleting elements, getting elements, querying elements, clearing elements, judging whether elements exist and traversing ArrayList.

1: The implementation of ArrayList

1.1: constructor

ArrayList<> list = new ArrayList();
ArrayList<> list2 = new ArrayList(10);

1.2: member variable

   //Default initialized capacity
   private static final int DEFAULT_CAPACITY = 10;
   //Shared empty array instance for empty instance.
   private static final Object[] EMPTY_ELEMENTDATA = {};
   //Shared empty array instance for the default size empty instance. The difference is that this one has a default capacity
   private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
   //Array buffer to store elements of ArrayList. The capacity of ArrayList is the length of the array buffer. 
   //When any empty ArrayList with elementdata = = defaultaccess? Empty? Elementdata adds the first element,
   //Expand to default u capacity.
   transient Object[] elementData;
   //The size of the ArrayList (the number of elements it contains).
   private int size;

Resolution: when the object is serialized (write byte sequence to the target file), transient prevents the variables declared with this keyword from being persisted in the instance; when the object is deserialized (read byte sequence from the source file for reconstruction), such instance variable values will not be persisted and recovered.

1.3: concrete implementation of constructor
  //Method 1:
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
 //Method two: 
  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);
        }
    }

Parsing: the two methods are basically the same. The difference is that one has default capacity and the other is an empty array. From here, we can see that the bottom layer of ArrayList is realized by array, because the real storage element here is realized by elementData array.

  //Method three: (not commonly used)
  public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
     }
    

Parsing: we seldom use the third method, and we do a little parsing here. Here, we mainly get an object array through the Collection.toArray() method, then assign it to elementData, and initialize it by judging the length of the array. First, assign the length of the array to size, and then make a judgment. If the length of the array is not empty, then use Arrays.copyOf() to copy the elements in collection c to the elementData array. If it is empty, then the empty array of empty? elementData will be assigned to elementData. Let's take a look at the Arrays.copyOf() method.

 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @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;
    }

Parsing: This is mainly through the transfer of the array, and size to complete the process of copying from the old array to the new array. First, we will judge whether it is a new object or a reflection based on the Class type to create an object and place it in a new array. Next is the process of array copy. The source code of System.arraycopy() is as follows:

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

Parsing: this method will be used all the time. After understanding, we will study the following content. The specific implementation here is to copy the src array from the element with the subscript of srcPos, copy the length elements into the dest array, and add the elements into the dest array from the subscript of destPos. Note that both src and dest must be arrays of the same type or can be converted.

1.4: add elements

There are four kinds of added elements: we analyze them one by one.

arrayList.add(Object element); boolean
arrayList.add(int index, Object element); void
arrayList.addAll(Collection<? extends E> c); boolean
arrayList.addAll(int index,Collection<? extends E> c); boolean
1.4.1: arrayList.add(Object element) source resolution:
//Represents the minimum value of int
public static final int   MIN_VALUE = 0x80000000;
//Represents the maximum value of int
public static final int  MAX_VALUE = 0x7fffffff;
//The maximum array size to allocate.
//As an object, array needs a certain amount of memory to store the object header information. The maximum memory occupied by the object header information cannot exceed 8 bytes.
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;


public boolean add(E e) {
         //Assign the initial length to judge whether to expand. The current length is size+1 
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //New elements
        elementData[size++] = e;
        return true;
    }

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

 private static int calculateCapacity(Object[] elementData, int minCapacity) {
     //If the array length is 0, a large value will be selected between 10 and size+1
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

private void ensureExplicitCapacity(int minCapacity) {
//Record the number of changes (0 by default). Inconsistent iterations will trigger the fail fast mechanism. Therefore, you should use Iterator.remove() to delete elements during traversal
        modCount++;

        // overflow-conscious code
       //If the initial length is greater than the length of the element, you need to expand the capacity
        if (minCapacity - elementData.length > 0)
            //Capacity expansion
            grow(minCapacity);
    }

  private void grow(int minCapacity) {
        // overflow-conscious code
        //Old capacity
        int oldCapacity = elementData.length;
      	//New capacity = 1.5 times the old capacity
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //If the new capacity is less than the capacity specified at the beginning, assign the starting capacity to the new capacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //If the new capacity is greater than the maximum length of the array, specify the new capacity
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //Array conversion assignment, as mentioned above
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

  private static int hugeCapacity(int minCapacity) {
        //If the initial specified capacity is less than 0, an error is reported directly
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
       //If the initial specified capacity of a ternary expression is greater than the maximum length of the array, then the maximum value of int is selected, otherwise the maximum length of the array is selected
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
1.4.2: arrayList.add(int index, Object element) source resolution:
public void add(int index, E element) {
        //Determine whether the array subscript is out of range
        rangeCheckForAdd(index);
        //Assign the initial length to determine whether to expand the capacity. The current length is size+1 (it has been analyzed in detail above)
        ensureCapacityInternal(size + 1);  // Increments modCount!!
       //Move the size index elements of the original array from the index position to the next one to make room for the new elements
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //Assign a value to the vacated position
        elementData[index] = element;
        size++;
    }

 private void rangeCheckForAdd(int index) {
        //If the subscript is larger than the existing size of the array or the subscript is smaller than 0, an out of range subscript exception will be thrown
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

1.4.3: arraylist.addall (collection <? Extensions E > C) source resolution:
public boolean addAll(Collection<? extends E> c) {
        //Convert to array
        Object[] a = c.toArray();
        int numNew = a.length;
       //Assign the initial length to determine whether to expand the capacity. The current length is size+ numNew (which has been analyzed in detail above)
        ensureCapacityInternal(size + numNew);  // Increments modCount
    	//Add all elements in the collection to the end of this list
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

1.4.4: arraylist.addall (int index, collection <? Extends E > C) source resolution:
 public boolean addAll(int index, Collection<? extends E> c) {
        //Determine whether the subscript is out of range
        rangeCheckForAdd(index);
        //Convert to array
        Object[] a = c.toArray();
        int numNew = a.length;
     	// //Assign the initial length to determine whether to expand the capacity. The current length is size+ numNew (which has been analyzed in detail above)
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
     	//When the size of the array is larger than the index, move the last numMoved elements of the source array from the index position to the numNew bit
     	//Then the new array will occupy the position of index+numNew
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

To sum up: when adding data to arrayList, it will judge whether the subscript will cross the boundary when it comes to index. Under normal circumstances, it will judge whether to expand the capacity. If the capacity is expanded, it will expand 1.5 times each time. However, when the size of the newly expanded array has reached the maximum, it will take the maximum value.

1.5: delete element

There are three common methods to delete elements. We analyze them one by one:

arrayList.remove(Object o); boolean
arrayList.remove(int index); String
arrayList.removeAll(Collection<?> c); boolean
1.5.1: source resolution of arrayList.remove(Object o):
 public boolean remove(Object o) {
         //null can be stored in arrayList, so the following judgment is made
        //Deleted element is null
        if (o == null) {
            //Find the first element to delete,
            //Because arrayList can store the same elements, the first one is deleted
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            //Deleted element is not null
            for (int index = 0; index < size; index++)
                //Find the element to delete
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

  private void fastRemove(int index) {
        modCount++;
        //Judge the rationality of deleted elements
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //The assignment operation of an array moves all elements after the index position in the elementData of the array forward one bit
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //Give GC to process the empty data at the end
        elementData[--size] = null; // clear to let GC do its work
    }
1.5.2: source code resolution of arrayList.remove(int index):
  public E remove(int index) {
        //Check whether the subscript is out of range
        rangeCheck(index);
        modCount++;
        //Records return deleted elements
        E oldValue = elementData(index);
		//Check the rationality of deleting elements
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //Move all the elements after the index of the original array forward one bit
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //Give GC to process the empty data at the end
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
1.5.3: source resolution of arraylist.removeall (collection <? > C):
 //Delete all elements in arrayList that contain c
public boolean removeAll(Collection<?> c) {
        //Check if c is empty
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }

 public static <T> T requireNonNull(T obj) {
     //If it is null, it will directly throw null pointer
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

 private boolean batchRemove(Collection<?> c, boolean complement) {
        //Used to store filtered elements
        final Object[] elementData = this.elementData;
        //Double pointer, r for traversal, w for recording the number of filtered elements
        int r = 0, w = 0;
     	//It is used to record whether there is a change this time. If there is a change, it will return true
        boolean modified = false;
        try {
            for (; r < size; r++)
              //1: The parameter of composition passed in is false
              //2: If c contains the values in the array, then only r is increasing automatically. If c does not contain the values in the array, then both r and w are increasing automatically
              //3: It is a process of forward assignment.
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            //In general, r equals to size. When an exception occurs, r! = size will be caused
            //Then copy and overwrite all the data behind the exception to the array.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            //When w is not equal to size, data is deleted      
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    // w records the number of filtered elements, so later data can be set to null and submitted to GC for processing
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }
1.6: modifying elements
arrayList.set(int index, E element)
1.6.1: source code resolution of arraylist.set (int index, e element):
public E set(int index, E element) {
        //Check whether the subscript is out of range
        rangeCheck(index);
		//Store old elements
        E oldValue = elementData(index);
    	//Overwrite old value under specified subscript
        elementData[index] = element;
    	//Return old value
        return oldValue;
    }
1.7: get element
arrayList.get(int index);
1.7.1: source code resolution of arrayList.get(int index):
public E get(int index) {
        //Determine whether the subscript is out of range
        rangeCheck(index);
        return elementData(index);
    }

 @SuppressWarnings("unchecked")
    E elementData(int index) {
        //Returns the element of the specified subscript
        return (E) elementData[index];
    }
1.8: empty element
arrayList.clear();
1.8.1: arrayList.clear() source resolution:
 public void clear() {
        modCount++;
        // clear to let GC do its work
     //Set all elements to null, and then submit to GC for processing
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }
1.9: include specified elements
arrayList.contains(Object o);
1.9.1: arrayList.contains(Object o) source resolution:
public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

 public int indexOf(Object o) {
     	//For null
        if (o == null) {
            //Return after traversing to find a null subscript
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            //Not null, return after finding the specific subscript
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
     //Return - 1 without specifying an element
        return -1;
    }
    
1.10: iterator() method of arrayList
 arrayList.iterator();
1.10.1: source code resolution of arrayList.iterator() iterator:
 public Iterator<E> iterator() {
        return new Itr();
    }
 private class Itr implements Iterator<E> {
       //Itr implements the Iterator interface, and obtains the hasNext and next methods of the Iterator interface
       //Record the subscript of the next element
        int cursor;       // index of next element to return
       //Record the subscript of the last element
        int lastRet = -1; // index of last element returned; -1 if no such
     	//Record the number of changes
     	//When entering the iterator method of ArrayList, expectedModCount is initialized, but when the user is in the process
        //When add and remove are used, the value of modCount will be changed. The next time the value is not equal, a concurrent modification exception will be thrown
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            //If the subscript of the next element is not equal to size, it means there are still elements
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            //Check for concurrent modification exceptions
            checkForComodification();
            int i = cursor;
            //When i is greater than size or the length of array, it means that there is concurrent operation, which will throw concurrent modification exception
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            //Return the next element when there is no exception
            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();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
         //When the number of changes is inconsistent with the expected number of changes (the number of changes when calling the iterator method), concurrent modification exceptions will occur
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

2: Analysis of problems related to ArrayList

2.1: why is ArrayList fast to find, slow to add and delete?

Parsing: from the source code, we can see that when we find the corresponding subscript elements directly, we can get the specific elements and return them. However, adding elements may lead to the capacity expansion mechanism and involve the assignment operation of two sets, so it will slow down.

2.2: why is the thread unsafe in ArrayList?

Resolution: thread safety means that multiple threads access the same code (objects, variables, etc.) without uncertain results. But we can see through the source code interpretation that the ArrayList.add() method will produce uncertain results when it is used.

public boolean add(E e) {
         //Assign the initial length to judge whether to expand. The current length is size+1 
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //New elements
        elementData[size++] = e;
        return true;
    }

From the above, elementData[size++] = e may cause thread insecurity. This step can be divided into two steps

elementData[size] = e;
size = size + 1;

When multithreading begins to add elements, thread 1 begins to add element A At this time, thread 1 puts element A at the position with subscript 0, and thread 2 also adds element B when it comes in. When it comes in, it finds that there is no data at the position with subscript 0, and thread 2 puts the data at the position with subscript 0. Thread 1 and thread 2 continue their operation. Next, thread 1 will increase the size to 1, and when thread 2 comes in again, it will increase the size to 2, But at this time, the actual situation is that the size is 2, but the position with subscript 0 becomes B, and the position with subscript 1 is null, and the next adding element will start from the position with subscript 2. To sum up: ArrayList is thread unsafe.

2.3: how to avoid the thread insecurity of ArrayList.

Analysis:

The first method: using the Vertor set

 Vector<Object> arrayListSafe1 = new Vector<Object>();

The second method: use Collections.synchronizedList

List<String> list = new ArrayList<String>();
//Can be changed into
List<String> data=Collections.synchronizedList(new ArrayList<String>());

At this point, the source code research of ArrayList is over. Welcome to discuss and learn together.

For more dry cargo, welcome to my WeChat official account: Javanese sword.

Published 8 original articles, won praise 6, visited 10000+
Private letter follow

Posted by basim on Tue, 03 Mar 2020 02:08:25 -0800