In Java, LinkedList is one of the most important data structures provided by the Collection Framework. It belongs to the java.util package and is widely used to store and manipulate dynamic data where elements are frequently inserted and deleted.
LinkedList allows dynamic memory allocation, efficient insertions/deletions, and bidirectional traversal.
This tutorial will walk you through every concept of LinkedList with simple explanations and code examples. So, let’s start from the basics and build up to advanced usage.
What is LinkedList?
LinkedList is a doubly-linked list implementation of the List and Deque interfaces. It stores elements in nodes, and each node contains:
- The data
- A reference to the next node
- A reference to the previous node
Unlike ArrayList, it doesn’t use arrays internally. Instead, it uses node-based memory structure, making insertions and deletions efficient.
Internal Node Structure of LinkedList
Each node in a LinkedList contains:
- The actual data (value to store)
- A reference to the next node
- A reference to the previous node
This structure allows for efficient insertions and deletions because only the links between nodes need to be updated—there’s no need to shift elements like in an array.
Key Characteristics of LinkedList:
- Elements are stored in nodes
- Each node points to the next and previous node
- Great for frequent insertion and deletion
- Slower for random access compared to ArrayList
Class Declaration and Hierarchy of LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable
Implements:
List(supports index-based operations)Deque(can be used as a queue or stack)Cloneable(can be cloned)Serializable(can be serialized)
Here’s what a node looks like internally (simplified):
private static class Node<E> {
E item; // Data stored in the node
Node<E> next; // Reference to next node
Node<E> prev; // Reference to previous node
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Visual Representation
Here’s a visual of a LinkedList with 3 elements:
null ← [A] ⇄ [B] ⇄ [C] → null
- “A” is the head (first node)
- “C” is the tail (last node)
- Each node links to the one before and after it
Syntax – How to Declare a LinkedList
To use a LinkedList in Java, import it from the java.util package:
import java.util.LinkedList;
Then, you can declare and initialize it like this:
LinkedList<String> names = new LinkedList<>();
This creates an empty LinkedList of strings.
Basic Operations Example
LinkedList<String> names = new LinkedList<>();
// Adding elements
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Accessing elements
System.out.println(names.get(1)); // Output: Bob
// Removing elements
names.removeFirst(); // Removes "Alice"
names.removeLast(); // Removes "Charlie"
// Resulting list
System.out.println(names); // [Bob]
Key Features of LinkedList
| Feature | Description |
|---|---|
| 💾 Dynamic Size | Grows and shrinks automatically |
| 🔄 Bidirectional Traversal | Each node has pointers to the next and previous nodes |
| 🧠 Implements | List, Deque, Queue, and Cloneable |
| 🚀 Efficient | Ideal for frequently adding/removing elements |
| ❌ Not Synchronized | Use Collections.synchronizedList() for thread safety |
LinkedList Internal Working
LinkedList is a doubly-linked list implementation of the List, Deque, and Queue interfaces in Java’s Collections Framework.
It stores elements in nodes, where each node contains:
- the data (element),
- a reference to the next node, and
- a reference to the previous node.
Unlike ArrayList, which uses a contiguous block of memory (array), LinkedList uses a node-based structure, making insertions and deletions faster, especially at the beginning or middle of the list
Internal Node Structure
Each node in a LinkedList contains:
- The actual data (value to store)
- A reference to the next node
- A reference to the previous node
This structure allows for efficient insertions and deletions because only the links between nodes need to be updated—there’s no need to shift elements like in an array.
Here’s what a node looks like internally (simplified):
private static class Node<E> {
E item; // Data stored in the node
Node<E> next; // Reference to next node
Node<E> prev; // Reference to previous node
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Visual Representation
Here’s a visual of a LinkedList with 3 elements:
null ← [A] ⇄ [B] ⇄ [C] → null
- “A” is the head (first node)
- “C” is the tail (last node)
- Each node links to the one before and after it
🛠️ Syntax – How to Declare a LinkedList
To use a LinkedList in Java, import it from the java.util package:
import java.util.LinkedList;
Then, you can declare and initialize it like this:
LinkedList<String> names = new LinkedList<>();
This creates an empty LinkedList of strings.
Structure of a Doubly Linked List
Visual representation of a doubly linked list:
null ← [Node1] ⇄ [Node2] ⇄ [Node3] → null
Each node holds:
- The actual data (called
item) - A reference to the next node
- A reference to the previous node
Node Class in LinkedList
Internally, Java’s LinkedList maintains a private static class called Node inside the LinkedList class. Here’s a simplified version of it:
private static class Node<E> {
E item; // The actual data element
Node<E> next; // Pointer to the next node
Node<E> prev; // Pointer to the previous node
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
🔄 How Nodes Are Connected
When we add elements to a LinkedList, nodes are created and linked in both directions.
Let’s say you add 3 elements:
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
Behind the scenes, this happens:
- A
Nodeis created for “A” - Then a
Nodeis created for “B”, and “A” is linked to “B” - Then a
Nodeis created for “C”, and “B” is linked to “C”
Each link has both forward and backward references:
null ← [A] ⇄ [B] ⇄ [C] → null
headpoints to Atailpoints to C
Some LinkedList Operations Internal Working
Let’s see how key operations behave internally:
1. Adding an Element
Case 1: At the end (default)
list.add("D");
- A new
Nodeis created for “D” - “C”’s
nextpoints to “D” - “D”’s
prevpoints to “C” tailis updated to “D”
2. Adding at a Specific Position
list.add(1, "X"); // Inserts at index 1
- The list is traversed to the index
- New node “X” is inserted between A and B
- Pointers are adjusted:
- A.next = X
- X.prev = A
- X.next = B
- B.prev = X
3. Removing an Element
list.remove("B");
- The node containing “B” is found
- Its previous and next nodes are linked to each other
- A.next = C
- C.prev = A
- Node “B” becomes unreachable and is garbage collected
4. Accessing an Element
list.get(2);
- The list starts traversal from the head if the index is in the first half.
- It starts from the tail if the index is in the second half.
- This gives
O(n)time complexity.
This is unlike ArrayList, where access is done in O(1) time due to indexing in an array.
Internal Fields of LinkedList Class
Here’s a simplified version of what the LinkedList class maintains:
public class LinkedList<E> extends AbstractSequentialList<E> {
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
}
first: points to the head nodelast: points to the tail nodesize: total number of elements in the list
Diagram Summary
null ← [Head: "A"] ⇄ [Node: "B"] ⇄ [Tail: "C"] → null
A.prev = null,A.next = BB.prev = A,B.next = CC.prev = B,C.next = null
Garbage Collection & Memory Efficiency
When a node is removed, its reference to previous and next is set to null, and the object becomes eligible for garbage collection.
However, due to the overhead of maintaining two references (next and prev), LinkedList uses more memory per element than ArrayList.
Creating a LinkedList
import java.util.LinkedList;
LinkedList<String> fruits = new LinkedList<>();
fruits.add("Apple");
fruits.add("Banana");
System.out.println(fruits); // [Apple, Banana]
Commonly Used LinkedList Methods (with Java version badges)
| Method | Description | Java |
|---|---|---|
add(E e) | Adds an element to the end | ✅ |
add(int index, E element) | Adds at specific index | ✅ |
addFirst(E e) | Inserts at the beginning | ✅ |
addLast(E e) | Inserts at the end | ✅ |
get(int index) | Returns element at index | ✅ |
getFirst() / getLast() | Fetches first/last element | ✅ |
remove() | Removes head (first element) | ✅ |
remove(int index) | Removes element at index | ✅ |
removeFirst() / removeLast() | Removes from beginning/end | ✅ |
contains(Object o) | Checks existence | ✅ |
size() | Returns number of elements | ✅ |
clear() | Removes all elements | ✅ |
iterator() | Returns an iterator | ✅ |
descendingIterator() | Iterates in reverse order | ✅ |
stream() | Java 8 Stream API support | 🏷 Java 8+ |
Examples of LinkedList Usage
1. Add and Iterate
LinkedList<String> names = new LinkedList<>();
names.add("Alice");
names.add("Bob");
for(String name : names) {
System.out.println(name);
}
🔍 Explanation:
new LinkedList<>(): Creates an empty LinkedList of typeString.add("Alice"): Adds “Alice” to the end of the list.add("Bob"): Adds “Bob” after “Alice”.
At this point, the list looks like:
["Alice", "Bob"]
for-each loop: Iterates over each element in the list and prints them:
Output:
Alice
Bob
2. Add at First and Last
names.addFirst("Zara");
names.addLast("Eve");
System.out.println(names); // [Zara, Alice, Bob, Eve]
🔍 Explanation:
addFirst("Zara"): Inserts “Zara” at the beginning of the list.addLast("Eve"): Inserts “Eve” at the end of the list.
Now the list becomes:
["Zara", "Alice", "Bob", "Eve"]
System.out.println(names): Displays the current list.
Output:
[Zara, Alice, Bob, Eve]
3. Remove Elements
names.removeFirst(); // Removes Zara
names.removeLast(); // Removes Eve
🔍 Explanation:
removeFirst(): Removes the first element in the list (“Zara”).removeLast(): Removes the last element in the list (“Eve”).
Now the list becomes:
["Alice", "Bob"]
You’ve successfully removed elements from both ends of the linked list.
4. Using Java 8 Stream
names.stream()
.filter(n -> n.startsWith("A"))
.forEach(System.out::println); // Alice
🔍 Explanation:
This is using Java 8 Stream API to process the list in a functional style.
names.stream(): Converts theLinkedListinto a stream of elements.filter(n -> n.startsWith("A")): Filters only the names that start with the letter “A”.forEach(System.out::println): Prints each filtered element.
In our current list:
["Alice", "Bob"]
Only “Alice” starts with “A”, so output will be:
Alice
Summary Table
| Operation | Description | Result |
|---|---|---|
add("Alice"), add("Bob") | Adds elements at end | [“Alice”, “Bob”] |
addFirst("Zara") | Add at start | [“Zara”, “Alice”, “Bob”] |
addLast("Eve") | Add at end | [“Zara”, “Alice”, “Bob”, “Eve”] |
removeFirst() | Removes “Zara” | [“Alice”, “Bob”, “Eve”] |
removeLast() | Removes “Eve” | [“Alice”, “Bob”] |
stream().filter(...).forEach(...) | Stream filtering | Prints “Alice” |
Traversing a LinkedList
1. Using for-each loop
for(String fruit : fruits) {
System.out.println(fruit);
}
2. Using Iterator
Iterator<String> it = fruits.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
3. Using ListIterator (Forward and Backward)
ListIterator<String> listIt = fruits.listIterator();
System.out.println("Forward:");
while(listIt.hasNext()) {
System.out.println(listIt.next());
}
System.out.println("Backward:");
while(listIt.hasPrevious()) {
System.out.println(listIt.previous());
}
LinkedList vs ArrayList
| Feature | LinkedList | ArrayList |
|---|---|---|
| Internal Structure | Doubly Linked List | Dynamic Array |
| Access Time | O(n) | O(1) |
| Insert/Delete (Middle) | O(1) | O(n) |
| Memory Usage | More (extra pointers) | Less |
| Use Case | Frequent insertions/deletions | Frequent access/search |
Fail-Fast Behavior
Like other Java collections, LinkedList is fail-fast. If modified while iterating using a regular iterator, it throws ConcurrentModificationException.
Example:
Iterator<String> it = names.iterator();
while(it.hasNext()) {
names.add("New"); // Throws ConcurrentModificationException
}
Use ListIterator or CopyOnWriteArrayList for fail-safe behavior if needed.
Linked List Example – Maintaining a History Stack
Let’s simulate a browser history using LinkedList.
import java.util.LinkedList;
public class BrowserHistory {
public static void main(String[] args) {
LinkedList<String> history = new LinkedList<>();
history.add("google.com");
history.add("openai.com");
history.add("github.com");
System.out.println("Current History: " + history);
// Going back
String lastVisited = history.removeLast();
System.out.println("Back to: " + history.getLast());
System.out.println("You just left: " + lastVisited);
}
}
Output:
Current History: [google.com, openai.com, github.com]
Back to: openai.com
You just left: github.com
❓ FAQs on LinkedList
Q1. Is LinkedList thread-safe?
No, it’s not synchronized. You can make it thread-safe using:
Collections.synchronizedList(new LinkedList<>());
Q2. When should I use LinkedList over ArrayList?
Use LinkedList when:
- You do frequent insertions/deletions.
- You don’t need random access (index-based).
Q3. Can LinkedList contain null elements?
Yes, it allows any number of null elements.
Q4. What interfaces does LinkedList implement?
ListDequeQueueCloneableSerializable
Q5. Is LinkedList faster than ArrayList?
For insertions and deletions, yes.
For access by index, no—ArrayList is faster.
🎯 Summary
| Feature | LinkedList |
|---|---|
| Type | Doubly Linked |
| Use Case | Insert/delete-heavy |
| Java 8 Stream Support | ✅ Yes |
| Thread-Safe | ❌ No (use wrapper) |
| Random Access | ❌ No (O(n)) |
| Allows Nulls | ✅ Yes |
Conclusion
Java’s LinkedList is a versatile and efficient data structure when dealing with lots of insertions and deletions. While it’s slower in random access compared to ArrayList, it offers more flexibility when managing dynamic data.