Definition
- Vector (Java 1.0): every public method is
synchronized, making it thread-safe but slow because even single-threaded code pays lock acquisition cost - ArrayList (Java 1.2): not synchronized, no lock overhead, preferred for all new code
- Growth: Vector doubles capacity (2x); ArrayList grows by 1.5x, using memory more efficiently
- Legacy API: Vector has
addElement(),elementAt(),elements()(returnsEnumeration); ArrayList uses standardadd(),get(),iterator() - Stack: extends Vector; never use it in new code – use
ArrayDequeinstead
Vector is like a bank counter where every operation – even checking the time – requires locking the entire counter. Even when there is only one customer, the clerk locks the counter before each action and unlocks it after. ArrayList is a counter with no lock: the clerk serves instantly. When multiple customers arrive, you can add a lock selectively (synchronizedList) or use a modern multi-window counter (CopyOnWriteArrayList) where readers never wait.
Understanding the Concepts
What is the difference between Vector and ArrayList?
Vector and ArrayList are both backed by dynamic arrays, implement the List interface, and provide the same operations. The differences are in synchronization, growth strategy, and API age.
| Feature | Vector | ArrayList |
|---|---|---|
| Introduced | Java 1.0 | Java 1.2 |
| Synchronized | Yes (every method) | No |
| Growth | 2x (doubles) | 1.5x |
| Performance | Slow (lock overhead) | Fast |
| Null allowed | Yes | Yes |
| Legacy methods | addElement, elementAt, elements() | Standard Collection API |
| Thread safety | Built-in (coarse-grained) | Not thread-safe |
| Recommendation | Do not use | Use this |
package com.bank.demo;
import java.util.*;
public class VectorVsArrayListDemo {
public static void main(String[] args) {
// Vector – synchronized, legacy
Vector<String> vector = new Vector<>();
vector.addElement("SAV2001"); // legacy method
String first = vector.elementAt(0); // legacy method
Enumeration<String> enumeration = vector.elements(); // legacy iterator
// ArrayList – modern, faster
List<String> arrayList = new ArrayList<>();
arrayList.add("SAV2001");
String firstAL = arrayList.get(0);
Iterator<String> iterator = arrayList.iterator();
System.out.println("Vector: " + vector);
System.out.println("ArrayList: " + arrayList);
}
}
Vector: [SAV2001]
ArrayList: [SAV2001]
Why is Vector slow?
Every public method in Vector is declared synchronized. This means that before executing any operation – even size() or get() – the calling thread must acquire the object's intrinsic lock (monitor).
In single-threaded code, this lock acquisition and release is pure waste. It adds 15-30% overhead compared to ArrayList for the same operations.
In multi-threaded code, the synchronization is too coarse. All threads compete for a single lock on the entire Vector. While one thread is reading, no other thread can read or write. This creates a bottleneck that modern concurrent collections avoid with fine-grained locking.
package com.bank.demo;
import java.util.*;
public class SynchronizationOverheadDemo {
public static void main(String[] args) {
int iterations = 10_000_000;
// Vector – synchronized on every operation
Vector<String> vector = new Vector<>();
vector.add("SAV2001");
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
vector.get(0); // lock, get, unlock – every time
}
long vectorTime = System.nanoTime() - start;
// ArrayList – no synchronization
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("SAV2001");
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
arrayList.get(0); // direct array access – no locking
}
long arrayListTime = System.nanoTime() - start;
System.out.printf("Vector: %d ms%n", vectorTime / 1_000_000);
System.out.printf("ArrayList: %d ms%n", arrayListTime / 1_000_000);
System.out.printf("Vector is ~%.1fx slower%n", (double) vectorTime / arrayListTime);
}
}
Vector: 85 ms
ArrayList: 32 ms
Vector is ~2.7x slower
How is the growth strategy different?
Vector doubles its capacity by default (2x growth). You can also specify a custom increment: new Vector<>(10, 5) starts at capacity 10 and grows by 5 each time.
ArrayList grows by approximately 1.5x (the formula old + old >> 1).
The 2x growth wastes more memory. After reaching capacity 1024, Vector jumps to 2048 (1024 wasted slots). ArrayList would go to 1536 (512 wasted slots).
| Starting from 10 | Vector growth | ArrayList growth |
|---|---|---|
| Resize 1 | 10 to 20 | 10 to 15 |
| Resize 2 | 20 to 40 | 15 to 22 |
| Resize 3 | 40 to 80 | 22 to 33 |
| Resize 4 | 80 to 160 | 33 to 49 |
| Resize 5 | 160 to 320 | 49 to 73 |
| To reach 1M | ~17 resizes | ~40 resizes |
| Memory at 1M | Up to 2M capacity | Up to 1.5M capacity |
What is wrong with Stack?
Stack extends Vector, which means it inherits all of Vector's problems: synchronized every method, 2x growth, legacy API. Additionally, because Stack extends Vector, it exposes all of Vector's methods – including add(index, elem), remove(index), and get(index) – which violate the LIFO contract of a stack.
Use ArrayDeque as a stack instead. It is faster, uses less memory, and does not expose non-stack methods.
package com.bank.demo;
import java.util.*;
public class StackVsArrayDequeDemo {
public static void main(String[] args) {
// BAD – Stack extends Vector (synchronized, legacy)
Stack<String> legacyStack = new Stack<>();
legacyStack.push("Step 1: Open account");
legacyStack.push("Step 2: Deposit 50000");
// Stack exposes Vector methods – breaks stack contract!
legacyStack.add(0, "INSERTED AT BOTTOM"); // not stack behavior!
// GOOD – ArrayDeque as stack
Deque<String> modernStack = new ArrayDeque<>();
modernStack.push("Step 1: Open account");
modernStack.push("Step 2: Deposit 50000");
System.out.println("Pop: " + modernStack.pop());
System.out.println("Peek: " + modernStack.peek());
// No add(index) method – clean stack API
}
}
Pop: Step 2: Deposit 50000
Peek: Step 1: Open account
What are the modern alternatives for thread safety?
| Need | Solution |
|---|---|
| Single-threaded | ArrayList (no synchronization) |
| Simple thread safety | Collections.synchronizedList(new ArrayList<>()) |
| Read-heavy concurrent | CopyOnWriteArrayList |
| Queue (producer-consumer) | ConcurrentLinkedQueue or LinkedBlockingQueue |
| Stack (concurrent) | ConcurrentLinkedDeque |
package com.bank.service;
import java.util.*;
import java.util.concurrent.*;
public class ThreadSafeAlternativesDemo {
public static void main(String[] args) {
// Option 1: synchronizedList – simple but slow under contention
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.add("SAV2001");
// Option 2: CopyOnWriteArrayList – ideal for read-heavy scenarios
// Writes copy the entire array (expensive)
// Reads are lock-free (fast)
List<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("SAV2001");
// 10,000 readers + 1 writer → perfect for event listener lists
// Iteration is always safe (uses snapshot)
for (String acc : cowList) {
System.out.println("Safe iteration: " + acc);
}
}
}
Safe iteration: SAV2001
What are common mistakes?
Mistake: Using Vector "because it is thread-safe"
// WRONG – Vector's synchronization is too coarse and slow
List<String> accounts = new Vector<>();
// Also WRONG – even Vector is not safe for compound operations
Vector<String> v = new Vector<>();
if (!v.contains("SAV2001")) {
v.add("SAV2001"); // RACE CONDITION! Another thread may add between check and add
}
// Vector synchronizes individual methods, not compound operations.
// You still need external synchronization for check-then-act patterns.
// CORRECT for compound operations
ConcurrentHashMap<String, Boolean> accounts = new ConcurrentHashMap<>();
accounts.putIfAbsent("SAV2001", Boolean.TRUE); // atomic check-and-add
What should a senior engineer know?
- Vector's compound action problem. Synchronizing individual methods does not make compound operations thread-safe. "Check if contains, then add" is still a race condition with Vector because the lock is released between contains() and add(). You need external synchronization or atomic operations.
- JIT optimization of locks. On HotSpot, the JIT compiler can sometimes eliminate locks on thread-local objects (lock elision) or replace contended locks with faster alternatives (lock coarsening). This reduces Vector's overhead in some cases, but it is not reliable enough to depend on.
- History. Vector was in Java from version 1.0 (1996). The Collections Framework was added in Java 1.2 (1998). Vector was retrofitted to implement List, which is why it has both legacy methods (elementAt) and modern methods (get). The same happened with Hashtable implementing Map.
- Stack is a design mistake. Stack should have been a interface or a composed class, not a subclass of Vector. By extending Vector, Stack inherits all Vector methods including random access – violating the LIFO abstraction. This is a textbook example of inheritance misuse.
What are common interview follow-up questions?
- "Can you make ArrayList thread-safe?" – Yes, wrap with Collections.synchronizedList() or use CopyOnWriteArrayList.
- "What is the growth factor of Vector?" – 2x by default, configurable via constructor.
- "Why is Stack a bad design?" – It extends Vector, exposing non-stack operations and inheriting synchronization overhead.
- "What is CopyOnWriteArrayList?" – A thread-safe list where writes copy the entire array, making reads lock-free.
Visual Overview
Vector vs ArrayList timeline Java 1.0 (1996) Java 1.2 (1998) Modern Java ┌──────────┐ ┌──────────┐ ┌─────────────────────┐ │ Vector │ │ ArrayList│ │ CopyOnWriteArrayList│ │ Stack │ │ LinkedList│ │ ConcurrentHashMap │ │ Hashtable│ │ HashMap │ │ ConcurrentLinkedQueue│ └──────────┘ └──────────┘ └─────────────────────┘ synchronized not synchronized fine-grained concurrency every method (faster) (fastest) Vector method overhead: ┌─────────────────────────────────┐ │ Thread calls vector.get(0) │ │ 1. Acquire monitor lock │ ← overhead │ 2. Read elementData[0] │ ← actual work │ 3. Release monitor lock │ ← overhead └─────────────────────────────────┘ ArrayList: just step 2.
Key Takeaway
Vector is a relic from Java 1.0 that synchronizes every method – add, get, remove, even size. This makes it thread-safe for individual operations but slow for everything else. Single-threaded code pays a 15-30% overhead for locks it does not need. Multi-threaded code hits a bottleneck because all threads compete for one lock.
ArrayList replaced Vector in Java 1.2 with the Collections Framework. It provides the same dynamic array behavior without synchronization. For thread safety, use Collections.synchronizedList() (simple but coarse-grained), CopyOnWriteArrayList (read-heavy, writes copy the array), or concurrent collections designed for specific access patterns.
Stack extends Vector, compounding the problem by exposing non-stack methods like add(index, elem) that violate the LIFO contract. Use ArrayDeque for both stack and queue behavior – it is faster, lighter, and has a cleaner API.
Remember one sentence: Vector is legacy, slow, and its synchronization does not even protect compound operations – use ArrayList plus modern concurrent collections instead.
---