7

1 Overview

This article mainly describes the similarities and differences between ArrayList and LinkedList , as well as the underlying implementation of the two (environment OpenJDK 11.0.12 ).

2 The difference between the two

Before introducing the underlying implementation of the two in detail, let's take a brief look at the similarities and differences between the two.

2.1 Similarities

  • Both implement the List interface, and both inherit AbstractList ( LinkedList indirect inheritance, ArrayList direct inheritance)
  • Both are thread unsafe, both are asynchronous
  • Both have addition, deletion, search and modification methods

2.2 Differences

  • The underlying data structures are different: ArrayList based on Object[] array, LinkedList based on LinkedList.Node doubly linked list
  • The random access efficiency is different: ArrayList random access can achieve O(1) (implement RandomAccess ), because the element can be found directly through the subscript, while LinkedList needs to be traversed from the head pointer, and the time is O(n)
  • The initialization operation is different: ArrayList is initialized to an empty array when initialized with no parameters, while LinkedList does not need
  • Expansion: ArrayList will expand when the length is not enough to accommodate new elements, while LinkedList will not
  • Memory space occupation: The memory space occupation of ArrayList is mainly reflected in that a certain capacity space will be reserved at the end of the array, while the space cost of LinkedList is mainly reflected in that each element needs to consume more space than ArrayList

3 ArrayList bottom layer

3.1 Basic structure

The bottom layer is implemented using Object[] array, and the member variables are as follows:

private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Object[] elementData;
private int size;
private static final int MAX_ARRAY_SIZE = 2147483639;

The default initialized capacity is 10, followed by two empty arrays for use by the default constructor and constructors with initialized capacity:

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

        this.elementData = EMPTY_ELEMENTDATA;
    }
}

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

Here are some more important methods, including:

  • add()
  • remove()
  • indexOf()/lastIndexOf()/contains()

3.2 add()

add() There are four methods:

  • add(E e)
  • add(int index,E e)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c

3.2.1 Single element add()

Let's take a look at add(E e) and add(int index,E eelment) first:

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length) {
        elementData = this.grow();
    }

    elementData[s] = e;
    this.size = s + 1;
}

public boolean add(E e) {
    ++this.modCount;
    this.add(e, this.elementData, this.size);
    return true;
}

public void add(int index, E element) {
    this.rangeCheckForAdd(index);
    ++this.modCount;
    int s;
    Object[] elementData;
    if ((s = this.size) == (elementData = this.elementData).length) {
        elementData = this.grow();
    }

    System.arraycopy(elementData, index, elementData, index + 1, s - index);
    elementData[index] = element;
    this.size = s + 1;
}

add(E e) actually calls a private method. After judging whether expansion is required, add it directly to the end. And add(int index,E element) will first check whether the subscript is legal, and if it is legal, then determine whether it needs to be expanded, then call System.arraycopy to copy the array, and finally assign the value and add 1 to the length.

Regarding System.arraycopy , the official documents are as follows:

在这里插入图片描述

There are a total of 5 parameters:

  • The first parameter: the original array
  • The second parameter: the position where the original array needs to start copying
  • The third parameter: the target array
  • Fourth parameter: copy to the beginning of the target array
  • The fifth parameter: the number of copies to be copied

That is:

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

The function is to "move backwards" the elements of the original array behind index , freeing up a position for index to insert.

3.2.2 addAll()

Let's take a look at the two addAll() :

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    ++this.modCount;
    int numNew = a.length;
    if (numNew == 0) {
        return false;
    } else {
        Object[] elementData;
        int s;
        if (numNew > (elementData = this.elementData).length - (s = this.size)) {
            elementData = this.grow(s + numNew);
        }

        System.arraycopy(a, 0, elementData, s, numNew);
        this.size = s + numNew;
        return true;
    }
}

