Public number: byte array, hope to help you 🤣🤣

This series of articles will be on the Java and Android collection framework (JDK 1.8, Android SDK 30) in several common containers combined with the source code to introduce, understand different containers in the data structure, application scenarios, advantages of the different points, I hope to help you 🤣🤣

Arrays and linked lists

Many collection frameworks use the array and linked list in the underlying structure of the two data structures, they have a great difference in data storage methods and advantages and disadvantages of these two aspects, here first to introduce the structure and differences between the two

1, the array

Assuming that there are now six elements in the array, the memory structure of the array looks like this

  1. An array is a contiguous memory space, and elements are arranged in sequence according to the coordinate index. The memory address of each data can be directly located through coordinates. For example, element4 can be directly obtained through coordinate 3, which saves the traversal operation from beginning to end
  2. Conversely, since arrays require contiguous storage of elements, it is possible to move large amounts of data when adding and removing data, making adding and removing data inefficient
  3. The size of an array must be specified before it is used, and cannot be changed after it is declared. If we know how much data to store before we use it, we can naturally initialize the array to the target size so that we don’t waste memory space. However, in fact, the amount of data is often unknown, often resulting in waste due to large memory space application, or the need for subsequent expansion due to less application, and array expansion can only create a new array and migrate the data as a whole, which affects the array performance

The bottom line of an ArrayList is to store data in arrays

2, list

Suppose you now have four elements that depend on a linked list, and the memory structure of the list looks like this

  1. What is shown in the figure is a bidirectional linked list, that is, in addition to the actual data of each node, there are two references to the previous node (PREv) and the next node (next), and each node is connected in series by this bidirectional link. There are also two references to the head node (first) and the tail node (last), facilitating forward and reverse traversal
  2. The linked list does not require contiguous memory space, and new nodes can be added anywhere in memory, as long as the previous node and the next node keep references to each other. This results in random access to data traversing the entire list, or in the worst case, traversing the entire list. Of course, you can choose between forward and reverse traversal depending on the situation to improve access efficiency, but in general linked lists are less efficient at randomly accessing data than arrays
  3. When adding or removing elements, you only need to modify the reference of adjacent nodes to the specified node, rather than moving elements like an array. Therefore, linked lists are efficient in adding and removing elements
  4. The linked list does not need to apply for memory space in advance, but can dynamically apply for memory space based on actual usage
  5. There is also a one-way linked list structure where each node contains a reference to the next node, next, but not prev, so the one-way linked list can only be traversed from beginning to end

The underlying LinkedList is a LinkedList to store data

Second, the ArrayList

ArrayList is probably the most frequently used collection container by most developers. ArrayList implements the List interface, which is an ordered container. That is, elements are stored in the same order as they were added, allowing the same elements to be added, including NULL. The bottom layer of ArrayList stores data through arrays. If the array space is insufficient when adding elements to the ArrayList, ArrayList will automatically expand the bottom layer array and migrate the existing data

Class declaration

The interface that ArrayList implements is fast access, cloneable, and serializable

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

2. Member variables

An ArrayList contains the following member variables, primarily elementData. ElementData is a low-level array used to store data, and since its data type is declared Object, it can be used to store any type of data. ArrayList, on the other hand, is a generic class. If we specify a data type at initialization, the compiler will automatically perform type verification and conversion when we read or write data to elementData, depending on the syntax sugar provided by Java generics, to ensure that the data types stored and retrieved are safe

	// serialize the ID
    private static final long serialVersionUID = 8683452581122892189L;

    // Minimum capacity after capacity expansion
    private static final int DEFAULT_CAPACITY = 10;

    // If the external initialization size set for the collection is 0, elementData points to this empty array
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // Point elementData to this empty array if the collection is initialized with a no-argument constructor
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // An array to hold elements
    transient Object[] elementData;

    // Set size
    private int size;

	// Snapshot version of ArrayList
	protected transient int modCount = 0;
Copy the code

constructors

If we already know the target data size, it is more efficient to pass in the final capacity value when we initialize the ArrayList. If initialCapacity is too large, memory will be wasted. If initialCapacity is too small, subsequent capacity expansion may require multiple times. Each capacity expansion requires copying the original data to a new array, which reduces the operating efficiency

