Back to Blog
Java

Java ArrayList Capacity: Size vs Capacity

java arraylist capacity: Understand how ArrayList capacity works, how resizing affects performance, and when to set initial capacity or call ensureCapacity and trimToS...

ArrayListJava CollectionsMemory ManagementPerformanceInitial Capacity
Diagram showing an ArrayList as a container with filled slots for size and empty slots for spare capacity.

Java's ArrayList is a resizable array implementation of the List interface. The phrase java arraylist capacity refers to the number of elements the backing array can hold before it must be resized. This is distinct from size, which is the number of elements currently stored. Understanding the difference matters because capacity affects both memory usage and runtime performance.

What Capacity Means in ArrayList

When you create an ArrayList without specifying an initial capacity, it starts with a small internal array, often of length 10. As you add elements, the list tracks its size separately from the array's length. The capacity is the length of that internal array; the size is how many of those slots are actually occupied. For example, after adding three elements to a new ArrayList, the size is 3, but the capacity might still be 10. The extra slots are unused but still consume memory.

The ArrayList class does not expose capacity as a public field or method. You cannot directly query it from your code. The only ways to influence capacity are through the constructor, ensureCapacity(), and trimToSize(). This design keeps the internal representation flexible, but it also means you must reason about capacity indirectly.

How ArrayList Grows Automatically

When you add an element and the size would exceed the current capacity, ArrayList must allocate a larger array and copy all existing elements into it. The exact growth factor is not specified by the Java API, but common OpenJDK implementations grow the array by roughly 50% (old capacity plus half the old capacity). This geometric growth avoids the O(n) cost of resizing on every add, because the number of resizes is logarithmic relative to the number of elements added.

Each resize is an O(n) operation because it copies every element. If you add elements one by one without knowing the final size, the total cost is still amortized O(1) per add, but the occasional resize can cause a noticeable pause in latency-sensitive code. The memory usage also spikes temporarily because both the old and new arrays exist during the copy.

Setting Initial Capacity with the Constructor

The most direct way to control java arraylist capacity is through the constructor that accepts an initialCapacity argument:

List<String> names = new ArrayList<>(1000);

This allocates an internal array of length 1000 immediately. Adding up to 1000 elements will not trigger any resize. This is useful when you know the approximate number of elements in advance, such as when reading a large file with a known number of lines or collecting results from a query that returns a bounded set.

Choosing an initial capacity that is too high wastes memory if the list ends up much smaller. Choosing one that is too low simply means resizing will occur later, which defeats the purpose. The constructor does not validate negative values; it throws IllegalArgumentException if you pass a negative number. Zero is allowed and creates an empty array.

Using ensureCapacity and trimToSize

After an ArrayList has been created, you can adjust its capacity manually. ensureCapacity(int minCapacity) grows the internal array if necessary so that it can hold at least the specified number of elements without resizing. This is useful when you know the size will grow but cannot set it at construction time. For example:

List<Integer> ids = new ArrayList<>(); ids.ensureCapacity(5000);

This call allocates an array of at least 5000 slots if the current capacity is lower. It does nothing if the current capacity is already sufficient. The method is a hint; the implementation may choose a larger capacity than requested.

trimToSize() does the opposite. It shrinks the internal array so that its capacity exactly matches the current size. This is useful when you have a large list that will remain stable and you want to release unused memory. After trimming, adding another element will force a resize, so only call trimToSize() when the list is effectively final.

Performance and Memory Tradeoffs

The main performance benefit of controlling capacity is avoiding repeated resizes. Each resize copies all existing elements, which is O(n). If you add 1 million elements to a default ArrayList, the total copying cost is amortized, but the last resize alone copies around 666,000 elements. Setting the initial capacity to 1 million avoids that entire sequence of copies.

Memory is the other side of the tradeoff. An ArrayList with a large capacity but a small size wastes memory because the backing array is allocated in full. For example, a list with capacity 1,000,000 but only 10 elements uses roughly 4 MB of references on a 64-bit JVM, even though only 40 bytes are used for the actual elements. This can become significant if you create many ArrayLists that are mostly empty.

There is also a subtle interaction with the garbage collector. A large backing array is a single object that must be scanned for references. If you keep a large-capacity ArrayList alive longer than needed, it can delay garbage collection of the objects it references. Calling trimToSize() after the list stops growing can reduce this retention.

Common Misconceptions and Edge Cases

One common misconception is that ensureCapacity() adds elements or changes the size. It does not. It only changes the capacity. The size remains unchanged. Similarly, trimToSize() does not remove elements; it only reduces the capacity to match the current size.

Another edge case is that the initial capacity constructor does not create a list with that many null elements. It creates an empty list with a backing array of that length. The size is still zero. If you need a list pre-filled with nulls or a default value, you must use a loop or Collections.nCopies().

Serialization also interacts with capacity. When you serialize an ArrayList, the serialized form includes the size and the elements, but not the capacity. When you deserialize it, the new list starts with a default capacity and will resize as needed. This means capacity is not preserved across serialization boundaries.

Choosing Capacity in Real Applications

In practice, you should set an initial capacity when you have a reasonable estimate of the final size and the list is large enough that resizing would be costly. For small lists (fewer than a few hundred elements), the overhead of resizing is negligible, and guessing a precise initial capacity adds little value. For large lists, the difference can be substantial.

Use ensureCapacity() when you cannot set the capacity at construction time but later learn the expected size. This is common in batch processing where you first count records and then populate a list. Use trimToSize() when you have a long-lived list that has finished growing and you want to minimize memory retention, such as a cached list that is read frequently but never modified.

There is no universal rule for the exact number to pass. The right choice depends on your data size, memory budget, and performance requirements. Measure your application's memory footprint and GC behavior if you suspect capacity is a bottleneck. In most cases, the default behavior is acceptable; explicit capacity management is an optimization, not a necessity.

java arraylist capacity: Practical Usage and Code Examples | RYUSLOG DEV