public boolean addAll(int index, Collection<? extends E> c) {
    this.rangeCheckForAdd(index);
    Object[] a = c.toArray();
    ++this.modCount;
    int numNew = a.length;
    if (numNew == 0) {
        return false;
    } else {
        Object[] elementData;
        int s;
        if (numNew > (elementData = this.elementData).length - (s = this.size)) {
            elementData = this.grow(s + numNew);
        }

        int numMoved = s - index;
        if (numMoved > 0) {
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
        }

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

In the first addAll , first determine whether expansion is required, and then directly call the target set to add to the tail. In the second addAll , due to an additional subscript parameter, the processing steps are slightly more:

  • First determine whether the subscript is legal
  • Then decide if you need to expand
  • Then calculate whether it is necessary to "move backward" the original array elements, that is, if in System.arraycopy
  • Finally, copy the target array to the specified index location

3.2.3 Expansion

The above add() methods all involve expansion, which is the grow method. Let's take a look:

private Object[] grow(int minCapacity) {
    return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(minCapacity));
}

private Object[] grow() {
    return this.grow(this.size + 1);
}

private int newCapacity(int minCapacity) {
    int oldCapacity = this.elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(10, minCapacity);
        } else if (minCapacity < 0) {
            throw new OutOfMemoryError();
        } else {
            return minCapacity;
        }
    } else {
        return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity);
    }
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) {
        throw new OutOfMemoryError();
    } else {
        return minCapacity > 2147483639 ? 2147483647 : 2147483639;
    }
}

grow() first calculates the capacity to be expanded through newCapacity , then calls Arrays.copyOf to copy the old elements, and overwrites the return value to the original array. And in newCapacity , there are two variables:

  • newCapacity : The new capacity, the default is 1.5 times the old capacity, that is, the default expansion is 1.5 times
  • minCapacity : Minimum required capacity

If the minimum capacity is greater than or equal to the new capacity, it is one of the following:

  • Array initialized by default construction: returns minCapacity with the max of 10
  • Overflow: throw OOM directly
  • Otherwise return the minimum capacity value

If not, judge whether the new capacity has reached the maximum value (here is a little curious why MAX_ARRAY_SIZE is not used, guess it is a problem of decompilation), if it does not reach the maximum value, return the new capacity, if the maximum value is reached, call hugeCapacity .

