🗃️ Collections Framework · Intermediate

Legacy collections in Java

Vector, Stack, Hashtable — why ArrayDeque and HashMap replaced them.

🧩 The mysteryEvery method of Vector is synchronized, so it's thread-safe... right? Not quite. And Stack lets you peek at the bottom plate. Let's dig up some fossils.

Fossils from Java 1.0

**Vector, Stack, Hashtable and Enumeration** predate the Collections Framework (Java 1.2). They still work, but each has a modern replacement.

// Vector      → ArrayList
// Stack       → ArrayDeque
// Hashtable   → HashMap / ConcurrentHashMap
// Enumeration → Iterator

Synchronized ≠ thread-safe

Every method is synchronized: each call takes a lock. That's slower in single-threaded code, and it still doesn't make sequences of calls atomic. Between containsKey and put, another thread can sneak in.

// two calls, two separate locks:
if (!table.containsKey(k)) {
    table.put(k, v); // race!
}
🔮 Predict it

Your turn

What does this print?

Stack<String> s = new Stack<>();
s.push("x");
s.push("y");
s.push("z");
System.out.println(s + " " + s.get(0));
  1. [z, y, x] z
  2. [x, y, z] x
  3. [x, y, z] z
Show the answer

Stack extends Vector, so it stores elements bottom first: toString prints [x, y, z]. And get(0) returns the bottom element, an index operation a real stack shouldn't even allow.

Stack is a Vector in disguise

Because **Stack extends Vector**, it inherits every List method: get(i), add(i, x), remove(i). Anyone can reach into the middle of your "stack". A Deque only exposes the ends.

Stack<Integer> old = new Stack<>();
old.add(0, 99);  // sneaks in at the bottom!
Deque<Integer> s = new ArrayDeque<>();
s.push(1);       // only the ends
🔮 Predict it

Null in a Hashtable

What happens?

Map<String, String> t = new Hashtable<>();
t.put("k", null);
System.out.println(t);
  1. Prints {k=null}
  2. Prints {}
  3. Throws NullPointerException
Show the answer

Hashtable rejects null keys AND null values with NullPointerException. HashMap allows one null key and any number of null values.

Shared counters across threads

✗ Legacy
Hashtable<String, Integer> hits =
    new Hashtable<>();
Integer n = hits.get(page);
hits.put(page, n == null ? 1 : n + 1);

Two separate locked calls: two threads can read the same n and lose an update.

✓ Modern
var hits =
    new ConcurrentHashMap<String, Integer>();
hits.merge(page, 1, Integer::sum);

merge does the read-modify-write atomically per key, without one big lock.

💼 In the real world

Legacy in the wild

You'll meet these classes in older enterprise code and some old APIs (Swing still uses Vector). When modernising, swap Vector → ArrayList, Stack → ArrayDeque, Hashtable → HashMap, or ConcurrentHashMap when threads share it, and run the tests: watch for code that relied on nulls being rejected.

Key takeaways

  1. Vector → ArrayList
  2. Stack → ArrayDeque (push/pop/peek)
  3. Hashtable → HashMap, or ConcurrentHashMap for threads
  4. Hashtable rejects null keys and values
🤯 Did you know?

Stack's own Javadoc recommends against it: it says the Deque interface provides a more complete and consistent set of LIFO stack operations, and suggests ArrayDeque.

Practice questions

What does this print?

Stack<String> s = new Stack<>();
s.push("a");
s.push("b");
s.push("c");
System.out.println(s + " " + s.get(0));
  1. [c, b, a] c
  2. [a, b, c] a
  3. [a, b, c] c
  4. [c, b, a] a
Check your answer

[a, b, c] a. Stack extends Vector, so it stores elements bottom-first and toString prints [a, b, c]. get(0) returns the bottom element — an index operation a real stack shouldn't even allow.

Why are Vector and Hashtable discouraged in new code?

  1. They were removed in Java 17
  2. Every method is synchronized — slower in single-threaded code, yet compound actions like check-then-put still aren't atomic
  3. They can only store Strings
  4. They don't implement any collection interfaces
Check your answer

Every method is synchronized — slower in single-threaded code, yet compound actions like check-then-put still aren't atomic. Locking each individual call adds overhead but doesn't protect sequences of calls. Use unsynchronized collections, or purpose-built concurrent ones like ConcurrentHashMap.

Next: remove an item inside a for-each loop and Java throws an exception... except when it doesn't. Get ready for one of Java's sneakiest edge cases.