ArrayList vs Vector
ArrayList and Vector look almost identical on the surface — both implement the List interface, both back their elements with a resizable array, and both have been in the JDK for decades. The crucial difference is that Vector was built in Java 1.0 and every single one of its public methods is synchronized, while ArrayList (added in Java 1.2) has no synchronization at all. That one design decision shapes every trade-off between them.
The Core Difference at a Glance
| Feature | ArrayList | Vector |
|---|---|---|
| Introduced | Java 1.2 | Java 1.0 |
| Synchronization | None | Every method is synchronized |
| Thread-safe? | No (by default) | Yes (but coarsely) |
| Growth factor | 50% (oldCapacity + oldCapacity >> 1) | 100% (doubles) or custom increment |
| Performance | Faster in single-threaded code | Slower due to lock overhead |
| Preferred today? | Yes | Rarely — use safer alternatives |
| Legacy status | Active | Effectively legacy |
Basic Usage — Identical API
Because both classes implement List, you can swap them by changing only the constructor:
import java.util.ArrayList;
import java.util.Vector;
import java.util.List;
public class BasicDemo {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> vector = new Vector<>();
arrayList.add("Java");
arrayList.add("Python");
vector.add("Java");
vector.add("Python");
System.out.println("ArrayList: " + arrayList);
System.out.println("Vector: " + vector);
System.out.println("ArrayList size: " + arrayList.size());
System.out.println("Vector size: " + vector.size());
}
}
Output:
ArrayList: [Java, Python]
Vector: [Java, Python]
ArrayList size: 2
Vector size: 2
Note: Because both implement
List, any method that accepts aList<T>parameter works with either class without any changes.
Synchronization — The Big Deal
Vector’s synchronization means that when one thread calls add(), get(), or remove(), it must acquire the object’s intrinsic lock before proceeding. Every other thread trying to call any method on that same Vector is blocked until the lock is released.
import java.util.Vector;
public class VectorSyncDemo {
public static void main(String[] args) throws InterruptedException {
Vector<Integer> vector = new Vector<>();
// Two threads safely adding to Vector
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) vector.add(i);
});
Thread t2 = new Thread(() -> {
for (int i = 5; i < 10; i++) vector.add(i);
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Size: " + vector.size()); // always 10
System.out.println(vector);
}
}
Output (order of elements may vary):
Size: 10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
This looks safe — and the size() call is indeed always 10 — but Vector’s synchronization has a hidden trap: it synchronizes individual method calls, not compound actions.
// DANGER: still not thread-safe, even with Vector!
if (!vector.isEmpty()) { // lock acquired and released
Object o = vector.get(0); // lock acquired and released — another thread may have removed index 0 between these two calls!
}
Warning: Method-level synchronization does not protect you from check-then-act races. For truly safe compound operations you still need external synchronization or a purpose-built concurrent collection like
CopyOnWriteArrayList.
Performance Impact of Synchronization
Every synchronized method call involves acquiring and releasing a monitor lock. Even when only one thread is running, the JVM must handle the lock bookkeeping. Here is a simplified benchmark that illustrates the overhead:
import java.util.ArrayList;
import java.util.Vector;
import java.util.List;
public class PerfDemo {
static final int N = 1_000_000;
public static void main(String[] args) {
List<Integer> al = new ArrayList<>(N);
List<Integer> v = new Vector<>(N);
long start = System.nanoTime();
for (int i = 0; i < N; i++) al.add(i);
long alTime = System.nanoTime() - start;
start = System.nanoTime();
for (int i = 0; i < N; i++) v.add(i);
long vTime = System.nanoTime() - start;
System.out.printf("ArrayList add: %d ms%n", alTime / 1_000_000);
System.out.printf("Vector add: %d ms%n", vTime / 1_000_000);
}
}
Output (approximate, single thread):
ArrayList add: 18 ms
Vector add: 45 ms
Vector can be 2–3× slower than ArrayList in single-threaded benchmarks purely because of the lock overhead. The JIT compiler can sometimes eliminate redundant locks via lock elision, but this optimisation is not guaranteed.
Growth Strategy
Both classes grow their internal array when capacity is exceeded, but they use different strategies:
ArrayListgrows tooldCapacity + (oldCapacity >> 1)— that is, 50% more than the current size.Vectordoubles to2 × oldCapacityby default. You can also pass acapacityIncrementto the constructor to set a fixed growth step.
import java.util.Vector;
public class VectorGrowthDemo {
public static void main(String[] args) {
// Capacity increment of 5 — grows by exactly 5 slots each time
Vector<String> v = new Vector<>(4, 5);
for (int i = 0; i < 10; i++) v.add("item" + i);
System.out.println("Size: " + v.size());
// internal capacity is now 4 + 5 + 5 = 14 after two growths
}
}
Output:
Size: 10
Tip:
Vector’s doubling strategy wastes more memory on average thanArrayList’s 50% growth. For large lists this difference adds up quickly.
Vector-Specific Methods
Vector exposes several legacy methods that predate the List interface. They still work, but modern code should prefer the standard List API equivalents:
import java.util.Vector;
public class LegacyMethodsDemo {
public static void main(String[] args) {
Vector<String> v = new Vector<>();
v.addElement("Alpha"); // same as add()
v.addElement("Beta");
v.addElement("Gamma");
System.out.println(v.elementAt(1)); // same as get(1)
System.out.println(v.firstElement()); // same as get(0)
System.out.println(v.lastElement()); // same as get(size()-1)
System.out.println(v.capacity()); // current internal array size (10)
v.removeAllElements(); // same as clear()
System.out.println("After clear: " + v.size());
}
}
Output:
Beta
Alpha
Gamma
10
After clear: 0
Note: Methods like
addElement(),elementAt(), andremoveAllElements()exist only onVector. Avoid them in new code — they make it harder to swap the implementation later.
When to Use Each
Use ArrayList when:
- You are writing single-threaded code (the vast majority of cases).
- You need maximum read/iteration performance.
- You want to add thread-safety yourself with a more targeted approach.
Use Vector when:
- You are maintaining legacy code that already uses
Vectorand the cost of migration outweighs the benefit. - Practically speaking, you almost never need to reach for
Vectorin new code.
The modern alternatives to Vector
If you genuinely need a thread-safe List, these are your options — all better than Vector:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ThreadSafeAlternatives {
public static void main(String[] args) {
// Option 1: wrap an ArrayList with a synchronized wrapper
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// Option 2: CopyOnWriteArrayList — reads are lock-free, writes copy the array
List<String> cowList = new CopyOnWriteArrayList<>();
syncList.add("hello");
cowList.add("hello");
System.out.println(syncList);
System.out.println(cowList);
}
}
Output:
[hello]
[hello]
| Thread-safe option | Best for |
|---|---|
Collections.synchronizedList(new ArrayList<>()) | Low-contention, general-purpose thread-safety |
CopyOnWriteArrayList | Many reads, very few writes (e.g., listener lists) |
Vector | Legacy code only |
Tip:
CopyOnWriteArrayListmakes a full array copy on every write, so it is only efficient when reads vastly outnumber writes. For high-write concurrent scenarios, consider aConcurrentLinkedQueueor other purpose-built concurrent structure fromjava.util.concurrent.
Under the Hood
Lock Elision by the JIT
Modern JVMs (via the JIT compiler) can perform lock elision — if escape analysis proves that a Vector instance cannot be accessed by more than one thread, the JIT may remove the lock overhead entirely. In practice this optimisation is unreliable across JVM versions, and you should never rely on it as a justification for using Vector.
modCount and Fail-Fast Iterators
Like ArrayList, Vector tracks structural modifications with a modCount counter. If you modify a Vector while iterating over it using an Iterator or enhanced for loop, a ConcurrentModificationException is thrown. This is the fail-fast behaviour shared by most java.util collections.
Relationship to Stack
Java’s legacy Stack class extends Vector, inheriting all its synchronization and growth behaviour. This is widely considered a design mistake — Stack is better replaced by ArrayDeque in modern code, just as Vector is better replaced by ArrayList.
Memory Layout
Both ArrayList and Vector store a reference to an Object[] backing array plus a handful of int fields (size, modCount, capacityIncrement for Vector). The actual element objects live elsewhere on the heap; the array holds only their references (4 or 8 bytes each, depending on JVM pointer compression). Garbage collection reclaims unreferenced elements automatically.
Quick Decision Cheat Sheet
| Question | Answer |
|---|---|
| Single-threaded code? | ArrayList — always |
| Need thread-safety? | CopyOnWriteArrayList or Collections.synchronizedList() |
Maintaining old code that uses Vector? | Keep Vector, migrate when opportunity allows |
| Need a stack? | ArrayDeque (not Stack / not Vector) |
| Need enumeration over elements? | Prefer Iterator; Vector.elements() is legacy |
Related Topics
- ArrayList — deep dive into ArrayList internals, capacity management, and methods
- Vector — full reference for the Vector class and its legacy API
- Concurrent Collections —
CopyOnWriteArrayList,ConcurrentHashMap, and other thread-safe containers - Synchronization — how Java monitors and
synchronizedwork under the hood - List Interface — the common contract shared by ArrayList, Vector, and LinkedList
- Collections Utility Class —
synchronizedList(),unmodifiableList(), and other wrappers