LinkedList vs ArrayList in Java: Complete Comparison

Table of Contents

  1. Introduction
  2. Internal Implementation
  3. Performance Comparison
  4. Memory Usage
  5. Use Cases and Recommendations
  6. API and Method Differences
  7. Practical Examples
  8. Best Practices

Introduction

Both ArrayList and LinkedList implement the List interface but have fundamentally different internal implementations that lead to significant performance differences in various operations.

Basic Characteristics

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class BasicComparison {
public static void main(String[] args) {
// Both implement List interface
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
// Common operations work the same way
arrayList.add("Hello");
linkedList.add("Hello");
System.out.println("ArrayList: " + arrayList);
System.out.println("LinkedList: " + linkedList);
}
}

Internal Implementation

ArrayList Internal Structure

// ArrayList is backed by a dynamic array
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// Internal array that stores elements
transient Object[] elementData;
// Current size of the list
private int size;
// Default initial capacity
private static final int DEFAULT_CAPACITY = 10;
}

LinkedList Internal Structure

// LinkedList is implemented as a doubly-linked list
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
// Node class for linked list elements
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
// First and last nodes
transient Node<E> first;
transient Node<E> last;
transient int size = 0;
}

Visualization

ArrayList: [0][1][2][3][4][5][6][7][8][9]... (contiguous memory)
LinkedList: first ↔ [0] ↔ [1] ↔ [2] ↔ [3] ↔ ... ↔ [9] ↔ last
(each node has reference to next and previous)

Performance Comparison

Time Complexity Analysis

OperationArrayListLinkedListWinner
get(int index)O(1)O(n)ArrayList
add(E element)O(1) amortizedO(1)Similar
add(int index, E element)O(n)O(1)LinkedList
remove(int index)O(n)O(1)LinkedList
remove(Object o)O(n)O(n)Similar
set(int index, E element)O(1)O(n)ArrayList
contains(Object o)O(n)O(n)Similar

Performance Testing Code

import java.util.*;
public class PerformanceComparison {
private static final int SIZE = 100000;
public static void main(String[] args) {
// Test random access (get)
testRandomAccess();
// Test insertion at beginning
testInsertAtBeginning();
// Test insertion at end
testInsertAtEnd();
// Test insertion in middle
testInsertInMiddle();
// Test iteration
testIteration();
}
public static void testRandomAccess() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// Fill both lists
for (int i = 0; i < SIZE; i++) {
arrayList.add(i);
linkedList.add(i);
}
long startTime = System.nanoTime();
for (int i = 0; i < SIZE; i++) {
arrayList.get(i);
}
long arrayListTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
for (int i = 0; i < SIZE; i++) {
linkedList.get(i);
}
long linkedListTime = System.nanoTime() - startTime;
System.out.println("Random Access (get):");
System.out.printf("ArrayList:  %10d ns%n", arrayListTime);
System.out.printf("LinkedList: %10d ns%n", linkedListTime);
System.out.printf("ArrayList is %.1f times faster%n%n", 
(double)linkedListTime / arrayListTime);
}
public static void testInsertAtBeginning() {
System.out.println("Insert at Beginning:");
long startTime = System.nanoTime();
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < SIZE; i++) {
arrayList.add(0, i); // Insert at beginning
}
long arrayListTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < SIZE; i++) {
linkedList.add(0, i); // Insert at beginning
}
long linkedListTime = System.nanoTime() - startTime;
System.out.printf("ArrayList:  %10d ns%n", arrayListTime);
System.out.printf("LinkedList: %10d ns%n", linkedListTime);
System.out.printf("LinkedList is %.1f times faster%n%n", 
(double)arrayListTime / linkedListTime);
}
public static void testInsertAtEnd() {
System.out.println("Insert at End:");
long startTime = System.nanoTime();
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < SIZE; i++) {
arrayList.add(i); // Insert at end
}
long arrayListTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < SIZE; i++) {
linkedList.add(i); // Insert at end
}
long linkedListTime = System.nanoTime() - startTime;
System.out.printf("ArrayList:  %10d ns%n", arrayListTime);
System.out.printf("LinkedList: %10d ns%n", linkedListTime);
System.out.printf("ArrayList is %.1f times faster%n%n", 
(double)linkedListTime / arrayListTime);
}
public static void testInsertInMiddle() {
System.out.println("Insert in Middle:");
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// Pre-fill lists
for (int i = 0; i < SIZE; i++) {
arrayList.add(i);
linkedList.add(i);
}
long startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
arrayList.add(SIZE/2, i); // Insert in middle
}
long arrayListTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
linkedList.add(SIZE/2, i); // Insert in middle
}
long linkedListTime = System.nanoTime() - startTime;
System.out.printf("ArrayList:  %10d ns%n", arrayListTime);
System.out.printf("LinkedList: %10d ns%n", linkedListTime);
System.out.printf("LinkedList is %.1f times faster%n%n", 
(double)arrayListTime / linkedListTime);
}
public static void testIteration() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < SIZE; i++) {
arrayList.add(i);
linkedList.add(i);
}
// Test foreach iteration
long startTime = System.nanoTime();
for (Integer num : arrayList) {
// Just iterating
}
long arrayListTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
for (Integer num : linkedList) {
// Just iterating
}
long linkedListTime = System.nanoTime() - startTime;
System.out.println("Iteration (foreach):");
System.out.printf("ArrayList:  %10d ns%n", arrayListTime);
System.out.printf("LinkedList: %10d ns%n", linkedListTime);
System.out.printf("ArrayList is %.1f times faster%n%n", 
(double)linkedListTime / arrayListTime);
}
}

