LinkList source code analysis

Posted by amit.patel on Wed, 05 Jan 2022 01:39:53 +0100

LinkList source code analysis

@auther worry free youth

@creatTime 2020/07/16

1, Foreword

This time, let's take a look at the second of the common lists - LinkedList. When analyzing ArrayList earlier, we mentioned that LinkedList is a linked List structure. In fact, it is similar to the LinkedHashMap structure we talked about when analyzing map, but it is much simpler. Let's take a look at its specific structure in detail today, And the scenarios used.

2, LinkedList structure overview

Before looking at the specific structure, let's take a look at its inheritance relationship:

Unlike ArrayList, LinkedList inherits AbstractSequentialList. It can be seen from the word Sequential that the abstract class implements the structure of Sequential access, because it can be inferred that it may be related to the linked list.

In addition, it is worth noting that the Deque interface, the origin of the class name is "double ended queue", that is, a two-way queue, that is, queue operations can be performed from the head and tail.

So to sum up, we can know that LinkedList is a two-way linked list data structure:

LinkedList can be traversed from the head and tail.

3, LinkedList source code reading

3.1 LinkedList member variable

// Number of elements in list
transient int size = 0;
// Head node of linked list
transient Node<E> first;
// Tail node of linked list
transient Node<E> last;

The internal class Node called Node is the key to implementing the linked list:

private static class Node<E> {
  // Actual stored elements
  E item;
  // Last element
  Node<E> next;
  // Previous element
  Node<E> prev;
  // The order of constructor elements is before, after and after. It's like waiting in line
  Node(Node<E> prev, E element, Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
  }
}

3.2 LinkedList construction method

Because the linked list structure is adopted, LinkedList does not have the construction method of specified capacity like ArrayList, so here we mainly talk about the construction method of pass through set.

public LinkedList(Collection<? extends E> c) {
  // Call the construction method without parameters, but there is nothing in it
  this();
  // Add the elements in the c set to the list
  addAll(c);
}

Considering that addall (collection <? Extensions E > C) is a public method, consider adding elements at the end of the linked list when elements already exist in the list:

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

public boolean addAll(int index, Collection<? extends E> c) {
  // Check whether the index is correct, i.e. between 0-size
  checkPositionIndex(index);
  // Convert collection to array
  Object[] a = c.toArray();
  int numNew = a.length;
  if (numNew == 0)
    return false;
  // pred is the preceding element and succ is the successor element
  Node<E> pred, succ;
  // Initialize pred and succ.
  if (index == size) {
    // index == size, indicating that the position of the element to be inserted is at the end of the linked list. The post element is null, and the previous element is last
    succ = null;
    pred = last;
  // index != size, indicating that it is inserted in the middle of the linked list. pred is the prev of the original index, and succ is the original element
  } else {
    succ = node(index);
    pred = succ.prev;
  }
  // Find out the relationship between the front and back elements, that is, traverse the array and add them one by one
  for (Object o : a) {
    @SuppressWarnings("unchecked") E e = (E) o;
    Node<E> newNode = new Node<>(pred, e, null);
    if (pred == null)
      first = newNode;
    else
      pred.next = newNode;
    pred = newNode;
  }

  // If the subsequent element is empty, the last element after insertion is prev last
  if (succ == null) {
    last = pred;
  // Otherwise, the relationship between the last element and the previous element is maintained
  } else {
    pred.next = succ;
    succ.prev = pred;
  }
  
  size += numNew;
  modCount++;
  return true;
}

The logic for traversing the Collection and inserting the linked list should be very clear:

  1. Call the Node's construction method according to the pre self post relationship for initialization.
  2. Since the previous element pred may be empty (constructor call), if it is judged that pred is empty, the initialized element is the header node
  3. Otherwise, maintain the direct relationship between pred and the new node newNode.
  4. Use the new node as pred to prepare for the next element insertion

In addition, as a two-way linked list, the node(int index) method also uses this feature to traverse faster:

Node<E> node(int index) {
  // If the index is in the first half of the linked list, the traversal starts from the head node
  if (index < (size >> 1)) {
    Node<E> x = first;
    for (int i = 0; i < index; i++)
      x = x.next;
    return x;
  // If the index is in the second half of the linked list, it will traverse from the tail node
  } else {
    Node<E> x = last;
    for (int i = size - 1; i > index; i--)
      x = x.prev;
    return x;
  }
}

The elements in the first half are traversed from scratch, and the elements in the second half are traversed from the tail.

3.3 important methods of LinkedList

3.3.1 add(E e)

