Java LinkedList

Java LinkedList

The question which often arises is: are LinkedList preferable to ArrayList? In which case should I use it?

As explained in the Java documentation, LinkedList is:

  • A doubly-linked chain: elements are stored in nodes, with linking back and forth between themselves,
  • Mutable: objects can be added and/or removed,
  • Not Thread-safe: LinkedList is not suitable for concurrent access. See Thread Safety for more information.

Let’s explore how to use a LinkedList through simple code examples! And find out when a LinkedList instead of an ArrayList should be used.

How Linked List works

Doubly Linked List

A Doubly Linked List consists of a sequence of nodes. Each node points to the previous and the next node. Sentinel nodes are used to keep a reference on both first and last node.

Type Hierarchy

Class Hierarchy inside JDK

The LinkedList inherits from:

  • java.util.AbstractList,
  • java.util.Object,
  • and implements java.util.List, java.util.Collection.

List is a subtype of Collection with guaranteed order of insertion.

Properties

The Java LinkedList has the following properties:

  • Can contain duplicate elements (i.e. elements which are equals),
  • Maintains the order of insertion,
  • is not synchronized (thus not thread-safe),
  • Random access (through an integer index) are slow (0(n/2) on average),
  • Element removal is fast because elements are stored in nodes which can be removed quickly (change links from preceding and next nodes),
  • Element insertion is efficient because only a new node must be allocated (no array resize like with LinkedList).

Methods

The following table lists LinkedList most commonly used methods:

Method Description
void add(int index, Object element) Inserts an element at a given index
boolean addAll(Collection c) Appends all the elements contained in the passed collection
void clear() Empties the list
int lastIndexOf(Object o) Returns last index of given object, or -1 if not found
Object[] toArray() Creates an array with a copy of all objects within the list
boolean add(Object o) Appends the passed object to the list
boolean addAll(int index, Collection c) Inserts all elements at the given index
Object clone() Creates a shallow copy of the list (elements within are not cloned)
int indexOf(Object o) Index of the object being passed, or -1 if not found
int size() Number of elements the list contains

Comparison with Arrays

Arrays is a primitive structure designed to hold a fixed number of objects. It’s possible but quite difficult to resize an array (the code is usually pretty ugly too). For this reason, a LinkedList is much more convenient than an Array when you need to add/remove objects without knowing the exact collection size.

package com.octoperf;

import org.junit.Test;

import java.util.LinkedList;
import java.util.List;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class LinkedListTest {

  @Test
  public void shouldArray() {
    final String[] arr = new String[0];
    assertTrue(arr.length == 0);
  }
}

In the code above, we show how to create an empty array in Java. The code asserts the array length is zero. Now, let’s see how the code differs when using a LinkedList.


public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

A LinkedList by default contains absolutely nothing. It has two pointers first and last pointing respectively to the first and last node of the list.

For this reason, adding elements to a LinkedList is much more efficient than adding elements into a Array.

Algorithmic Complexity

Before we dive into the code, let’s see the most common operations this List performs algorithmically speaking. We’re going to use the Big O-Notation to describe code complexity here.

Operation Complexity
add(object o) 0(1)
add(object o, int index) 0(n)
remove(int index) 0(n)
remove(object o) 0(n)
size() 0(n)
contains(object o) 0(n)

Being based on an doubly linked chain, LinkedList is best suited for heavily adding / removing elements. Also, beware of evaluating a LinkedList size because it’s costly operation. (while LinkedList.size() complexity is 0(1))

Constructors


package com.octoperf;

import org.junit.Test;

import java.util.LinkedList;
import java.util.List;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class LinkedListTest {

  @Test
  public void shouldCreate() {
    final List<String> list = new LinkedList<>();
    assertTrue(list.isEmpty());
    assertFalse(list.iterator().hasNext());
  }
}

In the code above, we instantiate a new LinkedList, which is empty by default.

You can also create a list by passing another Collection. It creates a copy of the passed collection:


@Test
public void shouldCopy() {
  final List<String> another = new LinkedList<>();
  another.add("Hello World!");

  final List<String> list = new LinkedList<>(another);
  assertFalse(list.isEmpty());
  assertTrue(list.contains("Hello World!"));

  another.clear();
  assertTrue(list.contains("Hello World!"));
}

The created List contains now contains a copy of all the elements the passed collection contains. Even if you clear the original collection, the element remains in the LinkedList.

Adding Elements

Elements can be added at the beginning, at the end one by one or multiples elements at once.

@Test
public void shouldAddElements() {
  final List<String> list = new LinkedList<>();
  list.add("John Smith");
  list.add("John Doe");
  list.add(0, "Donald Trump");
  list.addAll(ImmutableList.of("Barack Obama"));

  System.out.println(list);
}