Memory Usage

Memory Analysis

import java.util.*;
public class MemoryUsageAnalysis {
public static void analyzeMemoryUsage() {
final int ELEMENTS = 100000;
// Calculate memory usage for ArrayList
long memoryBefore = Runtime.getRuntime().totalMemory() - 
Runtime.getRuntime().freeMemory();
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < ELEMENTS; i++) {
arrayList.add(i);
}
long memoryAfter = Runtime.getRuntime().totalMemory() - 
Runtime.getRuntime().freeMemory();
long arrayListMemory = memoryAfter - memoryBefore;
// Force garbage collection
System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) {}
// Calculate memory usage for LinkedList
memoryBefore = Runtime.getRuntime().totalMemory() - 
Runtime.getRuntime().freeMemory();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < ELEMENTS; i++) {
linkedList.add(i);
}
memoryAfter = Runtime.getRuntime().totalMemory() - 
Runtime.getRuntime().freeMemory();
long linkedListMemory = memoryAfter - memoryBefore;
System.out.println("Memory Usage Analysis (" + ELEMENTS + " elements):");
System.out.printf("ArrayList:  %10d bytes%n", arrayListMemory);
System.out.printf("LinkedList: %10d bytes%n", linkedListMemory);
System.out.printf("ArrayList uses %.1f%% of LinkedList memory%n", 
(double)arrayListMemory / linkedListMemory * 100);
}
public static void calculateTheoreticalMemory() {
// Theoretical memory calculation for 1000 Integer objects
// ArrayList: array overhead + references
int arrayListOverhead = 16; // Object header
int arrayRefs = 1000 * 4;   // 1000 references (4 bytes each)
int arrayBuffer = 1000 * 4; // Typical buffer overhead
// LinkedList: node overhead for each element
int nodeOverhead = 24;      // Node object header (12) + references (3*4)
int linkedListRefs = 1000 * nodeOverhead;
System.out.println("\nTheoretical Memory Usage (1000 Integer elements):");
System.out.println("ArrayList:  " + (arrayListOverhead + arrayRefs + arrayBuffer) + " bytes");
System.out.println("LinkedList: " + (linkedListRefs) + " bytes");
}
public static void main(String[] args) {
analyzeMemoryUsage();
calculateTheoreticalMemory();
}
}

Memory Breakdown

  • ArrayList: Array overhead + object references
  • LinkedList: For each element: Node object (header + 3 references) + actual element

Use Cases and Recommendations

When to Use ArrayList

