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, or Enumeration and you have to consume it.
  • A textbook or an old snippet used Stack.push as the word for LIFO.
  • A review once said “use the synchronized one” and Vector was 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.

VectorArrayList
Lockeach method synchronizednone
Growthdoubles by default (or capacityIncrement)~1.5×
Nullsallowedallowed
Listyes (since 1.2)yes
New codenoyes — 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
HashtableHashMapConcurrentHashMap
Lockwhole table, every methodnoneper-bin / concurrent ops — not the whole table
Null key / valuenoone null key, null valuesno
IteratorsentrySet iterator fail-fast; keys() / elements() are Enumerationfail-fastweakly consistent
New codenodefault mapdefault 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);
}
EnumerationIterator
AdvancehasMoreElements / nextElementhasNext / next
Removeno (classic)remove (optional)
WhereVector.elements(), Hashtable.keys() / elements(), Properties.propertyNames()every modern collection
Fail-fastusually not on the legacy enumerationsfail-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 seeYou wantWhy
new Vector<>()new ArrayList<>()default list, no whole-list lock
shared Vectorconfine, or CopyOnWriteArrayListsnapshot iterators when reads dominate
new Stack<>()new ArrayDeque<>() as DequeLIFO without inheriting Vector
new Hashtable<>()new HashMap<>()default map, one thread
shared Hashtablenew ConcurrentHashMap<>()shared map without synchronizing the whole table
DictionaryMapthe framework type
EnumerationIterator / for-eachremove, fail-fast contract, every modern collection
Properties as a domain mapHashMap / ConcurrentHashMapProperties is string defaults, and it is still a Hashtable
Properties for string defaultsProperties (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.

QuestionHonest 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 → HashMap or ConcurrentHashMap.
  • Consume Enumeration at the boundary (Collections.list) and iterate with Iterator after that.
  • Keep Properties for string defaults; getProperty, not put of arbitrary objects.

Don’t:

  • new Vector<>() or new Hashtable<>() because they say synchronized.
  • Use Stack when you mean a deque — it is a Vector.
  • Treat Properties as Map<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.

Next optional step in the series The shared map that replaced whole-table synchronized. ConcurrentHashMap: Shared Maps Without Synchronizing the Whole Table