hugeCapacity will also first judge whether the minimum capacity is less than 0, and throw OOM if it is less than, otherwise it will be Integer.MAX_VALUE with the maximum value ( MAX_ARRAY_SIZE MAX_ARRAY_SIZE .

3.3 remove()

remove() contains four methods:

  • remove(int index)
  • remove(Object o)
  • removeAll()
  • removeIf()

3.3.1 Single element remove()

That is, remove(int index) and remove(Object o) :

public E remove(int index) {
    Objects.checkIndex(index, this.size);
    Object[] es = this.elementData;
    E oldValue = es[index];
    this.fastRemove(es, index);
    return oldValue;
}

public boolean remove(Object o) {
    Object[] es = this.elementData;
    int size = this.size;
    int i = 0;
    if (o == null) {
        while(true) {
            if (i >= size) {
                return false;
            }

            if (es[i] == null) {
                break;
            }

            ++i;
        }
    } else {
        while(true) {
            if (i >= size) {
                return false;
            }

            if (o.equals(es[i])) {
                break;
            }

            ++i;
        }
    }

    this.fastRemove(es, i);
    return true;
}

Among them, the logic of remove(int index) is relatively simple. First check the validity of the subscript, then save the value that needs remove , and call fastRemove() to remove it. In remove(Object o) , the array is traversed directly, and it is judged whether there is a corresponding element, if not Return false directly, if it exists, call fastRemove() , and return true .

Take a look at fastRemove() below:

private void fastRemove(Object[] es, int i) {
    ++this.modCount;
    int newSize;
    if ((newSize = this.size - 1) > i) {
        System.arraycopy(es, i + 1, es, i, newSize - i);
    }

    es[this.size = newSize] = null;
}

First, increase the number of modifications by 1, then decrease the length of the array by 1, and judge System.arraycopy the new length is the last one. A value that comes out is set to null .

3.3.2 removeAll()

public boolean removeAll(Collection<?> c) {
    return this.batchRemove(c, false, 0, this.size);
}

boolean batchRemove(Collection<?> c, boolean complement, int from, int end) {
    Objects.requireNonNull(c);
    Object[] es = this.elementData;

    for(int r = from; r != end; ++r) {
        if (c.contains(es[r]) != complement) {
            int w = r++;

            try {
                for(; r < end; ++r) {
                    Object e;
                    if (c.contains(e = es[r]) == complement) {
                        es[w++] = e;
                    }
                }
            } catch (Throwable var12) {
                System.arraycopy(es, r, es, w, end - r);
                w += end - r;
                throw var12;
            } finally {
                this.modCount += end - w;
                this.shiftTailOverGap(es, w, end);
            }

            return true;
        }
    }

    return false;
}

removeAll actually calls batchRemove() . In batchRemove() , there are four parameters with the following meanings:

  • Collection<?> c : target set
  • boolean complement : If the value is true , it means that the elements contained in the target set c in the array are retained, and if it is false , it means that the elements contained in the target set c in the array are deleted
  • from/end : Interval range, left closed and right open

So the passed (c,false,0,this.size) means to delete the element in the target set c in the array. The following is a brief description of the steps to perform:

  • First perform the empty operation
  • Then find the first element that meets the requirements (here is to find the first element that needs to be deleted)
  • After finding it, continue to search backward from the element, and record the subscript w of the last element in the deleted array.
  • try/catch is a protective behavior, because contains() will use AbstractCollection in the implementation of Iterator . Here, catch still calls System.arraycopy after an exception, so that the processed elements are "moved" to the front
  • Finally, the number of modifications will be increased and shiftTailOverGap will be called, which will be explained in detail later

3.3.3 removeIf()

public boolean removeIf(Predicate<? super E> filter) {
    return this.removeIf(filter, 0, this.size);
}

boolean removeIf(Predicate<? super E> filter, int i, int end) {
    Objects.requireNonNull(filter);
    int expectedModCount = this.modCount;

    Object[] es;
    for(es = this.elementData; i < end && !filter.test(elementAt(es, i)); ++i) {
    }

    if (i < end) {
        int beg = i;
        long[] deathRow = nBits(end - i);
        deathRow[0] = 1L;
        ++i;

        for(; i < end; ++i) {
            if (filter.test(elementAt(es, i))) {
                setBit(deathRow, i - beg);
            }
        }

        if (this.modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        } else {
            ++this.modCount;
            int w = beg;

            for(i = beg; i < end; ++i) {
                if (isClear(deathRow, i - beg)) {
                    es[w++] = es[i];
                }
            }

            this.shiftTailOverGap(es, w, end);
            return true;
        }
    } else if (this.modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    } else {
        return false;
    }
}

In removeIf , to delete the elements that meet the conditions, the null operation will be performed first, and then the subscript of the first element that meets the conditions will be found. If it is not found ( i>=end ), it will be judged whether there is a concurrent operation problem. If not, return false . i<end , that is to officially enter the deletion process:

  • Record start subscript beg
  • deathRow is a marker array with a length of (end-i-1)>>6 + 1 . Starting from beg , if a qualified element is encountered, the subscript will be marked (call setBit )
  • Delete after marking. The so-called deletion is actually moving the elements that do not meet the conditions to the positions after beg one by one.
  • Call shiftTailOverGap to process the elements at the end
  • Returns true , indicating that there is an element that meets the conditions and has been deleted

3.3.4 shiftTailOverGap()

The above removeAll() and removeIf() both involve shiftTailOverGap() . Let's take a look at the implementation:

private void shiftTailOverGap(Object[] es, int lo, int hi) {
    System.arraycopy(es, hi, es, lo, this.size - hi);
    int to = this.size;

    for(int i = this.size -= hi - lo; i < to; ++i) {
        es[i] = null;
    }

}

This method moves the elements in the es array forward by hi-lo bits, and sets the extra elements at the end after the move to null .

3.4 indexOf() series

include:

  • indexOf()
  • lastIndexOf()
  • contains()

3.4.1 indexOf

public int indexOf(Object o) {
    return this.indexOfRange(o, 0, this.size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = this.elementData;
    int i;
    if (o == null) {
        for(i = start; i < end; ++i) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for(i = start; i < end; ++i) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }

    return -1;
}

indexOf() is actually a packaged method. It will call the internal indexOfRange() to search. The logic is very simple. First, judge whether the value to be searched is empty. If it is not empty, use equals() == judge. mark, otherwise return -1 .

3.4.2 contains()

contains() is actually a indexOf() :

public boolean contains(Object o) {
    return this.indexOf(o) >= 0;
}

Call indexOf() method, and judge whether it is greater than or equal to 0 according to the returned subscript.

3.4.3 lastIndexOf()

The implementation of lastIndexOf() is similar to that of indexOf() , except that the traversal starts from the tail, and the internal call is lastIndexOfRange() :

public int lastIndexOf(Object o) {
    return this.lastIndexOfRange(o, 0, this.size);
}

int lastIndexOfRange(Object o, int start, int end) {
    Object[] es = this.elementData;
    int i;
    if (o == null) {
        for(i = end - 1; i >= start; --i) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for(i = end - 1; i >= start; --i) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }

    return -1;
}

4 LinkedList bottom layer

4.1 Basic structure

First look at the member variables inside:

transient int size;
transient LinkedList.Node<E> first;
transient LinkedList.Node<E> last;
private static final long serialVersionUID = 876323262645176354L;

One for the length, a head pointer and a tail pointer.

Among them, LinkedList.Node implemented as follows:

private static class Node<E> {
    E item;
    LinkedList.Node<E> next;
    LinkedList.Node<E> prev;

    Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

It can be seen that LinkedList is actually implemented based on a double linked list.

Here are some more important methods, including:

  • add()
  • remove()
  • get()

4.2 add()

add() methods include 6:

  • add(E e)
  • add(int index,E e)
  • addFirst(E e)
  • addLast(E e)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)

4.2.1 linkFirst implemented by add() / linkLast / linkBefore

Let's take a look at the relatively simple four add() :

public void addFirst(E e) {
    this.linkFirst(e);
}

public void addLast(E e) {
    this.linkLast(e);
}

public boolean add(E e) {
    this.linkLast(e);
    return true;
}

public void add(int index, E element) {
    this.checkPositionIndex(index);
    if (index == this.size) {
        this.linkLast(element);
    } else {
        this.linkBefore(element, this.node(index));
    }
}

It can be seen that the above four add() do not perform any operation of adding elements, add() is only the encapsulation of adding elements, the real implementation of add operation is linkLast() , linkFirst() and linkBefore() , these methods as the name suggests is to link elements , or the front of a node in the linked list:

void linkLast(E e) {
    LinkedList.Node<E> l = this.last;
    LinkedList.Node<E> newNode = new LinkedList.Node(l, e, (LinkedList.Node)null);
    this.last = newNode;
    if (l == null) {
        this.first = newNode;
    } else {
        l.next = newNode;
    }

    ++this.size;
    ++this.modCount;
}

private void linkFirst(E e) {
    LinkedList.Node<E> f = this.first;
    LinkedList.Node<E> newNode = new LinkedList.Node((LinkedList.Node)null, e, f);
    this.first = newNode;
    if (f == null) {
        this.last = newNode;
    } else {
        f.prev = newNode;
    }

    ++this.size;
    ++this.modCount;
}

void linkBefore(E e, LinkedList.Node<E> succ) {
    LinkedList.Node<E> pred = succ.prev;
    LinkedList.Node<E> newNode = new LinkedList.Node(pred, e, succ);
    succ.prev = newNode;
    if (pred == null) {
        this.first = newNode;
    } else {
        pred.next = newNode;
    }

    ++this.size;
    ++this.modCount;
}

The implementation is roughly the same, one is added to the tail, one is added to the head, and one is inserted to the front. In addition, all three have the following operations at the end of the method:

++this.size;
++this.modCount;

The first one indicates that the number of nodes is increased by 1, while the second one indicates that the number of modifications to the linked list is increased by 1.

For example, at the end of the unlinkLast method, there is the following code:

--this.size;
++this.modCount;

The operation of unlinkLast is to remove the last node. When the number of nodes is reduced by 1, the number of modifications to the linked list is increased by 1.

On the other hand, in general, the linked list insertion operation needs to find the position of the linked list, but in the three link methods, there is no code for for to loop to find the insertion position. Why?

Since the head and tail pointers are saved, linkFirst() and linkLast() do not need to traverse to find the insertion position, but for linkBefore() , it is necessary to find the insertion position, but linkBefore() does not have parameters such as "insertion position/insertion subscript", and is only one element value and one successor node. In other words, this successor node is the insertion position obtained through the loop, for example, the calling code is as follows:

this.linkBefore(element, this.node(index));

It can be seen that in this.node() , a subscript is passed in and a successor node is returned, that is, the traversal operation is completed in this method:

LinkedList.Node<E> node(int index) {
    LinkedList.Node x;
    int i;
    if (index < this.size >> 1) {
        x = this.first;

        for(i = 0; i < index; ++i) {
            x = x.next;
        }

        return x;
    } else {
        x = this.last;

        for(i = this.size - 1; i > index; --i) {
            x = x.prev;
        }

        return x;
    }
}

Here, we first judge which side the subscript is on. If it is close to the head, traverse backwards from the head pointer. If it is close to the tail, traverse backwards from the tail pointer.

4.2.2 Traversal implemented addAll()

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

public boolean addAll(int index, Collection<? extends E> c) {
    this.checkPositionIndex(index);
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0) {
        return false;
    } else {
        LinkedList.Node pred;
        LinkedList.Node succ;
        if (index == this.size) {
            succ = null;
            pred = this.last;
        } else {
            succ = this.node(index);
            pred = succ.prev;
        }

        Object[] var7 = a;
        int var8 = a.length;

        for(int var9 = 0; var9 < var8; ++var9) {
            Object o = var7[var9];
            LinkedList.Node<E> newNode = new LinkedList.Node(pred, o, (LinkedList.Node)null);
            if (pred == null) {
                this.first = newNode;
            } else {
                pred.next = newNode;
            }

            pred = newNode;
        }

        if (succ == null) {
            this.last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        this.size += numNew;
        ++this.modCount;
        return true;
    }
}

First of all, you can see that the two addAll actually call the same method. The steps are briefly described as follows:

  • First judge whether the subscript is legal by checkPositionIndex
  • Then convert the target collection to Object[] array
  • Perform some special judgment processing to determine whether the range of index is inserted in the middle or at the end
  • for loop through the target array and insert into the linked list
  • Modify the node length and return

4.3 remove()

Similar to add() , remove includes:

  • remove()
  • remove(int index)
  • remove(Object o)
  • removeFirst()
  • removeLast()
  • removeFirstOccurrence(Object o)
  • removeLastOccurrence(Object o)

Of course, there are actually two removeAll and removeIf , but they are actually the methods of the parent class, so I won't analyze them here.

4.3.1 unlinkFirst() implemented by remove() / unlinkLast()

remove() , removeFirst() , removeLast() are actually removed by calling unlinkFirst() / unlinkLast() , where remove() is just an alias for removeFirst() :

public E remove() {
    return this.removeFirst();
}

public E removeFirst() {
    LinkedList.Node<E> f = this.first;
    if (f == null) {
        throw new NoSuchElementException();
    } else {
        return this.unlinkFirst(f);
    }
}

public E removeLast() {
    LinkedList.Node<E> l = this.last;
    if (l == null) {
        throw new NoSuchElementException();
    } else {
        return this.unlinkLast(l);
    }
}

The logic is very simple, call unlinkFirst() / unlinkLast() after it is empty:

private E unlinkFirst(LinkedList.Node<E> f) {
    E element = f.item;
    LinkedList.Node<E> next = f.next;
    f.item = null;
    f.next = null;
    this.first = next;
    if (next == null) {
        this.last = null;
    } else {
        next.prev = null;
    }

    --this.size;
    ++this.modCount;
    return element;
}

private E unlinkLast(LinkedList.Node<E> l) {
    E element = l.item;
    LinkedList.Node<E> prev = l.prev;
    l.item = null;
    l.prev = null;
    this.last = prev;
    if (prev == null) {
        this.first = null;
    } else {
        prev.next = null;
    }

    --this.size;
    ++this.modCount;
    return element;
}

In these two unlink , since the positions of the head pointer and the tail pointer have been saved, they can be removed directly in O(1) , and finally the length of the node is decreased by 1, the number of modifications is increased by 1, and the old element is returned.

4.3.2 unlink() implemented by remove()

Let's take another look at remove(int index) , remove(Object o) , removeFirstOccurrence(Object o) , removeLastOccurrence(Object o) :

public E remove(int index) {
    this.checkElementIndex(index);
    return this.unlink(this.node(index));
}

public boolean remove(Object o) {
    LinkedList.Node x;
    if (o == null) {
        for(x = this.first; x != null; x = x.next) {
            if (x.item == null) {
                this.unlink(x);
                return true;
            }
        }
    } else {
        for(x = this.first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                this.unlink(x);
                return true;
            }
        }
    }

    return false;
}

public boolean removeFirstOccurrence(Object o) {
    return this.remove(o);
}

public boolean removeLastOccurrence(Object o) {
    LinkedList.Node x;
    if (o == null) {
        for(x = this.last; x != null; x = x.prev) {
            if (x.item == null) {
                this.unlink(x);
                return true;
            }
        }
    } else {
        for(x = this.last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                this.unlink(x);
                return true;
            }
        }
    }

    return false;
}