public class ArrayListUseCases {
// 1. Frequent random access
public static void frequentRandomAccess() {
List<String> userList = new ArrayList<>();
// Load users
for (int i = 0; i < 10000; i++) {
userList.add("User" + i);
}
// Frequent random access - ArrayList excels here
String user5000 = userList.get(5000); // O(1)
String user9999 = userList.get(9999); // O(1)
System.out.println("Random access example - ArrayList is optimal");
}
// 2. Mostly add/remove at the end
public static void stackLikeBehavior() {
List<Transaction> transactionLog = new ArrayList<>();
// Add transactions (at end)
transactionLog.add(new Transaction("deposit", 1000));
transactionLog.add(new Transaction("withdraw", 500));
// Process in order (iteration is fast)
for (Transaction t : transactionLog) {
t.process();
}
// Occasionally remove from end
if (!transactionLog.isEmpty()) {
transactionLog.remove(transactionLog.size() - 1);
}
}
// 3. Memory efficiency important
public static void memorySensitiveApplication() {
// When dealing with large datasets
List<Double> largeDataset = new ArrayList<>(1000000);
// ArrayList uses less memory than LinkedList
for (int i = 0; i < 1000000; i++) {
largeDataset.add(Math.random());
}
System.out.println("Memory-efficient storage with ArrayList");
}
}

When to Use LinkedList

import java.util.*;
public class LinkedListUseCases {
// 1. Frequent insertions/deletions in the middle
public static void middleOperations() {
LinkedList<Task> taskQueue = new LinkedList<>();
// Add initial tasks
taskQueue.add(new Task("Low", "Cleanup"));
taskQueue.add(new Task("High", "Emergency fix"));
// Insert high-priority task in the middle
taskQueue.add(1, new Task("Medium", "Regular task")); // O(1)
// Remove from middle
taskQueue.remove(1); // O(1)
System.out.println("Middle operations - LinkedList excels");
}
// 2. Implementing queues/deques
public static void queueOperations() {
// LinkedList implements Deque interface
Deque<String> messageQueue = new LinkedList<>();
// Use as FIFO queue
messageQueue.offer("Message 1");
messageQueue.offer("Message 2");
messageQueue.offer("Message 3");
// Process in order
while (!messageQueue.isEmpty()) {
String message = messageQueue.poll();
System.out.println("Processing: " + message);
}
// Use as LIFO stack
Deque<String> stack = new LinkedList<>();
stack.push("Item 1");
stack.push("Item 2");
stack.push("Item 3");
while (!stack.isEmpty()) {
String item = stack.pop();
System.out.println("Popped: " + item);
}
}
// 3. When you need Deque operations
public static void dequeOperations() {
LinkedList<Integer> deque = new LinkedList<>();
// Add to both ends efficiently
deque.addFirst(1);  // O(1)
deque.addLast(2);   // O(1)
deque.offerFirst(0); // O(1)
deque.offerLast(3);  // O(1)
// Remove from both ends
Integer first = deque.removeFirst(); // O(1)
Integer last = deque.removeLast();   // O(1)
// Peek at ends
Integer peekFirst = deque.peekFirst(); // O(1)
Integer peekLast = deque.peekLast();   // O(1)
System.out.println("Deque operations are efficient with LinkedList");
}
}
class Task {
String priority;
String description;
public Task(String priority, String description) {
this.priority = priority;
this.description = description;
}
}

API and Method Differences

Common Methods Comparison

import java.util.*;
public class MethodComparison {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
// Both support basic List operations
arrayList.add("A");
linkedList.add("A");
arrayList.add(0, "B"); // Insert at beginning
linkedList.add(0, "B"); // Insert at beginning
// But LinkedList has additional Deque methods
LinkedList<String> linkedListDeque = (LinkedList<String>) linkedList;
// Deque-specific methods (only in LinkedList)
linkedListDeque.addFirst("First");
linkedListDeque.addLast("Last");
linkedListDeque.offerFirst("OfferFirst");
linkedListDeque.offerLast("OfferLast");
String first = linkedListDeque.getFirst();
String last = linkedListDeque.getLast();
System.out.println("LinkedList as Deque: " + linkedListDeque);
}
}

Special LinkedList Methods

public class LinkedListSpecificMethods {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
// Deque methods (add/remove from both ends)
list.addFirst("First");     // Equivalent to push()
list.addLast("Last");       // Equivalent to add()
list.offerFirst("OfferFirst");
list.offerLast("OfferLast");
// Peek methods (retrieve without removal)
String first = list.getFirst();    // Throws exception if empty
String firstSafe = list.peekFirst(); // Returns null if empty
String last = list.getLast();
String lastSafe = list.peekLast();
// Remove methods
String removedFirst = list.removeFirst(); // Throws exception if empty
String removedFirstSafe = list.pollFirst(); // Returns null if empty
String removedLast = list.removeLast();
String removedLastSafe = list.pollLast();
// Stack operations (LIFO)
list.push("Pushed");        // Add to front - same as addFirst()
String popped = list.pop(); // Remove from front - same as removeFirst()
System.out.println("Final list: " + list);
}
}