The output is:

[Donald Trump, John Smith, John Doe, Barack Obama]


@Test
public void shouldAddStream() {
  final List<String> list = new LinkedList<>();

  ImmutableList.of("John Smith", "John Doe").forEach(list::add);

  System.out.println(list);
}

The output is:

[John Smith, John Doe]

The code above shows adding elements to the list using Java Streams and Lambdas expressions.

Removing Elements

Removing elements is pretty straight forward:


@Test
public void shouldRemoveElements() {
  final List<String> list = new LinkedList<>();
  list.add("John Smith");
  list.add("John Doe");

  list.remove("John Smith");

  System.out.println(list);
}

The following code prints:

[John Doe]

As the add operations, multiple elements can be removed at once.

Iterating



@Test
public void shouldIterate() {
  final List<String> list = new LinkedList<>();
  list.add("John Smith");
  list.add("John Doe");

  // Foreach loop
  for (final String name : list) {
    System.out.println(name);
  }

  // Traditional for loop
  for (int i = 0; i < list.size; i++) {
   System.out.println(list.get(i)); 
  }

  // Java Stream
  list.forEach(System.out::println);

  // Through iterator
  final Iterator<String> it = list.iterator();
  while (it.hasNext()) {
    System.out.println(it.next());
  }
}

The following code shows 4 different ways to iterate over list elements:

  • Foreach Loop: as List implements Iterable interface, lists can be iterated directly in for loops,
  • For Loop: traditional for loops using an integer index,
  • Java Streams: using Stream API introduced in Java 8,
  • Iterator: using list iterator.

These methods are roughly equivalent in term of performance. It depends on the context how you would iterate over the list.

Searching

One should be aware that Lists are not very well suited for search operations, except for sorted lists.

Unsorted List

When searching an unsorted list, the algorithm is pretty inefficient: it scans the whole list to find the wanted element. If you need to perform frequent searches in a collection, prefer a Set. The complexity of this operation is 0(n).


@Test
public void shouldSearch() {
  final List<String> list = new LinkedList<>();
  list.add("John Smith");
  list.add("John Doe");

  assertEquals(1, list.indexOf("John Doe"));
  assertTrue(list.stream().anyMatch("John Doe"::equals));
}

Sorted List

binary search algorithm must be avoided on LinkedList because it seeks elements by index which is inefficient on this kind of lists.


@Test
public void shouldSearch() {
  final List<String> list = new LinkedList<>();
  list.add("John Smith");
  list.add("John Doe");

  Collections.sort(list);
  assertEquals(0, Collections.binarySearch(list, "John Doe"));
}

Gladly enough, the Java binary search is capable of understanding this list does not support random access and switch to iterative search (O(n)):


public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

Best Use-Cases

LinkedList is best suited when:

  • Adding or Removing elements: only requires to move a few pointers for nodes around. ArrayList performs worse here because it needs to resize the internal array when growing (and copy all elements into it),
  • Reusing Iterators to insert / remove elements at specific index: These operations can be done in O(1) (where ArrayList needs to copy the internal array and shift all elements),

On the other side, LinkedList performs badly when:

  • Seeking an element by index: this operation takes O(n/2) on average,
  • Evaluating the list size: costs O(n) in all cases.

Alternatives

LinkedList is not the only implementation of the Java List interface. In fact there is also:

  • ArrayList: Array-based list implementation,
  • Vector: synchronized implementation based on a growable array of objects,
  • Stack: specialization of a Vector with LIFO strategy (Last In, First Out).

Depending on the nature of the operations frequently executed on the list instance, you should consider using one of these alternatives.

ArrayList is best suited for storing a fixed amount of elements, or when you need to evaluate list size frequently.

Vector is best suited in code executed concurrently by multiple threads because it’s synchronized. This data structure is pretty old (since JDK 1.0) and not used much these days.

Stack is great when you need to pop elements in the reverse order they have been pushed.

By - CTO.
Tags: Java Linked List Tutorial

Comments

David HB  

Those three sentences (see Properties 3) can not be true at the same time:

Random access (through an integer index) are fast (0(1)) because it’s backed by an array, Element removal is fast because elements are stored in nodes which can be removed quickly (change links from preceding and next nodes), Element insertion is efficient because only a new node must be allocated (no array resize like with LinkedList).

The reason: a removal of an element backed in an array and removing a double linked element is not the same.

Reply

Jerome
In reply to David HB
 

Hi David,

You are right! That’s a mistake in our article and has been corrected. Thanks for your feedback!

 

Thank you

Your comment has been submitted and will be published once it has been approved.

OK

OOPS!

Your post has failed. Please return to the page and try again. Thank You!

OK