🔄 Java List Interface and Implementations
📚 Introduction to Java List Interface
The List interface is one of the most fundamental and widely used interfaces in the Java Collections Framework. It represents an ordered collection of elements where duplicates are allowed. Unlike sets, lists maintain insertion order and allow positional access and manipulation of elements.
As part of the Java Collections Framework, the List interface extends the Collection interface and adds methods specifically designed for ordered collections. This makes lists perfect for scenarios where you need to:
- Maintain elements in a specific order
- Access elements by their position (index)
- Allow duplicate elements
- Insert or remove elements at specific positions
The Java Collections Framework provides several implementations of the List interface, each with its own characteristics, advantages, and trade-offs:
- ArrayList: A resizable array implementation that offers fast random access but slower insertions and deletions.
- LinkedList: A doubly-linked list implementation that provides fast insertions and deletions but slower random access.
- Vector: A synchronized (thread-safe) version of ArrayList.
- Stack: A subclass of Vector that implements the Last-In-First-Out (LIFO) stack data structure.
In this tutorial, we'll explore the List interface in depth, examine its various implementations, and learn how to use them effectively in your Java applications.
🧩 The List Interface in Java: Core Methods and Functionality
The List interface defines several methods beyond those inherited from the Collection interface. These methods take advantage of the ordered nature of lists and provide powerful functionality for working with indexed collections.
Core Methods of the List Interface in Java
public interface List<E> extends Collection<E> {
// Positional access
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
// Search
int indexOf(Object o);
int lastIndexOf(Object o);
// List iteration
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
// View
List<E> subList(int fromIndex, int toIndex);
}
Let's explore these methods with examples:
Basic List Operations
import java.util.*;
public class BasicListOperationsDemo {
public static void main(String[] args) {
// Create a list
List<String> fruits = new ArrayList<>();
// Add elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
System.out.println("Initial list: " + fruits);
// Access by index
String second = fruits.get(1);
System.out.println("Second fruit: " + second);
// Modify an element
fruits.set(1, "Blueberry");
System.out.println("After replacing Banana with Blueberry: " + fruits);
// Insert at specific position
fruits.add(1, "Banana");
System.out.println("After inserting Banana at index 1: " + fruits);
// Remove by index
String removed = fruits.remove(2);
System.out.println("Removed: " + removed);
System.out.println("After removing element at index 2: " + fruits);
// Remove by object
boolean wasRemoved = fruits.remove("Apple");
System.out.println("Was Apple removed? " + wasRemoved);
System.out.println("After removing Apple: " + fruits);
// Find position of an element
fruits.add("Cherry");
fruits.add("Banana");
System.out.println("Updated list: " + fruits);
int firstBanana = fruits.indexOf("Banana");
int lastBanana = fruits.lastIndexOf("Banana");
System.out.println("First occurrence of Banana: index " + firstBanana);
System.out.println("Last occurrence of Banana: index " + lastBanana);
// Check if list contains an element
boolean containsCherry = fruits.contains("Cherry");
System.out.println("Contains Cherry? " + containsCherry);
// Get size of list
int size = fruits.size();
System.out.println("List size: " + size);
// Clear the list
fruits.clear();
System.out.println("After clearing: " + fruits);
System.out.println("Is list empty? " + fruits.isEmpty());
}
}
Output:
Initial list: [Apple, Banana, Cherry]
Second fruit: Banana
After replacing Banana with Blueberry: [Apple, Blueberry, Cherry]
After inserting Banana at index 1: [Apple, Banana, Blueberry, Cherry]
Removed: Blueberry
After removing element at index 2: [Apple, Banana, Cherry]
Was Apple removed? true
After removing Apple: [Banana, Cherry]
Updated list: [Banana, Cherry, Banana]
First occurrence of Banana: index 0
Last occurrence of Banana: index 2
Contains Cherry? true
List size: 3
After clearing: []
Is list empty? true
Java List Iteration
Lists provide multiple ways to iterate through their elements:
import java.util.*;
public class ListIterationDemo {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
// 1. Using enhanced for loop
System.out.println("Using enhanced for loop:");
for (String fruit : fruits) {
System.out.println(fruit);
}
// 2. Using traditional for loop with index
System.out.println("\nUsing traditional for loop:");
for (int i = 0; i < fruits.size(); i++) {
System.out.println(i + ": " + fruits.get(i));
}
// 3. Using Iterator
System.out.println("\nUsing Iterator:");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
// 4. Using ListIterator (can move both forward and backward)
System.out.println("\nUsing ListIterator (forward):");
ListIterator<String> listIterator = fruits.listIterator();
while (listIterator.hasNext()) {
int index = listIterator.nextIndex();
String fruit = listIterator.next();
System.out.println(index + ": " + fruit);
}
System.out.println("\nUsing ListIterator (backward):");
while (listIterator.hasPrevious()) {
int index = listIterator.previousIndex();
String fruit = listIterator.previous();
System.out.println(index + ": " + fruit);
}
// 5. Using forEach method (Java 8+)
System.out.println("\nUsing forEach method:");
fruits.forEach(fruit -> System.out.println(fruit));
// 6. Using Stream API (Java 8+)
System.out.println("\nUsing Stream API:");
fruits.stream()
.forEach(System.out::println);
}
}
Output:
Using enhanced for loop:
Apple
Banana
Cherry
Using traditional for loop:
0: Apple
1: Banana
2: Cherry
Using Iterator:
Apple
Banana
Cherry
Using ListIterator (forward):
0: Apple
1: Banana
2: Cherry
Using ListIterator (backward):
2: Cherry
1: Banana
0: Apple
Using forEach method:
Apple
Banana
Cherry
Using Stream API:
Apple
Banana
Cherry
Java List Views with subList()
The subList()
method returns a view of a portion of the list:
import java.util.*;
public class SubListDemo {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
fruits.add("Dragonfruit");
fruits.add("Elderberry");
System.out.println("Original list: " + fruits);
// Get a sublist view (fromIndex inclusive, toIndex exclusive)
List<String> subList = fruits.subList(1, 4);
System.out.println("Sublist (1-4): " + subList);
// Modifying the sublist affects the original list
subList.set(0, "Blackberry"); // Replace Banana with Blackberry
System.out.println("After modifying sublist: " + fruits);
// Adding to the sublist affects the original list
subList.add("Cantaloupe"); // Add at the end of sublist
System.out.println("After adding to sublist: " + fruits);
// Removing from the sublist affects the original list
subList.remove("Cherry");
System.out.println("After removing from sublist: " + fruits);
// Clearing the sublist affects only that portion of the original list
subList.clear();
System.out.println("After clearing sublist: " + fruits);
// WARNING: Modifying the original list directly while working with a
// sublist can cause ConcurrentModificationException
List<String> anotherSubList = fruits.subList(0, 2);
System.out.println("Another sublist: " + anotherSubList);
try {
fruits.add("Fig"); // Modify original list
System.out.println(anotherSubList); // Try to access sublist
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException caught!");
System.out.println("Cannot access sublist after modifying original list");
}
}
}
Output:
Original list: [Apple, Banana, Cherry, Dragonfruit, Elderberry]
Sublist (1-4): [Banana, Cherry, Dragonfruit]
After modifying sublist: [Apple, Blackberry, Cherry, Dragonfruit, Elderberry]
After adding to sublist: [Apple, Blackberry, Cherry, Dragonfruit, Cantaloupe, Elderberry]
After removing from sublist: [Apple, Blackberry, Dragonfruit, Cantaloupe, Elderberry]
After clearing sublist: [Apple, Elderberry]
Another sublist: [Apple, Elderberry]
ConcurrentModificationException caught!
Cannot access sublist after modifying original list
Bulk Operations in Java List
Lists inherit bulk operations from the Collection interface:
import java.util.*;
public class ListBulkOperationsDemo {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
List<String> moreFruits = new ArrayList<>();
moreFruits.add("Banana");
moreFruits.add("Dragonfruit");
System.out.println("List 1: " + fruits);
System.out.println("List 2: " + moreFruits);
// Add all elements from another collection
List<String> combined = new ArrayList<>(fruits);
combined.addAll(moreFruits);
System.out.println("After addAll: " + combined);
// Remove all elements that exist in another collection
List<String> difference = new ArrayList<>(combined);
difference.removeAll(moreFruits);
System.out.println("After removeAll: " + difference);
// Retain only elements that exist in another collection
List<String> intersection = new ArrayList<>(combined);
intersection.retainAll(moreFruits);
System.out.println("After retainAll: " + intersection);
// Check if list contains all elements from another collection
boolean containsAll = fruits.containsAll(moreFruits);
System.out.println("fruits contains all elements from moreFruits? " + containsAll);
// Remove elements that match a condition (Java 8+)
List<String> filtered = new ArrayList<>(combined);
filtered.removeIf(s -> s.startsWith("B"));
System.out.println("After removing elements starting with 'B': " + filtered);
}
}
Output:
List 1: [Apple, Banana, Cherry]
List 2: [Banana, Dragonfruit]
After addAll: [Apple, Banana, Cherry, Banana, Dragonfruit]
After removeAll: [Apple, Cherry]
After retainAll: [Banana, Banana, Dragonfruit]
fruits contains all elements from moreFruits? false
After removing elements starting with 'B': [Apple, Cherry, Dragonfruit]
🔍 Java List Implementations in Detail
Now let's explore the main implementations of the List interface in detail.
ArrayList in Java
ArrayList is the most commonly used List implementation. It's backed by a resizable array, providing fast random access but slower insertions and deletions, especially in the middle of the list.
import java.util.*;
public class ArrayListDemo {
public static void main(String[] args) {
// Creating an ArrayList
ArrayList<String> fruits = new ArrayList<>();
// Adding elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
System.out.println("ArrayList: " + fruits);
// Creating with initial capacity
ArrayList<Integer> numbers = new ArrayList<>(20);
for (int i = 0; i < 10; i++) {
numbers.add(i);
}
System.out.println("Numbers: " + numbers);
// Creating from another collection
ArrayList<String> fruitsCopy = new ArrayList<>(fruits);
System.out.println("Copy of fruits: " + fruitsCopy);
// Ensuring capacity
fruits.ensureCapacity(100); // Ensures the list can hold at least 100 elements
// Trimming to size (reduces capacity to current size)
fruitsCopy.trimToSize();
// Performance characteristics
long startTime = System.nanoTime();
// Fast random access
for (int i = 0; i < 10000; i++) {
if (i < numbers.size()) {
numbers.get(i % numbers.size());
}
}
long accessTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
// Slower insertions at beginning
for (int i = 0; i < 1000; i++) {
numbers.add(0, i);
}
long insertTime = System.nanoTime() - startTime;
System.out.println("Time for 10000 random accesses: " + accessTime + " ns");
System.out.println("Time for 1000 insertions at beginning: " + insertTime + " ns");
// ArrayList is not synchronized
System.out.println("Is ArrayList synchronized? No");
// Creating a synchronized ArrayList
List<String> syncFruits = Collections.synchronizedList(fruits);
System.out.println("Synchronized list: " + syncFruits.getClass().getName());
}
}
Output (times will vary):
ArrayList: [Apple, Banana, Cherry]
Numbers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Copy of fruits: [Apple, Banana, Cherry]
Time for 10000 random accesses: 1234567 ns
Time for 1000 insertions at beginning: 7654321 ns
Is ArrayList synchronized? No
Synchronized list: java.util.Collections$SynchronizedList
ArrayList Internal Working in Java
Understanding how ArrayList works internally can help you use it more effectively:
import java.util.*;
public class ArrayListInternalsDemo {
public static void main(String[] args) {
// ArrayList is backed by an array
// When created, it has a default capacity (usually 10)
ArrayList<Integer> list = new ArrayList<>();
System.out.println("Initial capacity: 10 (default)");
// As elements are added, the array fills up
for (int i = 0; i < 10; i++) {
list.add(i);
}
System.out.println("List after adding 10 elements: " + list);
System.out.println("Size: " + list.size());
System.out.println("Capacity: 10");
// When capacity is reached and a new element is added,
// ArrayList creates a new, larger array (typically 1.5x size)
// and copies all elements to it
list.add(10);
System.out.println("List after adding 11th element: " + list);
System.out.println("Size: " + list.size());
System.out.println("New capacity: 15 (1.5x growth)");
// When elements are removed, the array size doesn't shrink
for (int i = 0; i < 5; i++) {
list.remove(list.size() - 1);
}
System.out.println("List after removing 5 elements: " + list);
System.out.println("Size: " + list.size());
System.out.println("Capacity still: 15 (doesn't shrink automatically)");
// trimToSize() can be used to shrink the capacity to match the size
((ArrayList<Integer>)list).trimToSize();
System.out.println("After trimToSize()");
System.out.println("Size: " + list.size());
System.out.println("Capacity now: 6 (matches size)");
// Inserting in the middle requires shifting elements
list.add(3, 99);
System.out.println("After inserting 99 at index 3: " + list);
System.out.println("Elements at indexes 3+ were shifted right");
// Removing from the middle also requires shifting elements
list.remove(2);
System.out.println("After removing element at index 2: " + list);
System.out.println("Elements at indexes 3+ were shifted left");
}
}
Output:
Initial capacity: 10 (default)
List after adding 10 elements: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Size: 10
Capacity: 10
List after adding 11th element: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Size: 11
New capacity: 15 (1.5x growth)
List after removing 5 elements: [0, 1, 2, 3, 4, 5]
Size: 6
Capacity still: 15 (doesn't shrink automatically)
After trimToSize()
Size: 6
Capacity now: 6 (matches size)
After inserting 99 at index 3: [0, 1, 2, 99, 3, 4, 5]
Elements at indexes 3+ were shifted right
After removing element at index 2: [0, 1, 99, 3, 4, 5]
Elements at indexes 3+ were shifted left
LinkedList in Java
LinkedList is implemented as a doubly-linked list, providing fast insertions and deletions but slower random access. It also implements the Deque interface, allowing it to be used as a queue or stack.
import java.util.*;
public class LinkedListDemo {
public static void main(String[] args) {
// Creating a LinkedList
LinkedList<String> fruits = new LinkedList<>();
// Adding elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
System.out.println("LinkedList: " + fruits);
// LinkedList specific methods (from Deque interface)
// Add to the beginning
fruits.addFirst("Apricot");
System.out.println("After addFirst: " + fruits);
// Add to the end
fruits.addLast("Dragonfruit");
System.out.println("After addLast: " + fruits);
// Get first element
String first = fruits.getFirst();
System.out.println("First element: " + first);
// Get last element
String last = fruits.getLast();
System.out.println("Last element: " + last);
// Remove first element
String removedFirst = fruits.removeFirst();
System.out.println("Removed first: " + removedFirst);
System.out.println("After removeFirst: " + fruits);
// Remove last element
String removedLast = fruits.removeLast();
System.out.println("Removed last: " + removedLast);
System.out.println("After removeLast: " + fruits);
// Using as a queue (FIFO)
LinkedList<String> queue = new LinkedList<>();
queue.offer("First"); // Add to the end
queue.offer("Second");
queue.offer("Third");
System.out.println("\nQueue: " + queue);
String polled = queue.poll(); // Remove from the beginning
System.out.println("Polled from queue: " + polled);
System.out.println("Queue after poll: " + queue);
// Using as a stack (LIFO)
LinkedList<String> stack = new LinkedList<>();
stack.push("Bottom"); // Add to the beginning
stack.push("Middle");
stack.push("Top");
System.out.println("\nStack: " + stack);
String popped = stack.pop(); // Remove from the beginning
System.out.println("Popped from stack: " + popped);
System.out.println("Stack after pop: " + stack);
// Performance characteristics
LinkedList<Integer> numbers = new LinkedList<>();
for (int i = 0; i < 10; i++) {
numbers.add(i);
}
long startTime = System.nanoTime();
// Slow random access (has to traverse the list)
for (int i = 0; i < 1000; i++) {
if (i < numbers.size()) {
numbers.get(i % numbers.size());
}
}
long accessTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
// Fast insertions at beginning
for (int i = 0; i < 1000; i++) {
numbers.addFirst(i);
}
long insertTime = System.nanoTime() - startTime;
System.out.println("\nTime for 1000 random accesses: " + accessTime + " ns");
System.out.println("Time for 1000 insertions at beginning: " + insertTime + " ns");
}
}
Output (times will vary):
LinkedList: [Apple, Banana, Cherry]
After addFirst: [Apricot, Apple, Banana, Cherry]
After addLast: [Apricot, Apple, Banana, Cherry, Dragonfruit]
First element: Apricot
Last element: Dragonfruit
Removed first: Apricot
After removeFirst: [Apple, Banana, Cherry, Dragonfruit]
Removed last: Dragonfruit
After removeLast: [Apple, Banana, Cherry]
Queue: [First, Second, Third]
Polled from queue: First
Queue after poll: [Second, Third]
Stack: [Top, Middle, Bottom]
Popped from stack: Top
Stack after pop: [Middle, Bottom]
Time for 1000 random accesses: 2345678 ns
Time for 1000 insertions at beginning: 1234567 ns
LinkedList Internal Working
Understanding how LinkedList works internally:
import java.util.*;
public class LinkedListInternalsDemo {
public static void main(String[] args) {
// LinkedList is implemented as a doubly-linked list
// Each element (node) has references to the previous and next nodes
System.out.println("LinkedList internal structure:");
System.out.println("null <- [Node1] <-> [Node2] <-> [Node3] -> null");
System.out.println(" (head) (tail)");
LinkedList<String> list = new LinkedList<>();
// Adding elements creates new nodes and updates references
list.add("A");
System.out.println("\nAfter adding 'A':");
System.out.println("null <- [A] -> null");
System.out.println(" (head/tail)");
list.add("B");
System.out.println("\nAfter adding 'B':");
System.out.println("null <- [A] <-> [B] -> null");
System.out.println(" (head) (tail)");
list.add("C");
System.out.println("\nAfter adding 'C':");
System.out.println("null <- [A] <-> [B] <-> [C] -> null");
System.out.println(" (head) (tail)");
// Adding at the beginning is fast - just update references
list.addFirst("Z");
System.out.println("\nAfter adding 'Z' at the beginning:");
System.out.println("null <- [Z] <-> [A] <-> [B] <-> [C] -> null");
System.out.println(" (head) (tail)");
// Adding in the middle requires traversing to that position
list.add(2, "X");
System.out.println("\nAfter adding 'X' at index 2:");
System.out.println("null <- [Z] <-> [A] <-> [X] <-> [B] <-> [C] -> null");
System.out.println(" (head) (tail)");
// Removing an element updates the surrounding references
list.remove("X");
System.out.println("\nAfter removing 'X':");
System.out.println("null <- [Z] <-> [A] <-> [B] <-> [C] -> null");
System.out.println(" (head) (tail)");
// Accessing by index requires traversing from head or tail
// (whichever is closer)
System.out.println("\nAccessing element at index 2 ('B'):");
System.out.println("Start at head, traverse: Z -> A -> B (found)");
System.out.println("\nAccessing element at index 3 ('C'):");
System.out.println("Start at tail, traverse: C (found)");
// Memory usage
System.out.println("\nMemory usage comparison:");
System.out.println("ArrayList: One array + small overhead");
System.out.println("LinkedList: Two references per element + overhead");
}
}
Output:
LinkedList internal structure:
null <- [Node1] <-> [Node2] <-> [Node3] -> null
(head) (tail)
After adding 'A':
null <- [A] -> null
(head/tail)
After adding 'B':
null <- [A] <-> [B] -> null
(head) (tail)
After adding 'C':
null <- [A] <-> [B] <-> [C] -> null
(head) (tail)
After adding 'Z' at the beginning:
null <- [Z] <-> [A] <-> [B] <-> [C] -> null
(head) (tail)
After adding 'X' at index 2:
null <- [Z] <-> [A] <-> [X] <-> [B] <-> [C] -> null
(head) (tail)
After removing 'X':
null <- [Z] <-> [A] <-> [B] <-> [C] -> null
(head) (tail)
Accessing element at index 2 ('B'):
Start at head, traverse: Z -> A -> B (found)
Accessing element at index 3 ('C'):
Start at tail, traverse: C (found)
Memory usage comparison:
ArrayList: One array + small overhead
LinkedList: Two references per element + overhead
Vector in Java: Thread-Safe List Implementation
Vector is similar to ArrayList but is synchronized, making it thread-safe. It's considered legacy code, but it's still useful in multi-threaded environments where synchronization is needed.
import java.util.*;
public class VectorDemo {
public static void main(String[] args) {
// Creating a Vector
Vector<String> vector = new Vector<>();
// Adding elements
vector.add("Apple");
vector.add("Banana");
vector.add("Cherry");
System.out.println("Vector: " + vector);
// Vector-specific methods
// Add element at index
vector.add(1, "Blueberry");
System.out.println("After adding at index 1: " + vector);
// Get element at index
String element = vector.elementAt(2);
System.out.println("Element at index 2: " + element);
// Get first element
String firstElement = vector.firstElement();
System.out.println("First element: " + firstElement);
// Get last element
String lastElement = vector.lastElement();
System.out.println("Last element: " + lastElement);
// Remove element at index
vector.removeElementAt(1);
System.out.println("After removing element at index 1: " + vector);
// Set element at index
vector.setElementAt("Cranberry", 1);
System.out.println("After setting element at index 1: " + vector);
// Vector capacity management
Vector<Integer> numbers = new Vector<>(10, 5); // Initial capacity 10, increment 5
System.out.println("Initial capacity: " + numbers.capacity());
for (int i = 0; i < 12; i++) {
numbers.add(i);
}
System.out.println("After adding 12 elements, capacity: " + numbers.capacity());
// Thread safety
System.out.println("\nVector is synchronized (thread-safe)");
// Performance comparison with ArrayList
List<Integer> vectorList = new Vector<>();
List<Integer> arrayList = new ArrayList<>();
long startTime = System.nanoTime();
for (int i = 0; i < 100000; i++) {
vectorList.add(i);
}
long vectorTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
for (int i = 0; i < 100000; i++) {
arrayList.add(i);
}
long arrayListTime = System.nanoTime() - startTime;
System.out.println("Time to add 100,000 elements to Vector: " + vectorTime + " ns");
System.out.println("Time to add 100,000 elements to ArrayList: " + arrayListTime + " ns");
System.out.println("Vector is slower due to synchronization overhead");
}
}
Output (times will vary):
Vector: [Apple, Banana, Cherry]
After adding at index 1: [Apple, Blueberry, Banana, Cherry]
Element at index 2: Banana
First element: Apple
Last element: Cherry
After removing element at index 1: [Apple, Banana, Cherry]
After setting element at index 1: [Apple, Cranberry, Cherry]
Initial capacity: 10
After adding 12 elements, capacity: 15
Vector is synchronized (thread-safe)
Time to add 100,000 elements to Vector: 12345678 ns
Time to add 100,000 elements to ArrayList: 7654321 ns
Vector is slower due to synchronization overhead
Stack in Java: LIFO Data Structure
Stack is a subclass of Vector that implements the Last-In-First-Out (LIFO) stack data structure. It's also synchronized but has been largely replaced by more modern alternatives.
import java.util.*;
public class StackDemo {
public static void main(String[] args) {
// Creating a Stack
Stack<String> stack = new Stack<>();
// Pushing elements onto the stack
stack.push("First");
stack.push("Second");
stack.push("Third");
System.out.println("Stack: " + stack);
// Peek at the top element without removing it
String top = stack.peek();
System.out.println("Top element (peek): " + top);
System.out.println("Stack after peek: " + stack);
// Pop (remove and return) the top element
String popped = stack.pop();
System.out.println("Popped element: " + popped);
System.out.println("Stack after pop: " + stack);
// Check if stack is empty
boolean isEmpty = stack.empty();
System.out.println("Is stack empty? " + isEmpty);
// Search for an element (returns 1-based position from the top)
int position = stack.search("First");
System.out.println("Position of 'First' from top: " + position);
// Note: Stack extends Vector, so all Vector methods are available
stack.add("Fourth"); // Same as push
System.out.println("Stack after adding 'Fourth': " + stack);
// Modern alternatives to Stack
System.out.println("\nModern alternatives to Stack:");
// 1. ArrayDeque as a stack
Deque<String> arrayDequeStack = new ArrayDeque<>();
arrayDequeStack.push("First");
arrayDequeStack.push("Second");
System.out.println("ArrayDeque as stack: " + arrayDequeStack);
System.out.println("Popped from ArrayDeque: " + arrayDequeStack.pop());
// 2. LinkedList as a stack
Deque<String> linkedListStack = new LinkedList<>();
linkedListStack.push("First");
linkedListStack.push("Second");
System.out.println("LinkedList as stack: " + linkedListStack);
System.out.println("Popped from LinkedList: " + linkedListStack.pop());
}
}
Output:
Stack: [First, Second, Third]
Top element (peek): Third
Stack after peek: [First, Second, Third]
Popped element: Third
Stack after pop: [First, Second]
Is stack empty? false
Position of 'First' from top: 2
Stack after adding 'Fourth': [First, Second, Fourth]
Modern alternatives to Stack:
ArrayDeque as stack: [Second, First]
Popped from ArrayDeque: Second
LinkedList as stack: [Second, First]
Popped from LinkedList: Second
🔄 Choosing the Right List Implementation
Each List implementation has its own strengths and weaknesses. Here's a guide to help you choose the right one for your needs:
ArrayList
✅ Use when:
- You need frequent random access to elements by index
- You mostly add/remove elements at the end of the list
- You need to iterate through the list frequently
- Memory efficiency is important
❌ Avoid when:
- You frequently insert or remove elements at the beginning or middle of the list
- You need thread safety (unless you explicitly synchronize it)
LinkedList
✅ Use when:
- You frequently add/remove elements at the beginning or middle of the list
- You need to implement a queue or deque
- You don't need random access by index often
❌ Avoid when:
- You need frequent random access to elements by index
- Memory efficiency is critical (each element has additional overhead for the node references)
Vector
✅ Use when:
- You need thread safety without implementing it yourself
- You're working with legacy code that uses Vector
❌ Avoid when:
- Performance is critical (synchronization adds overhead)
- You don't need thread safety
- You're writing new code (ArrayList with explicit synchronization is usually better)
Stack
✅ Use when:
- You're working with legacy code that uses Stack
- You need a simple LIFO data structure with thread safety
❌ Avoid when:
- You're writing new code (Deque implementations like ArrayDeque are better alternatives)
- Performance is critical
Comparing Java List Implementations: Performance Analysis
Operation | ArrayList | LinkedList | Vector | Stack |
---|---|---|---|---|
get(index) | O(1) | O(n) | O(1) | O(1) |
add(E) (at end) | O(1)* | O(1) | O(1)* | O(1)* |
add(index, E) | O(n) | O(n) | O(n) | O(n) |
remove(index) | O(n) | O(n) | O(n) | O(n) |
Iterator.remove() | O(n) | O(1) | O(n) | O(n) |
ListIterator.add(E) | O(n) | O(1) | O(n) | O(n) |
*Amortized constant time (occasional resizing of the backing array takes O(n) time)
🚫 Common Pitfalls and Gotchas
When working with Lists in Java, be aware of these common issues:
1. ConcurrentModificationException
This exception occurs when you modify a list while iterating through it using an iterator or enhanced for loop:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// WRONG: Will throw ConcurrentModificationException
for (String item : list) {
if (item.equals("B")) {
list.remove(item); // Modifying while iterating!
}
}
// CORRECT: Use Iterator's remove method
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("B")) {
iterator.remove(); // Safe way to remove during iteration
}
}
// CORRECT: Use removeIf (Java 8+)
list.removeIf(item -> item.equals("B"));
// CORRECT: Use a copy of the list
for (String item : new ArrayList<>(list)) {
if (item.equals("B")) {
list.remove(item); // Safe because we're iterating over a copy
}
}
2. IndexOutOfBoundsException
This occurs when you try to access an index that doesn't exist:
List<String> list = new ArrayList<>();
list.add("A");
// WRONG: Will throw IndexOutOfBoundsException
String item = list.get(1); // Index 1 doesn't exist (only index 0 exists)
// CORRECT: Check bounds first
if (1 < list.size()) {
String item = list.get(1);
}
// CORRECT: Use a try-catch block
try {
String item = list.get(1);
} catch (IndexOutOfBoundsException e) {
System.out.println("Index out of bounds");
}
3. Unexpected Behavior with subList()
The subList() method returns a view of the original list, not a copy:
List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = original.subList(1, 3); // Contains "B", "C"
// Changes to the sublist affect the original list
sub.set(0, "X");
System.out.println(original); // Prints [A, X, C, D]
// Changes to the original list can invalidate the sublist
original.add(0, "Z");
try {
System.out.println(sub); // Throws ConcurrentModificationException
} catch (ConcurrentModificationException e) {
System.out.println("Sublist invalidated");
}
// To avoid this, create a new list from the sublist
List<String> safeSub = new ArrayList<>(original.subList(1, 3));
4. Performance Issues with Wrong List Type
Using the wrong list type for your use case can lead to performance problems:
// WRONG: Using LinkedList for frequent random access
List<Integer> numbers = new LinkedList<>();
for (int i = 0; i < 100000; i++) {
numbers.add(i);
}
// Slow operation (O(n) for LinkedList)
for (int i = 0; i < 1000; i++) {
numbers.get(i * 50);
}
// CORRECT: Use ArrayList for frequent random access
List<Integer> betterNumbers = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
betterNumbers.add(i);
}
// Fast operation (O(1) for ArrayList)
for (int i = 0; i < 1000; i++) {
betterNumbers.get(i * 50);
}
5. Forgetting That Lists Allow Null Elements
Unlike some other collections, Lists in Java allow null elements:
List<String> list = new ArrayList<>();
list.add(null);
list.add("A");
list.add(null);
System.out.println(list); // Prints [null, A, null]
System.out.println(list.indexOf(null)); // Prints 0 (first occurrence)
// Be careful when working with nulls
for (String item : list) {
// WRONG: Will throw NullPointerException
if (item.equals("B")) { // item might be null
// ...
}
// CORRECT: Check for null first
if ("B".equals(item)) { // Safe even if item is null
// ...
}
}
🌟 Java List Interface: Best Practices
Follow these best practices when working with Lists in Java:
1. Program to the Interface, Not the Implementation
// GOOD: Programming to the interface
List<String> list = new ArrayList<>();
// AVOID: Programming to the implementation
ArrayList<String> list = new ArrayList<>();
Programming to the interface makes your code more flexible. You can change the implementation later without modifying the rest of your code.
2. Choose the Right List Implementation for Your Use Case
As discussed in the "Choosing the Right List Implementation" section, select the implementation that best fits your specific needs.
3. Use Generics for Type Safety
// GOOD: Using generics
List<String> strings = new ArrayList<>();
strings.add("Hello");
String s = strings.get(0); // No casting needed
// AVOID: Raw types (pre-Java 5 style)
List rawList = new ArrayList();
rawList.add("Hello");
String s = (String) rawList.get(0); // Casting required, potential ClassCastException
4. Consider Immutable Lists for Thread Safety
// Creating an immutable list (Java 9+)
List<String> immutableList = List.of("A", "B", "C");
// Pre-Java 9
List<String> unmodifiableList = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
// Attempting to modify these lists will throw UnsupportedOperationException
Immutable lists are inherently thread-safe and can be safely shared between threads.
5. Use the Right Method for Removing Elements During Iteration
// GOOD: Using Iterator's remove method
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (shouldRemove(item)) {
iterator.remove();
}
}
// GOOD: Using removeIf (Java 8+)
list.removeIf(item -> shouldRemove(item));
6. Be Careful with List Views
// Create a copy instead of a view when needed
List<String> copy = new ArrayList<>(originalList.subList(1, 3));
7. Consider Memory Usage
// Trim ArrayList to size when you're done adding elements
ArrayList<String> list = new ArrayList<>();
// ... add elements ...
list.trimToSize(); // Reduces memory usage
8. Use Enhanced For Loop or Stream API for Cleaner Code
// GOOD: Enhanced for loop
for (String item : list) {
System.out.println(item);
}
// GOOD: Stream API (Java 8+)
list.forEach(System.out::println);
9. Use Factory Methods for Small Lists (Java 9+)
// Creating small immutable lists
List<String> empty = List.of();
List<String> single = List.of("A");
List<String> multiple = List.of("A", "B", "C");
10. Consider Thread Safety Needs
// For thread safety with ArrayList
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
// When using a synchronized list with an iterator
synchronized (synchronizedList) {
Iterator<String> iterator = synchronizedList.iterator();
while (iterator.hasNext()) {
// ...
}
}
🔍 Why Lists Matter: Use Cases and Applications
Lists are one of the most versatile and commonly used data structures in Java. Here are some typical use cases:
1. Maintaining Ordered Collections
Lists are perfect when you need to maintain elements in a specific order:
- Displaying items in a user interface (e.g., menu items, search results)
- Processing steps in a workflow
- Command history in an application
2. Dynamic Collections
When the size of your collection changes frequently:
- Building a collection of results from a database query
- Managing a list of active users in a system
- Collecting user input over time
3. Random Access to Elements
When you need to access elements by their position:
- Pagination of results (accessing page 3 of search results)
- Accessing specific elements in a sequence
- Implementing algorithms that require indexed access
4. Implementing Other Data Structures
Lists serve as the foundation for implementing other data structures:
- Stacks (using ArrayList or LinkedList)
- Queues (using LinkedList)
- Deques (using LinkedList)
- Graphs (adjacency lists)
5. Real-World Examples
// Example: Managing a to-do list application
public class TodoListManager {
private List<Task> tasks = new ArrayList<>();
public void addTask(Task task) {
tasks.add(task);
}
public void completeTask(int taskId) {
for (Task task : tasks) {
if (task.getId() == taskId) {
task.setCompleted(true);
break;
}
}
}
public List<Task> getIncompleteTasks() {
List<Task> incompleteTasks = new ArrayList<>();
for (Task task : tasks) {
if (!task.isCompleted()) {
incompleteTasks.add(task);
}
}
return incompleteTasks;
}
public void reorderTask(int taskId, int newPosition) {
Task taskToMove = null;
int currentPosition = -1;
// Find the task and its current position
for (int i = 0; i < tasks.size(); i++) {
if (tasks.get(i).getId() == taskId) {
taskToMove = tasks.get(i);
currentPosition = i;
break;
}
}
if (taskToMove != null && currentPosition != -1) {
// Remove from current position
tasks.remove(currentPosition);
// Insert at new position
if (newPosition >= tasks.size()) {
tasks.add(taskToMove);
} else {
tasks.add(newPosition, taskToMove);
}
}
}
}
class Task {
private int id;
private String description;
private boolean completed;
// Constructor, getters, setters...
public int getId() { return id; }
public String getDescription() { return description; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
}
📝 Java List Interface: Key Takeaways
In this comprehensive guide to Java's List interface and implementations, we've covered:
-
The List Interface: An ordered collection that allows duplicates and provides indexed access to elements.
-
Core List Implementations:
- ArrayList: Backed by a resizable array, offering fast random access but slower insertions/deletions.
- LinkedList: Implemented as a doubly-linked list, providing fast insertions/deletions but slower random access.
- Vector: A synchronized version of ArrayList (thread-safe).
- Stack: A LIFO data structure extending Vector.
-
Key Operations:
- Adding, removing, and accessing elements
- Searching for elements
- Iterating through lists
- Working with sublists
- Bulk operations
-
Performance Characteristics:
- Time complexity of common operations for different implementations
- Memory usage considerations
-
Common Pitfalls:
- ConcurrentModificationException
- IndexOutOfBoundsException
- Unexpected behavior with subList()
- Performance issues with the wrong list type
-
Best Practices:
- Programming to the interface
- Choosing the right implementation
- Using generics for type safety
- Proper iteration techniques
- Thread safety considerations
-
Use Cases:
- Maintaining ordered collections
- Dynamic collections
- Random access to elements
- Implementing other data structures
By understanding the strengths and weaknesses of each List implementation, you can make informed decisions about which one to use in your Java applications, leading to more efficient and maintainable code.
🏋️ Exercises and Mini-Projects
Exercise 1: Custom ArrayList Implementation
Let's implement a simplified version of ArrayList to understand how it works internally:
public class SimpleArrayList<E> {
private static final int DEFAULT_CAPACITY = 10;
private Object[] elements;
private int size;
public SimpleArrayList() {
elements = new Object[DEFAULT_CAPACITY];
size = 0;
}
public void add(E element) {
ensureCapacity();
elements[size++] = element;
}
@SuppressWarnings("unchecked")
public E get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
return (E) elements[index];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
// Shift elements to the left
for (int i = index; i < size - 1; i++) {
elements[i] = elements[i + 1];
}
// Clear the last element and decrement size
elements[--size] = null;
}
private void ensureCapacity() {
if (size == elements.length) {
int newCapacity = elements.length * 2;
Object[] newElements = new Object[newCapacity];
System.arraycopy(elements, 0, newElements, 0, size);
elements = newElements;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < size; i++) {
sb.append(elements[i]);
if (i < size - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
SimpleArrayList<String> list = new SimpleArrayList<>();
// Test adding elements
list.add("Apple");
list.add("Banana");
list.add("Cherry");
System.out.println("List after adding 3 elements: " + list);
// Test getting elements
System.out.println("Element at index 1: " + list.get(1));
// Test removing elements
list.remove(1);
System.out.println("List after removing element at index 1: " + list);
// Test size and isEmpty
System.out.println("Size: " + list.size());
System.out.println("Is empty? " + list.isEmpty());
// Test capacity expansion
for (int i = 0; i < 10; i++) {
list.add("Item " + i);
}
System.out.println("List after adding 10 more elements: " + list);
}
}
Practice Exercise: Extend the SimpleArrayList implementation to include:
- An
add(int index, E element)
method to insert at a specific position - A
set(int index, E element)
method to replace an element - An
indexOf(Object o)
method to find the position of an element - A
contains(Object o)
method to check if an element exists
Exercise 2: Word Frequency Counter
Let's create a program that counts the frequency of words in a text file:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
public class WordFrequencyCounter {
public static void main(String[] args) {
// The text file to analyze
String filename = "sample.txt";
// Map to store word frequencies
Map<String, Integer> wordFrequency = new HashMap<>();
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) {
// Split the line into words
String[] words = line.toLowerCase()
.replaceAll("[^a-zA-Z ]", "")
.split("\\s+");
// Count each word
for (String word : words) {
if (!word.isEmpty()) {
wordFrequency.put(word, wordFrequency.getOrDefault(word, 0) + 1);
}
}
}
// Convert to list for sorting
List<Map.Entry<String, Integer>> entries = new ArrayList<>(wordFrequency.entrySet());
// Sort by frequency (descending)
entries.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue()));
// Display the top 10 most frequent words
System.out.println("Top 10 most frequent words:");
for (int i = 0; i < Math.min(10, entries.size()); i++) {
Map.Entry<String, Integer> entry = entries.get(i);
System.out.printf("%-15s %d%n", entry.getKey(), entry.getValue());
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}
}
Sample output (will vary based on the input file):
Top 10 most frequent words:
the 154
and 93
to 81
of 78
a 77
in 54
that 41
is 40
it 39
for 36
Practice Exercise: Modify the program to:
- Read from an actual text file
- Ignore common words (like "the", "a", "an")
- Display the results as a simple bar chart
Mini-Project: Contact Management System
Let's create a simple contact management system that demonstrates the use of various List implementations:
import java.util.*;
import java.util.function.Predicate;
class Contact {
private String name;
private String email;
private String phone;
private List<String> tags;
public Contact(String name, String email, String phone) {
this.name = name;
this.email = email;
this.phone = phone;
this.tags = new ArrayList<>();
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public List<String> getTags() { return new ArrayList<>(tags); }
public void addTag(String tag) {
if (!tags.contains(tag)) {
tags.add(tag);
}
}
public void removeTag(String tag) {
tags.remove(tag);
}
public boolean hasTag(String tag) {
return tags.contains(tag);
}
@Override
public String toString() {
return String.format("Name: %-20s Email: %-25s Phone: %-15s Tags: %s",
name, email, phone, String.join(", ", tags));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Contact contact = (Contact) o;
return email.equals(contact.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
}
class ContactManager {
private List<Contact> contacts;
public ContactManager() {
// Using ArrayList for efficient random access and iteration
contacts = new ArrayList<>();
}
public void addContact(Contact contact) {
// Check if contact with same email already exists
for (int i = 0; i < contacts.size(); i++) {
if (contacts.get(i).getEmail().equals(contact.getEmail())) {
contacts.set(i, contact); // Replace existing contact
return;
}
}
contacts.add(contact); // Add new contact
}
public void removeContact(String email) {
contacts.removeIf(contact -> contact.getEmail().equals(email));
}
public Contact findByEmail(String email) {
for (Contact contact : contacts) {
if (contact.getEmail().equals(email)) {
return contact;
}
}
return null;
}
public List<Contact> findByName(String namePart) {
List<Contact> result = new ArrayList<>();
String lowerNamePart = namePart.toLowerCase();
for (Contact contact : contacts) {
if (contact.getName().toLowerCase().contains(lowerNamePart)) {
result.add(contact);
}
}
return result;
}
public List<Contact> findByTag(String tag) {
List<Contact> result = new ArrayList<>();
for (Contact contact : contacts) {
if (contact.hasTag(tag)) {
result.add(contact);
}
}
return result;
}
public List<Contact> findByCustomCriteria(Predicate<Contact> criteria) {
List<Contact> result = new ArrayList<>();
for (Contact contact : contacts) {
if (criteria.test(contact)) {
result.add(contact);
}
}
return result;
}
public List<Contact> getAllContacts() {
return new ArrayList<>(contacts);
}
public void sortContacts(Comparator<Contact> comparator) {
Collections.sort(contacts, comparator);
}
}
public class ContactManagementSystem {
public static void main(String[] args) {
ContactManager manager = new ContactManager();
// Add some contacts
Contact john = new Contact("John Smith", "john@example.com", "555-1234");
john.addTag("friend");
john.addTag("work");
Contact alice = new Contact("Alice Johnson", "alice@example.com", "555-5678");
alice.addTag("family");
Contact bob = new Contact("Bob Williams", "bob@example.com", "555-9012");
bob.addTag("work");
manager.addContact(john);
manager.addContact(alice);
manager.addContact(bob);
// Display all contacts
System.out.println("All contacts:");
for (Contact contact : manager.getAllContacts()) {
System.out.println(contact);
}
// Sort contacts by name
System.out.println("\nContacts sorted by name:");
manager.sortContacts(Comparator.comparing(Contact::getName));
for (Contact contact : manager.getAllContacts()) {
System.out.println(contact);
}
// Find contacts by tag
System.out.println("\nWork contacts:");
List<Contact> workContacts = manager.findByTag("work");
for (Contact contact : workContacts) {
System.out.println(contact);
}
// Find contact by email
System.out.println("\nLooking up by email:");
Contact foundContact = manager.findByEmail("alice@example.com");
if (foundContact != null) {
System.out.println(foundContact);
}
// Find by custom criteria (contacts with gmail addresses)
System.out.println("\nContacts with gmail addresses:");
List<Contact> gmailContacts = manager.findByCustomCriteria(
contact -> contact.getEmail().endsWith("@gmail.com")
);
if (gmailContacts.isEmpty()) {
System.out.println("No contacts with gmail addresses found");
} else {
for (Contact contact : gmailContacts) {
System.out.println(contact);
}
}
// Update a contact
System.out.println("\nUpdating John's phone number:");
Contact johnUpdated = manager.findByEmail("john@example.com");
if (johnUpdated != null) {
johnUpdated.setPhone("555-4321");
manager.addContact(johnUpdated); // This will replace the existing contact
System.out.println("After update:");
Contact johnAfterUpdate = manager.findByEmail("john@example.com");
System.out.println(johnAfterUpdate);
}
// Remove a contact
System.out.println("\nRemoving Bob:");
manager.removeContact("bob@example.com");
System.out.println("\nAll contacts after removal:");
for (Contact contact : manager.getAllContacts()) {
System.out.println(contact);
}
}
}
Output:
All contacts:
Name: John Smith Email: john@example.com Phone: 555-1234 Tags: friend, work
Name: Alice Johnson Email: alice@example.com Phone: 555-5678 Tags: family
Name: Bob Williams Email: bob@example.com Phone: 555-9012 Tags: work
Contacts sorted by name:
Name: Alice Johnson Email: alice@example.com Phone: 555-5678 Tags: family
Name: Bob Williams Email: bob@example.com Phone: 555-9012 Tags: work
Name: John Smith Email: john@example.com Phone: 555-1234 Tags: friend, work
Work contacts:
Name: John Smith Email: john@example.com Phone: 555-1234 Tags: friend, work
Name: Bob Williams Email: bob@example.com Phone: 555-9012 Tags: work
Looking up by email:
Name: Alice Johnson Email: alice@example.com Phone: 555-5678 Tags: family
Contacts with gmail addresses:
No contacts with gmail addresses found
Updating John's phone number:
After update:
Name: John Smith Email: john@example.com Phone: 555-4321 Tags: friend, work
Removing Bob:
All contacts after removal:
Name: Alice Johnson Email: alice@example.com Phone: 555-5678 Tags: family
Name: John Smith Email: john@example.com Phone: 555-4321 Tags: friend, work
Practice Exercise: Extend the Contact Management System to:
- Add persistence (save/load contacts to/from a file)
- Create a simple command-line interface for user interaction
- Add more search capabilities (e.g., by phone number, partial matches)
- Implement contact groups (a contact can belong to multiple groups)
🔄 Related Topics
- Java Collections Framework: The broader framework that includes Lists, Sets, Maps, and more
- Java Generics: For type-safe collections
- Java Streams API: For functional-style operations on collections
- Data Structures: Understanding the underlying implementations of Lists
- Algorithms: Sorting, searching, and other operations on Lists
- Design Patterns: Many patterns use Lists (Iterator, Composite, etc.)
By mastering Java Lists, you'll have a powerful tool in your programming toolkit for solving a wide range of problems efficiently and elegantly.