Practical Examples

Real-World Scenario: Student Management System

import java.util.*;
class Student {
private int id;
private String name;
private double gpa;
public Student(int id, String name, double gpa) {
this.id = id;
this.name = name;
this.gpa = gpa;
}
// Getters
public int getId() { return id; }
public String getName() { return name; }
public double getGpa() { return gpa; }
@Override
public String toString() {
return String.format("Student{id=%d, name='%s', gpa=%.2f}", id, name, gpa);
}
}
public class StudentManagementSystem {
// Use ArrayList for student catalog (frequent search/access)
private List<Student> studentCatalog = new ArrayList<>();
// Use LinkedList for waitlist (frequent insertions/removals)
private LinkedList<Student> courseWaitlist = new LinkedList<>();
public void addStudentToCatalog(Student student) {
studentCatalog.add(student); // O(1) amortized
Collections.sort(studentCatalog, Comparator.comparing(Student::getName));
}
public Student findStudentById(int id) {
// Linear search - both are O(n), but ArrayList has better locality
for (Student s : studentCatalog) {
if (s.getId() == id) {
return s;
}
}
return null;
}
public Student getStudentByIndex(int index) {
return studentCatalog.get(index); // O(1) - ArrayList excels
}
public void addToWaitlist(Student student) {
courseWaitlist.addLast(student); // O(1) - LinkedList excels
}
public Student removeFromWaitlist() {
return courseWaitlist.pollFirst(); // O(1) - LinkedList excels
}
public void prioritizeWaitlistStudent(Student student) {
// Remove from current position (if exists) and add to front
courseWaitlist.remove(student); // O(n) - have to search
courseWaitlist.addFirst(student); // O(1) - LinkedList excels
}
public static void main(String[] args) {
StudentManagementSystem sms = new StudentManagementSystem();
// Add students to catalog
sms.addStudentToCatalog(new Student(1, "Alice", 3.8));
sms.addStudentToCatalog(new Student(2, "Bob", 3.5));
sms.addStudentToCatalog(new Student(3, "Charlie", 3.9));
// Waitlist operations
Student david = new Student(4, "David", 3.7);
Student eve = new Student(5, "Eve", 3.6);
sms.addToWaitlist(david);
sms.addToWaitlist(eve);
sms.prioritizeWaitlistStudent(eve); // Eve moves to front
// Process waitlist
Student nextStudent = sms.removeFromWaitlist();
System.out.println("Next student: " + nextStudent);
}
}

Real-World Scenario: Undo/Redo Functionality

import java.util.*;
class TextEditor {
private LinkedList<String> undoStack = new LinkedList<>();
private LinkedList<String> redoStack = new LinkedList<>();
private String currentText = "";
public void type(String text) {
// Save current state to undo stack
undoStack.push(currentText);
redoStack.clear(); // Clear redo stack when new action is performed
currentText += text;
System.out.println("Typed: " + text + " | Current: " + currentText);
}
public void undo() {
if (!undoStack.isEmpty()) {
// Save current state to redo stack
redoStack.push(currentText);
// Restore previous state
currentText = undoStack.pop();
System.out.println("Undo | Current: " + currentText);
}
}
public void redo() {
if (!redoStack.isEmpty()) {
// Save current state to undo stack
undoStack.push(currentText);
// Restore redone state
currentText = redoStack.pop();
System.out.println("Redo | Current: " + currentText);
}
}
// LinkedList is perfect for stack operations (LIFO)
public void demonstrateStackOperations() {
System.out.println("\n--- Stack Operations Demo ---");
type("Hello");
type(" World");
type("!");
undo();
undo();
redo();
undo();
}
}
public class UndoRedoDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
editor.demonstrateStackOperations();
}
}

Best Practices

Choosing Between ArrayList and LinkedList