If we use a no-argument constructor or specify initialCapacity as 0, elementData will only be pointed to the empty array and an array variable will not be created

	// Specify the initial capacity of the collection to initialize the array
    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); }}// No initial capacity is specified externally
    public ArrayList(a) {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    // Pass in a copy of initial data for initialization
    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 {
            this.elementData = EMPTY_ELEMENTDATA; }}Copy the code

4. Get elements

When an ArrayList obtains an element at a specified index, it obtains the element directly through the coordinate value, and does not need to iterate from the beginning. Therefore, ArrayList traversal and random access are efficient

	@SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

    public E get(int index) {
        // Check whether the value range is valid
        rangeCheck(index);
        return elementData(index);
    }
Copy the code

5. Add elements

Adding elements to an ArrayList is less ideal. If you add data directly to the end of the collection, then you can directly locate the location to perform the assignment; If you are inserting data to index in the middle of the set, you need to push all data in the array after index index back one bit, and then insert the data into the empty position. In addition, elementData may run out of space before data is inserted, so you need to expand it first. The expansion operation creates a new array that matches the size, migrates the data from the original array to the new array, and then makes elementData point to the new array

As you can see, adding data to a collection and expanding it can cause a lot of movement of array elements, so an ArrayList is not very efficient at storing data

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

    public void add(int index, E element) {
        rangeCheckForAdd(index);
        // Expand as needed
        ensureCapacityInternal(size + 1);
        // Move all values after index index back one bit
        System.arraycopy(elementData, index, elementData, index + 1,size - index);
        elementData[index] = element;
        size++;
    }
Copy the code

This is the case for storing individual data, but there is also the case for storing entire collections

	// Return true if the data to be added is not empty, false otherwise
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);
        // Copy array A to the end of elementData
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        returnnumNew ! =0;
    }

    // Adds data from the specified index, returning true if the data to be added is not empty, false otherwise
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);
        // The number of array elements to move
        int numMoved = size - index;
        NumMoved may be 0 because the data to be added may happen to be from the end of the array
        // So only move the element value of the array when numMoved > 0 to make space for array A
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
        // Add the data contained in array A to elementData
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        returnnumNew ! =0;
    }
Copy the code

6. Remove elements

Because an array is a data structure with contiguous memory addresses, removing an element can also cause a large number of elements to move

	// Removes the element value at the specified index and returns the value
    public E remove(int index) {
        rangeCheck(index);
        modCount++;
        // The element value to be removed
        E oldValue = elementData(index);
        // The number of elements that need to be moved because they need to be removed
        int numMoved = size - index - 1;
        NumMoved may be 0 because the element to be removed may be the last bit of the array
        // Move the array only if numMoved > 0
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        // The last bit of the array is invalid regardless of whether the element value needs to be moved
        // Set it to NULL here to aid GC collection
        elementData[--size] = null;
        return oldValue;
    }

    // Remove the object containing the first element of the collection with the value o
    // Return true if the object is included, false otherwise
    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;
    }
Copy the code

7. Capacity expansion mechanism

Let’s look at the specific implementation logic of the array expansion mechanism

MinCapacity, the input parameter to the ensureCapacity method, is used to specify the minimum amount of space that you want to expand, but minCapacity should not be less than DEFAULT_CAPACITY, that is, the array capacity should not be less than 10. The reason for the minimum size limit is to reduce the possibility of multiple capacity increases, which can easily occur for arrays less than 10

If you know the size of the target data before you initialize the ArrayList, it is best to use ArrayList(int initialCapacity) to initialize the underlying array directly to the target size. Or you can call the ensureCapacity method before adding data to directly expand the array to its target size, avoiding multiple expansions during subsequent assignments

	public void ensureCapacity(int minCapacity) {
        intminExpand = (elementData ! = DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ?0
            : DEFAULT_CAPACITY;
        if(minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); }}private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // If the current array size is indeed smaller than the required minCapacity, expand it
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
Copy the code

The grow(int minCapacity) method actually performs the capacity expansion. Before capacity expansion, the system determines whether the capacity meets the requirements of minCapacity by increasing the capacity to 1.5 times the current capacity. If the capacity meets the requirements, the system directly expands the capacity to 1.5 times the current capacity. Otherwise, the system expands the capacity to minCapacity, but the final capacity cannot be greater than integer.max_value

Once you’ve built a new array that fits the size, you copy the elements from the original array into the new array, and you’re done

	// The maximum capacity of the array
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // Suppose the space is 1.5 times larger
        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);
    }
Copy the code

8. Modify elements

	// Set the index element value to Element and return the original value
	public E set(int index, E element) {
    	rangeCheck(index);
    	E oldValue = elementData(index);
    	elementData[index] = element;
    	return oldValue;
	}
Copy the code

9. Iterate over the number group

Traversal number group method includes the following several, the logic is relatively simple, directly read the comments. An important point is to look at the modCount validation inside the method

	@Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            // Pass the collection elements in turn to the Accept method
            action.accept(elementData[i]);
        }
        // If modCount is changed, the array was changed during the traversal
        // Stop traversing and throw an exception
        if(modCount ! = expectedModCount) {throw newConcurrentModificationException(); }}// Filter the set elements according to the given rules, and remove the elements if they match the filtering rules
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        // Number of elements to remove
        int removeCount = 0;
        // Used to indicate which index position in the collection needs to be removed
        final BitSet removeSet = new BitSet(size);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i];
            // Determine whether the collection elements conform to the filter rule
            if (filter.test(element)) {
                // The set method causes the element at index position I to be trueremoveSet.set(i); removeCount++; }}// Do not allow the collection to be modified by other methods during sorting (e.g. removing elements)
        if(modCount ! = expectedModCount) {throw new ConcurrentModificationException();
        }
        // Remove elements only if removeCount > 0
        final boolean anyToRemove = removeCount > 0;
        if (anyToRemove) {
            // The size of the collection after the specified element is removed
            final int newSize = size - removeCount;
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                // Skip the position marked true and jump directly to the index bit where the element does not need to be removed
                i = removeSet.nextClearBit(i);
                // Valid data is gradually aggregated from tail to head
                elementData[j] = elementData[i];
            }
            // Remove invalid data from tail to facilitate GC collection
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;
            }
            this.size = newSize;
            // Do not allow the collection to be modified by other methods during sorting (e.g. removing elements)
            if(modCount ! = expectedModCount) {throw new ConcurrentModificationException();
            }
            modCount++;
        }
        return anyToRemove;
    }

    // Pass the collection element traversal to operator and replace the raw data with the value returned by operator
    @Override
    @SuppressWarnings("unchecked")
    public void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            // Pass the array elements in turn to the Apply method and replace the original data with their return values
            elementData[i] = operator.apply((E) elementData[i]);
        }
        // Do not allow the collection to be modified by other methods during sorting (e.g. removing elements)
        if(modCount ! = expectedModCount) {throw new ConcurrentModificationException();
        }
        modCount++;
    }
Copy the code

iterators

An ArrayList contains an Iterator implementation class for iterating over elements, as shown below

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("https://github.com/leavesC");
        Iterator<String> iterator = stringList.iterator();
        if(iterator.hasNext()) { String next = iterator.next(); System.out.println(next); }}Copy the code

ModCount is a simple “snapshot” of the ArrayList. ModCount is a version of the ArrayList. Whenever an element is added, removed, or modified, ModCount is always incremented

If we were adding or subtracting elements at the same time as we were going through an ArrayList, or if we were adding or subtracting elements at the same time as multiple threads, that would either make the result unreliable or cause an out-of-bounds exception. So ArrayList uses modCount to indicate that the current iteration is in a reliable state. If the value of modCount changes before and after the iterating process, it indicates that the ArrayList was changed during the iterating process. In this case, the iterating result is considered unreliable and an exception is directly thrown. It’s important to note that modCount does a simple check, so it can’t tell if the current traversal is really safe

    protected transient int modCount = 0;

    public Iterator<E> iterator(a) {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        
        // The index of the next element of the element to which lastRet points
        int cursor;

        // The index of the last returned element
        // If the value is -1, the element has not been returned or the modified element has been removed
        int lastRet = -1;

        // The data structure used to verify that the collection is not modified during the iteration
        int expectedModCount = modCount;

        // If any elements are not traversed
        public boolean hasNext(a) {
            returncursor ! = size; }// Get the next element
        @SuppressWarnings("unchecked")
        public E next(a) {
            checkForComodification();
            int i = cursor;
            // If the index value exceeds the value range, an exception is thrown
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            // Throw an exception if the index value exceeds the indexable range of the array
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        // Remove the element pointed to by lastRet
        public void remove(a) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            try {
                ArrayList.this.remove(lastRet);
                // Because the original element in lastRet is removed, the element that lastRet refers to is the element in lastRet+1
                cursor = lastRet;
                lastRet = -1;
                // Since Itr modifies the set proactively, the expectedModCount value needs to be updated to avoid throwing an exception later
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw newConcurrentModificationException(); }}// Iterate over the remaining elements since the index cursor
        @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();
            }
            // Iterate through the accept method
            while(i ! = size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } cursor = i; lastRet = i -1;
            checkForComodification();
        }

        // Determine whether the collection was changed externally (e.g. by removing elements by other iterators) as the iterator traverses the collection.
        // If so, an exception is thrown
        final void checkForComodification(a) {
            if(modCount ! = expectedModCount)throw newConcurrentModificationException(); }}Copy the code

11. Efficiency test

Finally, test the influence of the number of expansion times of ArrayList on its operating efficiency

Store the same amount of data to three ArrayLists, but specify different initialization sizes for each ArrayList

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        List<String> stringList = new ArrayList<>();
        for (int i = 0; i < 300000; i++) {
            stringList.add("leavesC " + i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Initial capacity is 0, elapsed time:" + (endTime - startTime) + "毫秒");


        startTime = System.currentTimeMillis();
        List<String> stringList2 = new ArrayList<>(100000);
        for (int i = 0; i < 300000; i++) {
            stringList2.add("leavesC " + i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("Initial capacity 100000, elapsed time:" + (endTime - startTime) + "毫秒");


        startTime = System.currentTimeMillis();
        List<String> stringList3 = new ArrayList<>(300000);
        for (int i = 0; i < 300000; i++) {
            stringList3.add("leavesC " + i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("Initial capacity 300000, time:" + (endTime - startTime) + "毫秒");
    }
Copy the code

There is still a large gap in the running efficiency of ArrayList under the three methods. Although this test method is not rigorous, it can be seen that the running efficiency of ArrayList is improved a lot after eliminating the expansion operation

Initial capacity is0, time:39Ms initial capacity is100000, time:32Ms initial capacity is300000, time:13msCopy the code

Third, LinkedList

LinkedList implements both the List interface and the Deque interface, so it can be treated as an ordered container, a Queue, or a Stack. Although LinkedList implements the same List interface as ArrayList, its underlying implementation is through a bidirectional LinkedList, so it is more efficient to insert and delete elements than ArrayList, and therefore less efficient to access randomly

Class declaration

As you can see from the several interfaces that LinkedList implements, the LinkedList is fast access, clonable, serializable, and can be viewed as a queue or stack that supports ordered access

public class LinkedList<E> extends AbstractSequentialList<E> 
    implements List<E>, Deque<E>, Cloneable.java.io.Serializable
Copy the code

LinkedList is internally realized through the data structure of bidirectional LinkedList. In addition to storing the data elements of the Node, each LinkedList Node also has two Pointers to point to its two adjacent nodes, which are static class nodes in LinkedList

	private static class Node<E> {

        // The actual element that the current node contains
        E item;

        // point to the next node
        Node<E> next;

        // point to the last node
        Node<E> prev;

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

2. Member variables

	// The total number of nodes in a bidirectional list
    transient int size = 0;

    // The header of the bidirectional list
    transient Node<E> first;

    // The end of the bidirectional list
    transient Node<E> last;

    // serialize the ID
    private static final long serialVersionUID = 876323262645176354L;
Copy the code

The member variables first and last are used to point to the head and tail of the list, respectively, so the data structure of LinkedList looks something like this

constructors

Instead of requesting a contiguic memory space to store data, the LinkedList dynamically requests memory space every time a new element needs to be added, so both LinkedList constructors are simple

	public LinkedList(a) {}// Pass in initial data
    public LinkedList(Collection<? extends E> c) {
        this(a); addAll(c); }Copy the code

4. Add elements

The add(E E) method is used to add nodes to the end of the list. Since last points to the end of the list, adding new elements to the end requires only a few references, which is efficient

    // add element e as a tail
    // Because LinkedList allows the same elements to be added, this method always returns true
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    // set element e to the end
    void linkLast(E e) {
        // Save the original tail
        final Node<E> l = last;
        // Build a new endpoint and point to the original endpoint
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        // If the original endpoint is null, the number of elements in the original linked list is 0, and the inserted endpoint is also the header
        // If the old tail is not null, next points to the new tail
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        // The number of elements increases by 1
        size++;
        modCount++;
    }
Copy the code

The add(int index, E element) method is used to add an element to the specified index. First, you need to obtain the node at the corresponding position through the index index, and create a new node at that position to store the element element. Finally, you need to modify the reference between adjacent nodes

 	// Insert element element in index index
    public void add(int index, E element) {
        / / judgement index size is legal, thrown out rule IndexOutOfBoundsException
        checkPositionIndex(index);
        // If index == size, add element as a tail
        // create a new node before index index
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

	// set element e to the top of succ
    void linkBefore(E e, Node<E> succ) {
        // Save the last node of succ
        final Node<E> pred = succ.prev;
        // Construct the node corresponding to element e
        final Node<E> newNode = new Node<>(pred, e, succ);
        // set the last node of succ to newNode
        succ.prev = newNode;
        // If pred is null, succ is the header, and newNode is the new header
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        // The number of elements increases by 1
        size++;
        modCount++;
    }
Copy the code

5. Remove elements

The remove() method has two overloaded forms, both of which internally remove a reference to a specified Node from a linked list by calling the unlink(Node

x) method, unlike the massive data movement that ArrayList can cause when an element is removed. The LinkedList simply removes the specified element from the list by removing the reference

    // Remove the node from index index
    public E remove(int index) {
        / / judgement index size is legal, thrown out rule IndexOutOfBoundsException
        checkElementIndex(index);
        return unlink(node(index));
    }

	// Forward traversal of the list, remove the node whose first element is o
    // Return true if the removal succeeded, false otherwise
    public boolean remove(Object o) {
        if (o == null) {
            for(Node<E> x = first; x ! =null; x = x.next) {
                if (x.item == null) {
                    // Remove node x
                    unlink(x);
                    return true; }}}else {
            for(Node<E> x = first; x ! =null; x = x.next) {
                if (o.equals(x.item)) {
                    // Remove node x
                    unlink(x);
                    return true; }}}return false;
    }

	// Remove node x and return its element value
    E unlink(Node<E> x) {
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
        // if prev == null, x is the first node, and the first node is the second node
        // If prev! = null, the reference to node x is removed
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        // If next == null, x is the last node, and the last node is the penultimate node
        // If next! = null, the reference to node x is removed
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        // Help GC collection
        x.item = null;
        // The number of elements is reduced by 1
        size--;
        modCount++;
        return element;
    }
Copy the code

Random access to elements

For a one-way linked list, if you want to randomly locate a node, you can only locate it by traversing from the beginning node. In the most extreme case, you need to traverse the whole list to locate the target node. If it is a bidirectional list, forward or reverse traversal can be selected. In the most extreme case, half of the list is traversed to locate the target node. As a result, LinkedList is not as efficient for random access as arrays

    // Get the node element at index
    public E get(int index) {
        / / judgement index size is legal, thrown out rule IndexOutOfBoundsException
        checkElementIndex(index);
        return node(index).item;
    }

    // Change the element contained in the node at index index to Element and return the old element
    public E set(int index, E element) {
        / / judgement index size is legal, thrown out rule IndexOutOfBoundsException
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

    // Get the node at index index
    Node<E> node(int index) {
        //size >> 1
        // If index is near the head of the list, the node is traversed from head to tail
        // If index is near the end of the list, traverse backwards from the end to the head to find the node
        // The most extreme case is to traverse half of the elements to reach the target node
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            returnx; }}Copy the code

7. Several common methods

	// Check whether the element o is included
    public boolean contains(Object o) {
        returnindexOf(o) ! = -1;
    }

    // Get the number of elements
    public int size(a) {
        return size;
    }
    
    // Clear the list elements and cut off references between nodes
    public void clear(a) {
        for(Node<E> x = first; x ! =null;) { Node<E> next = x.next; x.item =null;
            x.next = null;
            x.prev = null;
            x = next;
        }
        first = last = null;
        size = 0;
        modCount++;
    }
    
    // return the index of the node whose first element is o
    // If not found, -1 is returned
    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for(Node<E> x = first; x ! =null; x = x.next) {
                if (x.item == null)
                    returnindex; index++; }}else {
            for(Node<E> x = first; x ! =null; x = x.next) {
                if (o.equals(x.item))
                    returnindex; index++; }}return -1;
    }

    // Return the index of the node whose last element is o
    // If not found, -1 is returned
    public int lastIndexOf(Object o) {
        int index = size;
        if (o == null) {
            for(Node<E> x = last; x ! =null; x = x.prev) {
                index--;
                if (x.item == null)
                    returnindex; }}else {
            for(Node<E> x = last; x ! =null; x = x.prev) {
                index--;
                if (o.equals(x.item))
                    returnindex; }}return -1;
    }
Copy the code

8. Deque interface

The methods described above are declared in the List interface, so let’s look at the methods in the Deque interface

In fact, the meanings of many methods in the Deque interface are similar, and some methods are called each other, which is not complicated

	// set element e as the header
    public void addFirst(E e) {
        linkFirst(e);
    }

    // set element e to the end
    public void addLast(E e) {
        linkLast(e);
    }
    
    // add element e as a tail
    public boolean offer(E e) {
        return add(e);
    }

    // add element e as a header
    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }

    // add element e as a tail
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }

    // Get the element value of the header node
    public E peekFirst(a) {
        final Node<E> f = first;
        return (f == null)?null : f.item;
    }

    // Get the element value of the tail node
    public E peekLast(a) {
        final Node<E> l = last;
        return (l == null)?null : l.item;
    }

    // Get the element value of the header node and remove it from the list
    public E pollFirst(a) {
        final Node<E> f = first;
        return (f == null)?null : unlinkFirst(f);
    }

    // Get the element value of the tail node and remove it from the list
    public E pollLast(a) {
        final Node<E> l = last;
        return (l == null)?null : unlinkLast(l);
    }

    // add element e as a header
    public void push(E e) {
        addFirst(e);
    }

    // Get the element value of the header node and remove it from the list
    public E pop(a) {
        return removeFirst();
    }

    // Remove the node whose first element is o from the end of the list
    // Return true if the removal succeeded, false otherwise
    public boolean removeFirstOccurrence(Object o) {
        return remove(o);
    }

    // Iterate backwards from the end of the list to the head, removing the node whose first element is o
    // Return true if the removal succeeded, false otherwise
    public boolean removeLastOccurrence(Object o) {
        if (o == null) {
            for(Node<E> x = last; x ! =null; x = x.prev) {
                if (x.item == null) {
                    unlink(x);
                    return true; }}}else {
            for(Node<E> x = last; x ! =null; x = x.prev) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true; }}}return false;
    }
Copy the code

9. Efficiency test

As mentioned above, LinkedList is much more efficient at adding and removing elements than ArrayList, but randomly accessing elements is much less efficient than ArrayList. Here’s a test to verify the difference

Store the same amount of data to the ArrayList and LinkedList, and then remove 100 elements and traverse 10,000 elements each to see how long they take

ArrayList:

    public static void main(String[] args) {
        List<String> stringArrayList = new ArrayList<>();
        for (int i = 0; i < 300000; i++) {
            stringArrayList.add("leavesC " + i);
        }

        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            stringArrayList.remove(100 + i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Remove 100 elements from ArrayList when:" + (endTime - startTime) + "毫秒");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            stringArrayList.get(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("Traversal of 10000 elements in ArrayList with time:" + (endTime - startTime) + "毫秒");
    }
Copy the code

LinkedList:

    public static void main(String[] args) {
        List<String> stringLinkedList = new LinkedList<>();
        for (int i = 0; i < 300000; i++) {
            stringLinkedList.add("leavesC " + i);
        }

        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            stringLinkedList.remove(100 + i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Remove 100 elements from LinkedList when:" + (endTime - startTime) + "毫秒");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            stringLinkedList.get(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("Traversal 10000 elements in the LinkedList with time:" + (endTime - startTime) + "毫秒");
    }

Copy the code

It can be seen that the gap between the two is still very large. Before using the collection framework, we need to decide which one to use according to the actual application scenario

Remove from the ArrayList100Element, time:18Milliseconds to traverse the ArrayList10000Element, time:1Milliseconds to remove the LinkedList100Element, time:0Milliseconds traverse the LinkedList10000Element, time:237msCopy the code