Skip to content
TobussSystems in Practice

Java Collections Cheat Sheet

4 min read

A practical reference covering how Java’s core collections work under the hood — HashMap bucket internals, ConcurrentHashMap’s lock-free design, ArrayList vs LinkedList performance, sorted/ordered maps, and choosing the right queue.


HashMap Internals

Array of buckets where each bucket is a linked list (Java 7) or a red-black tree when the bucket size exceeds 8 (Java 8+). Load factor 0.75 means resize at 75% capacity — doubles the array and rehashes all entries. hashCode() + equals() contract is critical: equal objects must have equal hash codes.

buckets array
┌─────┐
│  0  │──▶ null
├─────┤
│  1  │──▶ ["alice", 25] ──▶ ["carol", 31]  (collision → linked list)
├─────┤
│  2  │──▶ null
├─────┤
│  3  │──▶ ["bob", 30]
└─────┘
 ...
When bucket list length > 8 → converts to red-black tree → O(log n) lookup
// hashCode + equals contract
class Money {
    final long amount;
    final String currency;

    @Override
    public int hashCode() {
        return Objects.hash(amount, currency); // both fields
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Money m)) return false;
        return amount == m.amount && currency.equals(m.currency);
    }
}

// If you override equals() without hashCode():
Map<Money, String> map = new HashMap<>();
map.put(new Money(100, "EUR"), "hundred");
map.get(new Money(100, "EUR")); // returns null! — different hash buckets

// Initial capacity to avoid rehashing
Map<String, Integer> map = new HashMap<>(expectedSize / 0.75 + 1);

ConcurrentHashMap

Java 8 replaced segment-based locking with CAS + per-bucket synchronization. Reads are completely lock-free. Writes synchronize only on the specific bucket being modified — far less contention than a single lock. size() is approximate (uses LongAdder internally).

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// Atomic compound operations
map.putIfAbsent("key", 1);
map.computeIfAbsent("key", k -> expensiveLoad(k));  // atomic
map.merge("key", 1, Integer::sum);  // atomic read-modify-write

// Common mistake: non-atomic check-then-act
if (!map.containsKey("key")) {      // check
    map.put("key", compute("key")); // act — NOT atomic! race condition
}
// Fix: use computeIfAbsent

// size() is approximate — avoid in logic
int approxSize = map.size(); // may not reflect concurrent inserts

// forEach is weakly consistent — sees snapshot, won't throw ConcurrentModificationException
map.forEach((k, v) -> process(k, v));

ArrayList vs LinkedList

ArrayList wins in almost all real-world cases due to CPU cache locality. Contiguous memory means the prefetcher works. LinkedList nodes are scattered in the heap — every .get(i) is a cache miss.

// ArrayList — O(1) random access, O(n) insert at middle
List<String> list = new ArrayList<>();
list.get(500);           // O(1) — direct index into array
list.add(0, "first");    // O(n) — shifts all elements right

// LinkedList — O(1) insert at iterator position, O(n) get(i)
LinkedList<String> ll = new LinkedList<>();
ll.addFirst("head");     // O(1)
ll.get(500);             // O(n) — traverses 500 nodes

// When LinkedList wins: frequent insert/delete via iterator
Iterator<String> it = ll.iterator();
while (it.hasNext()) {
    if (shouldRemove(it.next())) it.remove(); // O(1) at current position
}
// Same with ArrayList is O(n) per remove due to shifting

// In practice: ArrayList + removeIf() is often still faster
list.removeIf(s -> shouldRemove(s)); // bulk shift once

TreeMap vs LinkedHashMap

TreeMap: sorted by natural order or Comparator, O(log n) ops, backed by a red-black tree. LinkedHashMap: maintains insertion or access order, O(1) ops — the classic LRU cache base.

// TreeMap — sorted keys
TreeMap<String, Integer> tree = new TreeMap<>();
tree.put("banana", 2);
tree.put("apple", 1);
tree.put("cherry", 3);
tree.firstKey();                    // "apple"
tree.subMap("apple", "cherry");     // range query
tree.floorKey("blueberry");         // "banana" — largest key <= query

// LinkedHashMap — LRU cache
int MAX = 100;
Map<String, Data> lruCache = new LinkedHashMap<>(MAX, 0.75f, true) {
    // accessOrder=true: get() moves entry to end
    protected boolean removeEldestEntry(Map.Entry e) {
        return size() > MAX; // evict oldest on overflow
    }
};
lruCache.put("key", data);
lruCache.get("key"); // moves "key" to most-recently-used end

Queue Family

Choose the right queue for the job:

Queue type              Bounded?  Blocking?   Use case
────────────────────────────────────────────────────────
ArrayDeque              No        No          General stack/queue, faster than LinkedList
PriorityQueue           No        No          Min-heap, task scheduling
ArrayBlockingQueue      YES       Yes         Producer-consumer with back-pressure
LinkedBlockingQueue     Optional  Yes         Producer-consumer (default unbounded!)
SynchronousQueue        0-size    Yes         Direct handoff, no buffering
DelayQueue              No        Yes         Scheduled tasks (expires after delay)
// ArrayDeque — general purpose, no null allowed, faster than LinkedList
Deque<String> deque = new ArrayDeque<>();
deque.push("a");    // stack: push to front
deque.pop();        // stack: pop from front
deque.offer("b");   // queue: add to back
deque.poll();       // queue: remove from front

// PriorityQueue — min-heap
PriorityQueue<Task> pq = new PriorityQueue<>(
    Comparator.comparingInt(t -> t.priority)
);
pq.offer(new Task(3, "low"));
pq.offer(new Task(1, "high"));
pq.poll(); // returns Task(1, "high") — lowest priority value first

// BlockingQueue — producer-consumer
BlockingQueue<Order> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put(order);      // blocks if full
queue.offer(order, 1, SECONDS); // times out if full

// Consumer
Order o = queue.take();          // blocks if empty
Order o = queue.poll(1, SECONDS);// times out if empty

// SynchronousQueue — zero buffer, direct handoff
BlockingQueue<Work> handoff = new SynchronousQueue<>();
// put() blocks until a consumer calls take() — and vice versa