First, let's talk about the specific idea. As a linked list structure, adding elements is to insert elements at the end of the linked list. In this process, we should consider:

  • What should I do if the last element is null
public boolean add(E e) {
  linkLast(e);
  return true;
}

void linkLast(E e) {
  // Record last node
  final Node<E> l = last;
  // Initialize a new node
  final Node<E> newNode = new Node<>(l, e, null);
  last = newNode;
  // Judge the last node
  if (l == null)
    first = newNode;
  else
    l.next = newNode;
  // Number of elements + 1
  size++;
  // Add modification times
  modCount++;
}

The general processing is similar to linkNodeLast of LinkedHashMap.

3.3.2 remove(Object o)

Similarly, let's talk about the specific treatment ideas first:

  1. Since the inserted element may be null, judge O. otherwise, null pointer exceptions will be reported whether o is null or the element is null during traversal
  2. After finding the element, re maintain the relationship between the front and back elements, taking into account whether the element is at the beginning and end
public boolean remove(Object o) {
  // Judgment of whether it is empty
  if (o == null) {
    // Traverse the linked list to find elements
    for (Node<E> x = first; x != null; x = x.next) {
      if (x.item == null) {
        // Once found, re maintain the relationship between the elements before and after deleting the element
        unlink(x);
        return true;
      }
    }
  // Same as above
  } else {
    for (Node<E> x = first; x != null; x = x.next) {
      if (o.equals(x.item)) {
        unlink(x);
        return true;
      }
    }
  }
  return false;
}

When unlink ing, you need to consider the second point mentioned earlier:

E unlink(Node<E> x) {
  final E element = x.item;
  // Elements before and after recording
  final Node<E> next = x.next;
  final Node<E> prev = x.prev;
  // If prev is null, it means that x node is the first node. After deletion, next is the first node
  if (prev == null) {
    first = next;
  // Otherwise, the next element of prev is the next element of x
  } else {
    prev.next = next;
    // Set to null to facilitate prev's GC
    x.prev = null;
  }
  // ditto
  if (next == null) {
    last = prev;
  } else {
    next.prev = prev;
    // Set to null to facilitate the GC of next
    x.next = null;
  }
  // Set to null for GC convenience
  x.item = null;
  size--;
  modCount++;
  return element;
}

3.3.3 listIterator(int index)

Finally, the author makes a brief analysis on the implementation of iterator.

public Iterator<E> iterator() {  // Call the method return listiterator() in AbstractList;} public ListIterator<E> listIterator() {  return listIterator(0);}

What iterator() calls is actually the listIterator() method. Different implementation classes will implement different methods. In LinkedList, what is it like:

public ListIterator<E> listIterator(int index) {  checkPositionIndex(index);  return new ListItr(index);}

The call returns the ListItr object.

private class ListItr implements ListIterator<E> {  // Record the last returned element private node < E > lastreturned// Record the next element private node < E > next; private int nextIndex;  //  It is used to judge whether there are changes to elements during iteration. Private int expectedmodcount = modcount; Listitr (int index) {/ / initialize next to return next = (index = = size)? Null in the next method: node (index); nextindex = index;} public boolean hasNext() {    return nextIndex < size;  }   Public e next() {/ / judge whether there is any change to the element. If so, throw an exception checkforcomodification(); if (! Hasnext()) throw new nosuchelementexception(); 	//  The next element in next () is the result to be returned lastreturned = next; next = next. next;     nextIndex++;     return lastReturned. item;  }     final void checkForComodification() {    if (modCount != expectedModCount)      throw new ConcurrentModificationException();  }}

From the source code, we can know why we throw exceptions when we modify the collection in the process of iteration. The purpose of doing so is to prevent problems caused by multithreading operating the same collection.

4, Usage scenario of LinkedList

As a feature of the linked list structure, LinkedList can ensure its endpoint operations, such as insertion and deletion, which is faster than ArrayList. The reason is very simple. After ArrayList is deleted, the following elements must be moved forward every time (although the copy method is adopted), and LinkedList only needs to maintain the relationship between the front and rear elements again.

Quote from Java programming thought:

The best practice may be to use ArrayList as the default choice. Only when you need to use additional functions (personally understood as the operation of Queue), or when the performance of the program deteriorates due to frequent insertion and deletion from the middle of the table, you can choose LinkedList.

5, Summary

For frequent insert and delete operations in the middle of the collection, or when the queue feature needs to be used, we can consider using LinkedList. Finally, thank you for watching. If there is something wrong, please correct it and make progress with you!

Topics: Java Container