Java > Java Collections Framework > List, Set, and Map Interfaces > ArrayList vs LinkedList

ArrayList vs LinkedList: Basic Operations

This snippet demonstrates the fundamental differences in performance between ArrayList and LinkedList when performing common operations like adding, getting, and removing elements. The example highlights scenarios where one data structure is more efficient than the other.

Code Demonstration: ArrayList vs LinkedList

This code compares the performance of `ArrayList` and `LinkedList` for common operations. 1. Initialization: Two lists, `arrayList` (an `ArrayList`) and `linkedList` (a `LinkedList`), are created. 2. Adding at the end: Elements are added to the end of both lists, and the time taken is measured. `ArrayList` is usually faster for this, as it uses a dynamically resizing array. 3. Adding at the beginning: Elements are inserted at the beginning of both lists. `LinkedList` is generally much faster here because it only needs to update the head, while `ArrayList` needs to shift all existing elements. 4. Accessing elements: Elements are accessed by their index using `get()`. `ArrayList` provides faster access because it can directly access elements using their index (constant time). `LinkedList` needs to traverse the list from the head to find the element (linear time). 5. Removing elements from the beginning: This part tests removing elements from the front of the list. `LinkedList` is more efficient because it only updates head pointer, but `ArrayList` has to shift all the remaining elements.

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

public class ListComparison {

    public static void main(String[] args) {
        // Initialize lists
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();

        int numElements = 100000; // Adjust number of elements for testing

        // --- Adding elements at the end ---
        long startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
            arrayList.add(i);
        }
        long endTime = System.nanoTime();
        System.out.println("ArrayList add (end): " + (endTime - startTime) / 1000000 + " ms");

        startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
            linkedList.add(i);
        }
        endTime = System.nanoTime();
        System.out.println("LinkedList add (end): " + (endTime - startTime) / 1000000 + " ms");

        // --- Adding elements at the beginning ---
        arrayList = new ArrayList<>();
        linkedList = new LinkedList<>();

        startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
            arrayList.add(0, i); // Add at the beginning
        }
        endTime = System.nanoTime();
        System.out.println("ArrayList add (beginning): " + (endTime - startTime) / 1000000 + " ms");

        startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
            linkedList.add(0, i); // Add at the beginning
        }
        endTime = System.nanoTime();
        System.out.println("LinkedList add (beginning): " + (endTime - startTime) / 1000000 + " ms");

        // --- Accessing elements (get) ---
        startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
            arrayList.get(i);
        }
        endTime = System.nanoTime();
        System.out.println("ArrayList get: " + (endTime - startTime) / 1000000 + " ms");

        startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
            linkedList.get(i);
        }
        endTime = System.nanoTime();
        System.out.println("LinkedList get: " + (endTime - startTime) / 1000000 + " ms");

        // --- Removing elements from the beginning ---
        startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
           arrayList.remove(0);  
        }
        endTime = System.nanoTime();
        System.out.println("ArrayList remove (beginning): " + (endTime - startTime) / 1000000 + " ms");

        arrayList = new ArrayList<>();
        for (int i = 0; i < numElements; i++) { arrayList.add(i);} //refill list to execute remove example below
        linkedList = new LinkedList<>();
        for (int i = 0; i < numElements; i++) { linkedList.add(i);} //refill list to execute remove example below

        startTime = System.nanoTime();
        for (int i = 0; i < numElements; i++) {
            linkedList.remove(0);
        }
        endTime = System.nanoTime();
        System.out.println("LinkedList remove (beginning): " + (endTime - startTime) / 1000000 + " ms");

    }
}

Concepts Behind the Snippet

  • ArrayList: Internally uses a dynamic array. Accessing elements by index is very fast (O(1)). Adding or removing elements in the middle or at the beginning requires shifting elements, which can be slow (O(n)).
  • LinkedList: Implemented as a doubly-linked list. Each element stores a reference to the previous and next element. Adding or removing elements at the beginning or middle is fast (O(1) if you have a reference to the element), but accessing elements by index requires traversing the list from the head or tail (O(n)).

Real-Life Use Case Section

  • ArrayList: Suitable when you need fast random access to elements, such as when implementing a data cache or when you're performing frequent lookups by index. For example, storing a list of product IDs where you often need to retrieve the product details by ID.
  • LinkedList: Useful when you need to frequently add or remove elements from the beginning or middle of the list, such as implementing a queue, a stack, or performing text editing operations where characters are frequently inserted or deleted. For example, managing a history of operations in an undo/redo feature.

Best Practices

  • Consider the frequency of operations: Analyze whether your application performs more random access or frequent insertions/deletions.
  • Use the appropriate data structure: Choose `ArrayList` for read-heavy operations and `LinkedList` for write-heavy operations, especially at the beginning or middle of the list.
  • Initial capacity for ArrayList: If you know the approximate number of elements beforehand, setting the initial capacity of the `ArrayList` can reduce the number of resize operations and improve performance.

Interview Tip

Be prepared to discuss the time complexity of different operations for both `ArrayList` and `LinkedList`. Understand the underlying data structures and explain the trade-offs between them. Specifically, be ready to explain why inserting at the *beginning* of an `ArrayList` is slow, and why random access of a `LinkedList` is slow.

When to use them

  • ArrayList: Use when you need fast random access or when you mainly perform add operations at the end of the list.
  • LinkedList: Use when you need frequent insertions or deletions at the beginning or middle of the list.

Memory footprint

  • ArrayList: Generally, `ArrayList` has a smaller memory footprint if the list is mostly full, as it only stores the elements contiguously in memory. However, it can waste memory if the allocated capacity is much larger than the actual number of elements.
  • LinkedList: `LinkedList` usually has a higher memory overhead because each element (node) stores references to the next and previous elements in addition to the data itself.

Alternatives

  • ArrayDeque: A double-ended queue implemented using a resizable array. It provides efficient add/remove operations at both ends. Can be a good alternative when you need queue or stack functionalities.
  • CopyOnWriteArrayList: A thread-safe variant of `ArrayList` where modifications create a new copy of the underlying array. Useful in concurrent scenarios where reads are much more frequent than writes.

Pros

  • ArrayList: Fast random access (O(1)), efficient storage if list is mostly full.
  • LinkedList: Fast insertions/deletions at the beginning/middle (O(1) if you have a reference to the node), dynamic size.

Cons

  • ArrayList: Slow insertions/deletions at the beginning/middle (O(n)), resizing can be expensive.
  • LinkedList: Slow random access (O(n)), higher memory overhead.

FAQ

  • Why is ArrayList faster for random access?

    ArrayList uses a contiguous block of memory (an array). Given the index, the memory address of the element can be directly calculated, allowing constant-time (O(1)) access.
  • Why is LinkedList faster for adding/removing at the beginning?

    LinkedList uses a linked structure, so adding or removing at the beginning only requires updating a few pointers (the head and the next pointer of the new/previous first element). This takes constant time (O(1)). ArrayList, on the other hand, requires shifting all existing elements to make space (for adding) or fill the gap (for removing), which takes linear time (O(n)).
  • When should I use LinkedList?

    Use LinkedList when you need frequent insertions or deletions at the beginning or middle of the list and random access is not a primary concern. Examples include implementing a queue or a stack.
  • Can I improve ArrayList's insertion performance?

    Yes, if you know the size of the list in advance, you can specify the initial capacity in the constructor. This prevents the array from being resized multiple times as you add elements. Also, consider adding elements at the end whenever possible.