You open a checkout helper and the bag of orders is a Vector. The SKU table is a Hashtable. Someone left a comment: “thread-safe.” The code compiles on Java 21. That is not a recommendation.
Legacy collections are pre-framework types that lock the whole structure on every call. They still exist because Java does not delete the 1990s from the JDK, and because APIs still return them. This post is how to recognize them, what they actually guarantee, and what to type instead. The Collections hub already said Vector is not the thread-safe ArrayList you want. Here is why that sentence is true.
Same checkout records as the rest of the series:
public record LineItem(String sku, int quantity, BigDecimal unitPrice) {}
public record Order(
String id,
String customerEmail,
List<LineItem> items,
BigDecimal total,
boolean active) {}
Why they still appear
Java 1.0 shipped Vector, Stack, Hashtable, Dictionary, Enumeration, and Properties before there was a Collections Framework. Java 1.2 added List, Map, ArrayList, and HashMap, and retrofitted the old classes onto the new interfaces so existing binaries kept running.
You still meet them because:
- A library or JDK API returns
Properties,Vector, orEnumerationand you have to consume it. - A textbook or an old snippet used
Stack.pushas the word for LIFO. - A review once said “use the synchronized one” and
Vectorwas the name that came to mind.
They compile. They are maintained. They are not what you new in application code in 2026.
Vector: a synchronized ArrayList ancestor
Vector is a growable array that implements List. Every public method is synchronized. That is a whole-list lock, the same cost model as Collections.synchronizedList(new ArrayList<>()) — except ArrayList plus an explicit wrapper makes the locking visible, and Vector hides it in the type you thought was “just a list.”
Vector<Order> vec = new Vector<>();
vec.add(new Order("o-100", "ada@example.com", List.of(), new BigDecimal("12.00"), true));
vec.add(new Order("o-101", "linus@example.com", List.of(), new BigDecimal("4.50"), true));
Order first = vec.get(0); // synchronized get
vec.add(1, first); // synchronized insert
ArrayList does the same job without the lock:
List<Order> pack = new ArrayList<>();
pack.add(new Order("o-100", "ada@example.com", List.of(), new BigDecimal("12.00"), true));
If one thread owns the list, the synchronized is wasted contention you never needed. If several threads share the list, locking every get and add still does not make a compound action safe:
if (!vec.contains(order)) { // lock, unlock
vec.add(order); // lock, unlock — another thread can sneak in between
}
Two synchronized calls are not one atomic decision. Iteration still needs care. Vector.iterator() is fail-fast. Vector.elements() returns an Enumeration that is not fail-fast — a mix that only exists because the class is that old.
Vector | ArrayList | |
|---|---|---|
| Lock | each method synchronized | none |
| Growth | doubles by default (or capacityIncrement) | ~1.5× |
| Nulls | allowed | allowed |
List | yes (since 1.2) | yes |
| New code | no | yes — the default list |
Need a shared list with snapshot iteration? CopyOnWriteArrayList. Need a shared structure at all? Usually you wanted a queue or a concurrent map, not a locked array. Need a list in one thread? ArrayList.
Stack extends Vector
LIFO should be a deque. In the JDK it is historically a Vector with five extra methods:
Stack<Order> stack = new Stack<>();
stack.push(order);
Order top = stack.peek();
Order popped = stack.pop();
int oneBasedFromTop = stack.search(order); // 1 = top, not an index
Because Stack extends Vector, this also compiles:
stack.insertElementAt(order, 0); // not a stack operation
stack.get(1); // indexing through the Vector
The type does not protect the LIFO contract. It inherits a synchronized random-access list and adds push / pop. search is 1-based from the top, which is its own interview trap.
Use ArrayDeque for a stack (and for a queue) in new code:
Deque<Order> stack = new ArrayDeque<>();
stack.push(order);
Order top = stack.peek();
Order popped = stack.pop();
ArrayDeque is not synchronized, forbids nulls, and does not offer insertElementAt. That is the point. The hub default deque is this type. LinkedList as a stack is the other leftover; skip it unless you already have a reason to hold nodes.
Hashtable vs HashMap vs ConcurrentHashMap
Hashtable is a synchronized hash table. It implements Map. It extends Dictionary. It allows no null key and no null value — put(null, order) throws NullPointerException. Every method locks the whole table.
Hashtable<String, Order> table = new Hashtable<>();
table.put("o-100", order);
Order found = table.get("o-100");
Enumeration<Order> values = table.elements();
Enumeration<String> keys = table.keys();
HashMap is the single-thread default: one null key, null values, no lock.
Map<String, Order> byId = new HashMap<>();
byId.put("o-100", order);
ConcurrentHashMap is the shared-map default: no nulls (same as Hashtable on that point), but it does not lock the entire table on every call. Compound in-place updates go through compute / merge / putIfAbsent, not through a synchronized get plus a synchronized put.
Map<String, Integer> qty = new ConcurrentHashMap<>();
qty.put("SKU-MUG", 2);
qty.merge("SKU-MUG", 1, Integer::sum); // 3 — one atomic remap
Hashtable | HashMap | ConcurrentHashMap | |
|---|---|---|---|
| Lock | whole table, every method | none | per-bin / concurrent ops — not the whole table |
| Null key / value | no | one null key, null values | no |
| Iterators | entrySet iterator fail-fast; keys() / elements() are Enumeration | fail-fast | weakly consistent |
| New code | no | default map | default shared map |
A Hashtable is not “HashMap but safe.” It is the 1990s lock. Replacing it with Collections.synchronizedMap(new HashMap<>()) keeps the same whole-map lock. Replacing it with ConcurrentHashMap is the actual upgrade when another thread looks.
Dictionary is the abstract ancestor
Hashtable extends Dictionary<K,V>, an abstract class with get, put, remove, keys, elements, size, isEmpty. It is not a Map. It is not an interface you should declare on a field.
Dictionary<String, Order> dict = new Hashtable<>();
dict.put("o-100", order);
Enumeration<Order> e = dict.elements();
If you see Dictionary in a signature, the honest adapter is to copy into a Map at the boundary and program to Map everywhere else. Do not add a second Dictionary implementation.
Enumeration vs Iterator
Classic traversal before Iterator:
Hashtable<String, Order> table = new Hashtable<>();
table.put("o-100", order);
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
String id = keys.nextElement();
Order o = table.get(id);
}
Enumeration | Iterator | |
|---|---|---|
| Advance | hasMoreElements / nextElement | hasNext / next |
| Remove | no (classic) | remove (optional) |
| Where | Vector.elements(), Hashtable.keys() / elements(), Properties.propertyNames() | every modern collection |
| Fail-fast | usually not on the legacy enumerations | fail-fast on java.util collections |
You will still receive an Enumeration from old APIs. Consume it, or wrap with Collections.list(enumeration) to get an ArrayList, and then iterate normally. Do not return a new Enumeration from your own code. Iterator (and for-each, which uses it) is the contract the rest of this series uses.
Enumeration<Order> e = vec.elements();
List<Order> copy = Collections.list(e);
for (Order o : copy) {
System.out.println(o.id());
}
Properties is a Hashtable
Properties extends Hashtable<Object,Object>. As a type, it is a string-to-string table with a parent defaults table — not a general-purpose map, and not a tutorial on loading .properties files (that I/O lives elsewhere).
Properties defaults = new Properties();
defaults.setProperty("region", "us-east");
defaults.setProperty("currency", "USD");
Properties overrides = new Properties(defaults);
overrides.setProperty("region", "eu-west");
String region = overrides.getProperty("region"); // eu-west
String currency = overrides.getProperty("currency"); // USD — from defaults
String missing = overrides.getProperty("timeout", "30"); // "30" — method default, not the parent table
setProperty / getProperty are the string API. Because the class extends Hashtable, this also compiles:
overrides.put(42, order); // Object key, Object value — Hashtable heritage
That entry will not round-trip through the string methods cleanly. Do not use Properties as your Map<String, Order>. Use HashMap or ConcurrentHashMap. Keep Properties for the string-defaults job — JVM / process settings that already speak that type — and even then prefer getProperty over get so you stay on the string contract.
The parent Properties in the constructor is a fallback chain for missing keys, distinct from getProperty(key, defaultValue) which is a one-shot fallback string.
What to reach for instead
| You see | You want | Why |
|---|---|---|
new Vector<>() | new ArrayList<>() | default list, no whole-list lock |
shared Vector | confine, or CopyOnWriteArrayList | snapshot iterators when reads dominate |
new Stack<>() | new ArrayDeque<>() as Deque | LIFO without inheriting Vector |
new Hashtable<>() | new HashMap<>() | default map, one thread |
shared Hashtable | new ConcurrentHashMap<>() | shared map without synchronizing the whole table |
Dictionary | Map | the framework type |
Enumeration | Iterator / for-each | remove, fail-fast contract, every modern collection |
Properties as a domain map | HashMap / ConcurrentHashMap | Properties is string defaults, and it is still a Hashtable |
Properties for string defaults | Properties (or a purpose-built config type) | that is the remaining honest job |
The hub catalog lists the implementations. This table is only the legacy off-ramps.
Interview lens
Interviewers use these names to see whether you know history or whether you would type it on Monday.
| Question | Honest answer |
|---|---|
Vector vs ArrayList? | Same growable-array job. Vector synchronizes every method. ArrayList is the default. Whole-list lock is not how you share a list now. |
Why is Stack a Vector? | 1.0 history. It extends Vector, so you can index and insert in the middle. Use ArrayDeque. |
Hashtable vs ConcurrentHashMap? | Both reject nulls. Hashtable locks the whole table. ConcurrentHashMap is the shared map; see the next post. |
Enumeration vs Iterator? | Enumeration is the old cursor (hasMoreElements). No remove on the classic form. Iterator is the framework cursor. |
Is Properties a Map of strings? | It is a Hashtable<Object,Object> with a string API and a defaults parent. Not your domain map. |
Wrong answer: “Vector is the thread-safe ArrayList you want.” It is a synchronized list from before the framework. Compound actions are still races. Iteration is not “handled.” Name confinement, CopyOnWriteArrayList, or a concurrent queue — not Vector.
What to say if they ask why these classes were not removed: binary compatibility and APIs that still return them. Recognition is the skill; new Vector<>() is not.
Cheat sheet
Vector synchronized ArrayList ancestor → ArrayList
Stack extends Vector (LIFO + indexing) → ArrayDeque as Deque
Hashtable synchronized Map, no nulls → HashMap or ConcurrentHashMap
Dictionary abstract ancestor of Hashtable → Map
Enumeration hasMoreElements / nextElement → Iterator
Properties extends Hashtable → string defaults only
Vector.elements() / Hashtable.keys() Enumeration, not fail-fast
Vector.iterator() / Map.entrySet() fail-fast (java.util)
Stack.search 1-based from top
Properties setProperty / getProperty + parent defaults
put(Object,Object) is the Hashtable leak
Still compile = still in the JDK, not a default for new code
Do:
- Translate
Vector→ArrayList,Stack→ArrayDeque,Hashtable→HashMaporConcurrentHashMap. - Consume
Enumerationat the boundary (Collections.list) and iterate withIteratorafter that. - Keep
Propertiesfor string defaults;getProperty, notputof arbitrary objects.
Don’t:
new Vector<>()ornew Hashtable<>()because they say synchronized.- Use
Stackwhen you mean a deque — it is aVector. - Treat
PropertiesasMap<String, Order>. - Assume a synchronized method makes a check-then-act atomic.
Wrap-up
Vector, Stack, Hashtable, Dictionary, Enumeration, and Properties are the collections Java had before List and Map were names. They lock the whole structure, they still compile, and they still show up in APIs. None of them is the thread-safe default of the type they resemble. Reach for ArrayList, ArrayDeque, HashMap, and — when another thread really does look — ConcurrentHashMap.
That last type is the one Hashtable was pretending to be. Next is how it actually shares a map without synchronizing the whole table.