Choosing ArrayList when you need a LinkedList (or vice versa) causes O(n²) performance. Learn the time complexity of Java collections operations and how to pick the right one every time.
JOptimize Team
Using the wrong Java collection doesn't fail fast — it degrades gradually. LinkedList used as a stack is fine for 100 elements and catastrophic for 100,000. ArrayList with remove(0) in a loop is O(n²). HashMap with a poorly implemented hashCode() degrades to O(n) per lookup. The fix in each case is one line — but you have to know which line.
| Operation | ArrayList | LinkedList | HashMap | TreeMap | HashSet |
|---|---|---|---|---|---|
| get(index) | O(1) | O(n) | — | — | — |
| add(end) | O(1) amort. | O(1) | O(1) amort. | O(log n) | O(1) |
| add(front) | O(n) | O(1) | — | — | — |
| remove(index) | O(n) | O(n) | O(1) | O(log n) | O(1) |
| contains | O(n) | O(n) | O(1) | O(log n) | O(1) |
| Ordered? | Index | Insertion | No | Sorted key | No |
| Null keys | — | — | 1 null key | No | 1 null |
// SLOW — remove(0) shifts all elements: O(n) per call → O(n²) total LinkedList<Task> queue = new LinkedList<>(); while (!queue.isEmpty()) { process(queue.remove(0)); } // FAST for queue operations — use ArrayDeque ArrayDeque<Task> queue = new ArrayDeque<>(); queue.offer(task); // Enqueue task = queue.poll(); // Dequeue — O(1), no shifting
Rule: for FIFO queues, use ArrayDeque, not LinkedList or ArrayList. ArrayDeque is faster than LinkedList (better cache locality) and faster than ArrayList.remove(0) (no shifting).
// For stack (LIFO), also use ArrayDeque ArrayDeque<String> stack = new ArrayDeque<>(); stack.push("a"); // O(1) stack.pop(); // O(1) // Do NOT use Stack<T> — it's synchronized (legacy) and slow
// BAD hashCode — all objects hash to same bucket → O(n) lookups public class BadKey { private final String value; @Override public int hashCode() { return 1; } // Everything in one bucket! @Override public boolean equals(Object o) { ... } } Map<BadKey, String> map = new HashMap<>(); // With 10,000 entries: every get() scans all 10,000 — O(n) not O(1)
// GOOD — use records or proper hashCode public record OrderKey(Long orderId, String region) {} // Records auto-generate correct hashCode and equals // Or use Objects.hash() @Override public int hashCode() { return Objects.hash(orderId, region, customerId); }
For critical hot paths, you can verify distribution: map.entrySet().stream().collect(Collectors.groupingBy(e -> System.identityHashCode(e.getKey()) % 16, Collectors.counting())) — buckets should be roughly equal.
// HashMap — O(1) operations, unordered Map<String, Product> productById = new HashMap<>(); productById.get("prod-123"); // Fast lookup // TreeMap — O(log n), sorted by key TreeMap<LocalDate, SalesReport> reportsByDate = new TreeMap<>(); reportsByDate.get(date); // Slower lookup reportsByDate.subMap( // ONLY possible with TreeMap LocalDate.of(2026, 1, 1), LocalDate.of(2026, 3, 31) ).values(); // Reports for Q1
Use TreeMap when: you need range queries, floor/ceiling operations, or sorted iteration. For everything else, HashMap is faster.
// BAD — synchronized on every read AND write Map<String, CacheEntry> cache = Collections.synchronizedMap(new HashMap<>()); // synchronized(map) wraps every get/put — contention under load // GOOD — ConcurrentHashMap: reads don't lock, writes lock segment only Map<String, CacheEntry> cache = new ConcurrentHashMap<>(); cache.computeIfAbsent(key, k -> loadFromDb(k)); // Atomic, efficient // For read-heavy, rarely-modified lists List<Listener> listeners = new CopyOnWriteArrayList<>(); listeners.add(listener); // Creates new array copy — slow write listeners.forEach(l -> l.notify(event)); // No lock needed — reads are snapshot
Use CopyOnWriteArrayList for:
ConcurrentModificationException// New in Java 21 — consistent API for collections with defined order SequencedCollection<String> list = new ArrayList<>(List.of("a", "b", "c")); list.getFirst(); // "a" — previously: list.get(0) list.getLast(); // "c" — previously: list.get(list.size()-1) list.addFirst("z"); // Add to front — previously: list.add(0, "z") list.reversed(); // Reversed view — previously: Collections.reverse(copy) // Works on List, Deque, LinkedHashSet, TreeSet SequencedSet<String> set = new LinkedHashSet<>(Set.of("a", "b", "c")); set.getFirst(); // "a" — first element by insertion order
// Pre-Java 9 — verbose and still mutable if cast List<String> list = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("a", "b"))); // Java 9+ — truly immutable, no copy needed, memory-efficient List<String> immutable = List.of("a", "b", "c"); // Compact, immutable Map<String, Integer> map = Map.of("a", 1, "b", 2); Set<String> set = Set.of("x", "y", "z"); // For large collections List<String> copy = List.copyOf(mutableList); // Defensive copy, immutable
Immutable collections from List.of() have a smaller memory footprint than ArrayList and are inherently thread-safe — no synchronization needed.
LinkedList as a general-purpose list — ArrayList beats LinkedList for almost everything due to better cache locality; only use LinkedList when you need O(1) head/tail additions with Deque interfaceVector or Hashtable — legacy, fully synchronized on every operation; use ArrayList + ConcurrentHashMap insteadnew ArrayList<>(collection) when you only need read access — use List.copyOf(collection) for an immutable snapshot, or pass the collection directly if mutation isn't neededHashMap capacity — new HashMap<>(expectedSize * 4 / 3 + 1) avoids rehashing for large maps; the default initial capacity of 16 with load factor 0.75 causes rehashing at 12 entriesChoose Java collections based on access patterns: ArrayList for indexed access, ArrayDeque for queues and stacks, HashMap for key lookup, TreeMap for sorted ranges, ConcurrentHashMap for concurrent maps, CopyOnWriteArrayList for read-heavy concurrent lists, and List.of() / Map.of() for immutable data. The wrong choice rarely breaks anything immediately — but it makes your application slower in proportion to its success.
JOptimize detects LinkedList used as a general list, Vector/Hashtable usage, missing initial capacity on large HashMaps, and synchronization bottlenecks in concurrent code.
Pick the right collection once, save performance tuning forever.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.