These methods actually call unlink to remove elements, of which removeFirstOccurrence(Object o) equivalent to remove(Object o) , let's talk about remove(int index) first, the logic of this method is relatively simple, first check the validity of the subscript, then find the node by subscript and perform unlnk .

In remove(Object o) , it is necessary to first judge whether the value of the element is null . The two loops are actually equivalent, and the first encountered element with the same target value will be removed. In removeLastOccurrence(Object o) , the code is roughly the same, except that remove(Object o) traverses from the head pointer, and removeLastOccurrence(Object o) traverses from the tail pointer.

It can be seen that these remove methods actually find the node to be deleted, and finally call unlink() to delete it. Let's take a look at unlink() :

E unlink(LinkedList.Node<E> x) {
    E element = x.item;
    LinkedList.Node<E> next = x.next;
    LinkedList.Node<E> prev = x.prev;
    if (prev == null) {
        this.first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        this.last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    --this.size;
    ++this.modCount;
    return element;
}

The implementation logic is similar to unlinkFirst() / unlinkLast() . The deletion is performed in O(1) . There are only some simple special judgment operations. Finally, the length of the node is decreased by 1, and the number of modifications is increased by 1, and finally the old value is returned.

4.4 get()

get method is relatively simple and provides three external methods:

  • get(int index)
  • getFirst()
  • getLast()

Among them, getFirst() and getLast() save the head and tail pointers. After the special judgment, O(1) returns directly:

public E getFirst() {
    LinkedList.Node<E> f = this.first;
    if (f == null) {
        throw new NoSuchElementException();
    } else {
        return f.item;
    }
}

public E getLast() {
    LinkedList.Node<E> l = this.last;
    if (l == null) {
        throw new NoSuchElementException();
    } else {
        return l.item;
    }
}

And get(int index) undoubtedly takes O(n) time:

public E get(int index) {
    this.checkElementIndex(index);
    return this.node(index).item;
}

get(int index) judges the subscript, the actual operation is this.node() . Since this method finds the corresponding node by subscript, the source code is also written before, so it will not be analyzed here, and it will take O(n) time.

5 Summary

  • ArrayList based on Object[] , LinkedList based on double linked list
  • ArrayList random access efficiency is higher than LinkedList
  • LinkedList provides more insertion methods than ArrayList , and the head and tail insertion efficiency is higher than ArrayList
  • The two methods of removing elements are not exactly the same, ArrayList provides unique removeIf() , while LinkedList provides unique removeFirstOccurrence() and removeLastOccurrence()
  • The ArrayList method of get() is always O(1) , while the LinkedList has only getFirst() / getLast() is O(1)
  • The two core methods in ArrayList are grow() and System.arraycopy , the former is the expansion method, the default is 1.5 times the expansion, the latter is the copy array method, which is a native method, and operations such as insertion, deletion, etc. need to be used
  • Many methods in LinkedList require special judgment on the head and tail, which is simpler to create than ArrayList , does not require initialization, and does not involve expansion issues

6 Appendix: An Experiment on Insertion and Deletion

Regarding insertion and deletion, it is generally believed that the efficiency of LinkedList is higher than that of ArrayList , but this is not the case. The following is a small experiment to test the insertion and deletion time.

6.1 Test Environment

  • JDKOpenJDK 11.0.12
  • JMHJMH 1.33
  • System: Manjaro 21.1.2
  • Test array length: 5w , 50w , 500w
  • Test method: List.add(int,E),list.remove(int)
  • Insertion and deletion of subscripts: randomly generated
  • Test Metrics: Average Time
  • with preheat
  • single thread

6.2 Code

package com.company;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Threads(1)
@State(Scope.Benchmark)
public class Main {
    private static final Random random = new Random();
    private static final List<Integer> data_50_000 = Stream.generate(random::nextInt).limit(50_000).collect(Collectors.toList());
    private static final List<Integer> data_500_000 = Stream.generate(random::nextInt).limit(500_000).collect(Collectors.toList());
    private static final List<Integer> data_5_000_000 = Stream.generate(random::nextInt).limit(5_000_000).collect(Collectors.toList());

    private static final String ARRAY_LIST_50_000 = "1";
    private static final String ARRAY_LIST_500_000 = "2";
    private static final String ARRAY_LIST_5_000_000 = "3";
    private static final String LINKED_LIST_50_000 = "4";
    private static final String LINKED_LIST_500_000 = "5";
    private static final String LINKED_LIST_5_000_000 = "6";

    @Param({ARRAY_LIST_50_000, ARRAY_LIST_500_000, ARRAY_LIST_5_000_000, LINKED_LIST_50_000, LINKED_LIST_500_000, LINKED_LIST_5_000_000})
    private String type;

    private List<Integer> list;
    private int size;

    @Setup
    public void setUp() {
        switch (type) {
            case ARRAY_LIST_50_000:
                list = new ArrayList<>(data_50_000);
                size = 50_000;
                break;
            case ARRAY_LIST_500_000:
                list = new ArrayList<>(data_500_000);
                size = 500_000;
                break;
            case ARRAY_LIST_5_000_000:
                list = new ArrayList<>(data_5_000_000);
                size = 5_000_000;
                break;
            case LINKED_LIST_50_000:
                list = new LinkedList<>(data_50_000);
                size = 50_000;
                break;
            case LINKED_LIST_500_000:
                list = new LinkedList<>(data_500_000);
                size = 500_000;
                break;
            case LINKED_LIST_5_000_000:
                list = new LinkedList<>(data_5_000_000);
                size = 5_000_000;
                break;
            default:
                throw new IllegalArgumentException("Illegal argument exception:" + type);
        }
    }

    @Benchmark
    public void add() {
        int index = random.nextInt(size);
        int element = random.nextInt();
        list.add(index, element);
    }

    @Benchmark
    public void remove() {
        int n = list.size();
        if (n > 0) {
            list.remove(random.nextInt(n));
        } else {
            switch (type) {
                case ARRAY_LIST_50_000:
                    list = new ArrayList<>(data_50_000);
                    break;
                case ARRAY_LIST_500_000:
                    list = new ArrayList<>(data_500_000);
                    break;
                case ARRAY_LIST_5_000_000:
                    list = new ArrayList<>(data_5_000_000);
                    break;
                case LINKED_LIST_50_000:
                    list = new LinkedList<>(data_50_000);
                    break;
                case LINKED_LIST_500_000:
                    list = new LinkedList<>(data_500_000);
                    break;
                default:
                    list = new LinkedList<>(data_5_000_000);
                    break;
            }
            list.remove(random.nextInt(list.size()));
        }
    }

    public static void main(String[] args) throws RunnerException {
        new Runner(new OptionsBuilder().include(Main.class.getSimpleName()).build()).run();
    }
}

Test Results:

Benchmark    (type)  Mode  Cnt  Score    Error  Units
Main.add          1  avgt    5  0.147 ±  0.083  ms/op
Main.add          2  avgt    5  0.159 ±  0.117  ms/op
Main.add          3  avgt    5  1.949 ±  0.102  ms/op
Main.add          4  avgt    5  0.274 ±  0.010  ms/op
Main.add          5  avgt    5  0.916 ±  0.507  ms/op
Main.add          6  avgt    5  4.199 ±  0.477  ms/op
Main.remove       1  avgt    5  0.001 ±  0.001  ms/op
Main.remove       2  avgt    5  0.010 ±  0.001  ms/op
Main.remove       3  avgt    5  1.844 ±  0.213  ms/op
Main.remove       4  avgt    5  0.010 ±  0.001  ms/op
Main.remove       5  avgt    5  0.107 ±  0.097  ms/op
Main.remove       6  avgt    5  3.934 ±  0.124  ms/o

Among them, type of 1-6 corresponds to 5w and LinkedList of ArrayList , 50w , 500w , respectively.

Although this experiment has certain limitations, it also proves that the insertion and deletion performance of ArrayList is not worse than that of LinkedList . In fact, from the source code (see the above analysis), we can know that the main time-consuming of ArrayList insertion and deletion is System.arraycopy , while the main time-consuming of LinkedList is this.node() . In fact, both require O(n) time.

As for why the insertion and deletion speed of ArrayList is faster than that of LinkedList , the author guesses that the speed of System.arraycopy is faster than the loop traversal speed of LinkedList in for , because the position of insertion/deletion in LinkedList is found through this.node() for is implemented in a loop (of course, the bottom layer is to first determine which side it is on, if it is close to the head, it will start from the head, and if it is close to the tail, it will start from the tail). The native C++ method implementation relative to System.arraycopy may be slower than C++ , thus causing the speed difference.


氷泠
420 声望647 粉丝