public class ListSelectionGuide {
public static <T> List<T> chooseOptimalList(UsagePattern pattern) {
switch (pattern) {
case FREQUENT_RANDOM_ACCESS:
case MEMORY_SENSITIVE:
case ITERATION_HEAVY:
return new ArrayList<>();
case FREQUENT_INSERTIONS_DELETIONS:
case QUEUE_OPERATIONS:
case MIDDLE_OPERATIONS:
return new LinkedList<>();
default:
return new ArrayList<>(); // Default choice
}
}
public static void demonstrateOptimalChoices() {
// Scenario 1: Read-heavy application (e.g., configuration)
List<Configuration> configList = chooseOptimalList(UsagePattern.FREQUENT_RANDOM_ACCESS);
// Scenario 2: Real-time data processing queue
List<DataPacket> dataQueue = chooseOptimalList(UsagePattern.QUEUE_OPERATIONS);
// Scenario 3: Large dataset with occasional updates
List<BigData> bigDataList = chooseOptimalList(UsagePattern.MEMORY_SENSITIVE);
}
enum UsagePattern {
FREQUENT_RANDOM_ACCESS,    // ArrayList
FREQUENT_INSERTIONS_DELETIONS, // LinkedList  
MEMORY_SENSITIVE,          // ArrayList
ITERATION_HEAVY,           // ArrayList
QUEUE_OPERATIONS,          // LinkedList
MIDDLE_OPERATIONS          // LinkedList
}
}

Performance Optimization Tips

public class OptimizationTips {
// 1. Pre-size ArrayList when possible
public static void optimizedArrayList() {
// Bad - resizes multiple times
List<String> badList = new ArrayList<>();
// Good - pre-sized
List<String> goodList = new ArrayList<>(1000);
// Even better - use exact size if known
String[] data = getDataFromSource();
List<String> bestList = new ArrayList<>(Arrays.asList(data));
}
// 2. Use the right iterator
public static void efficientIteration() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// Fill lists...
for (int i = 0; i < 1000; i++) {
arrayList.add(i);
linkedList.add(i);
}
// For ArrayList - foreach is optimal
long start = System.nanoTime();
for (int num : arrayList) {
// Process
}
System.out.println("ArrayList foreach: " + (System.nanoTime() - start));
// For LinkedList - ListIterator might be better for certain operations
start = System.nanoTime();
ListIterator<Integer> iterator = linkedList.listIterator();
while (iterator.hasNext()) {
int num = iterator.next();
// Process
}
System.out.println("LinkedList ListIterator: " + (System.nanoTime() - start));
}
// 3. Avoid indexed operations on LinkedList
public static void linkedListAntiPattern() {
LinkedList<String> list = new LinkedList<>();
// BAD - O(n) for each access
for (int i = 0; i < list.size(); i++) {
String item = list.get(i); // SLOW!
}
// GOOD - O(1) for each step
for (String item : list) {
// Fast iteration
}
}
// 4. Consider alternative data structures
public static void considerAlternatives() {
// For queue operations - ArrayDeque might be better than LinkedList
Deque<String> arrayDeque = new ArrayDeque<>(); // Often faster than LinkedList
// For sorted data - TreeSet or PriorityQueue
SortedSet<String> sortedSet = new TreeSet<>();
// For frequent contains checks - HashSet
Set<String> hashSet = new HashSet<>();
}
private static String[] getDataFromSource() {
return new String[]{"A", "B", "C"};
}
}

Summary Decision Chart

public class ListDecisionChart {
public static String recommendListType(boolean frequentRandomAccess,
boolean frequentInsertDelete,
boolean memorySensitive,
boolean needQueueOperations) {
if (frequentRandomAccess && !frequentInsertDelete) {
return "ArrayList - Excellent for random access";
}
if (frequentInsertDelete && !frequentRandomAccess) {
return "LinkedList - Excellent for insertions/deletions";
}
if (memorySensitive) {
return "ArrayList - More memory efficient";
}
if (needQueueOperations) {
return "LinkedList - Implements Deque interface";
}
// Default recommendation
return "ArrayList - Good general-purpose choice";
}
public static void main(String[] args) {
System.out.println(recommendListType(true, false, true, false));
System.out.println(recommendListType(false, true, false, true));
System.out.println(recommendListType(true, true, false, false));
}
}

Key Takeaways

  1. ArrayList is generally better for:
  • Random access (get/set)
  • Memory efficiency
  • Iteration performance
  • Most common use cases
  1. LinkedList is better for:
  • Frequent insertions/deletions in the middle
  • Queue/stack operations (when used as Deque)
  • When you need Deque-specific operations
  1. Default Choice: Start with ArrayList, switch to LinkedList only when you have specific needs that LinkedList addresses better.
  2. Performance: Always profile your specific use case - theoretical complexity doesn't always translate to real-world performance due to CPU cache effects and other factors.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper