Java List vs ArrayList: Choosing the Right Reference Type
java list vs arraylist: Understand the practical difference between the List interface and ArrayList in Java, and learn which reference type to use in your code.
When you write List<String> names = new ArrayList<>(); you are using the List interface as the reference type, not the ArrayList class itself. The distinction between java list vs arraylist is a common source of confusion for Java developers, and it directly affects how your code behaves, what methods are available, and how easily you can swap implementations later.
The Difference Between an Interface and a Class
List is an interface that defines a contract for an ordered collection that allows duplicate elements. It declares methods like add, get, remove, and size, but it provides no implementation. ArrayList is a concrete class that implements List using a resizable array internally. When you declare a variable as List, you are programming to the interface. When you declare it as ArrayList, you are tying your code to a specific implementation.
This distinction matters because Java allows you to assign any List implementation to a List variable, but only ArrayList (or a subclass) to an ArrayList variable. The choice of reference type determines what methods are visible to the compiler and how flexible your code is when requirements change.
Declaring Variables: List vs ArrayList
Consider two common declarations:
List<String> firstList = new ArrayList<>(); ArrayList<String> secondList = new ArrayList<>();
Both create an ArrayList object, but the variable type differs. With firstList, you can only call methods declared in the List interface. With secondList, you can also call methods specific to ArrayList, such as ensureCapacity and trimToSize. If you never need those extra methods, using List keeps your code more general.
More importantly, the reference type affects assignment. You can later reassign firstList to a LinkedList or a CopyOnWriteArrayList without changing the declaration, as long as those classes implement List. Reassigning secondList to anything other than an ArrayList would require changing the variable type.
What ArrayList Does Under the Hood
ArrayList maintains an internal array that grows dynamically when elements are added. The default capacity is 10, and when the array is full, it creates a new array with roughly 1.5 times the old capacity and copies the elements over. This behavior is hidden behind the List interface, so when you use a List reference, you don't need to know whether the underlying implementation is an array, a linked structure, or something else.
This implementation detail has performance implications. Accessing an element by index is O(1) because it is a direct array lookup. Inserting or removing an element in the middle is O(n) because elements must be shifted. A LinkedList has different tradeoffs: O(n) index access but O(1) insertion at the ends. If you declare your variable as List, you can switch between these implementations without changing the rest of your code, but you must understand the performance characteristics of the actual object you create.
When to Use List Over ArrayList
Use List as the reference type in most production code, especially in method parameters, return types, and fields. This is the classic "program to an interface" principle. It gives you the freedom to change the implementation later without breaking callers. For example, a method that accepts a List can be called with an ArrayList, a LinkedList, or any other List implementation.
public void processItems(List<String> items) { for (String item : items) { // process item } }
If you declare the parameter as ArrayList, you force every caller to pass an ArrayList, even if they have a different List implementation. This reduces flexibility and can lead to unnecessary conversion code.
Use ArrayList as the reference type only when you specifically need methods that are not part of the List interface, such as ensureCapacity or trimToSize, or when you are working with a local variable and you are certain the implementation will never change. Even then, consider whether those extra methods are worth the loss of abstraction.
Performance and Memory Considerations
The performance of ArrayList is generally excellent for random access and iteration, but it is not optimal for frequent insertions or deletions at arbitrary positions. The internal array must be resized when it reaches capacity, which involves copying all elements. This resizing is amortized O(1) per add, but the occasional resize can cause a noticeable pause if the list is large.
If you know the approximate number of elements in advance, you can pass an initial capacity to the ArrayList constructor to reduce resizing:
List<String> names = new ArrayList<>(1000);
This allocates an internal array of size 1000 upfront. If you exceed that, the list still grows, but fewer resizes mean less copying. This technique is useful when building a list that will hold many elements, but it is not a reason to use ArrayList as the reference type; you can still declare it as List.
Memory usage also differs between implementations. An ArrayList stores elements in a contiguous array, which has less per-element overhead than a LinkedList, which requires a separate node object for each element. If you are storing millions of objects, this overhead can be significant. Again, the reference type does not change the memory footprint; only the actual object does.
Common Pitfalls with List and ArrayList References
One common mistake is assuming that a List reference always points to an ArrayList. If you write code that relies on ArrayList-specific behavior, such as constant-time index access, and later change the implementation to LinkedList, your performance assumptions break. Always document the expected complexity of your methods, or better yet, choose the implementation that matches your performance requirements and keep the reference type generic.
Another pitfall is using ArrayList as the reference type in a public API. This locks clients into a specific implementation and makes it harder for them to pass other List types. It also prevents you from changing the implementation in the future without a breaking change. For example, if you later want to use a CopyOnWriteArrayList for thread safety, you would need to change the method signature, which could break existing callers.
Choosing the Right Type for Your API
When designing a method or a class, ask yourself: does the caller need to know the concrete type? Usually the answer is no. The List interface provides all the common operations. If you need to expose a collection that supports order and duplicates, List is the right contract. If you need to expose a collection that can be modified concurrently, you might choose a specific implementation like CopyOnWriteArrayList, but you can still declare the return type as List.
public List<String> getNames() { return new ArrayList<>(internalNames); }
This returns a List, so callers can iterate, add, or remove elements without knowing the concrete implementation. If you later decide to return a LinkedList instead, the method signature remains unchanged.
There are cases where using ArrayList as the return type is justified, such as when you want to guarantee that the returned collection supports fast random access, or when you want to allow callers to call ensureCapacity. But these cases are rare. In general, prefer List for public APIs and reserve ArrayList for internal implementation details.
Edge Cases and Compatibility Notes
Java's type system treats List and ArrayList as distinct types, which means you cannot assign an ArrayList to a List variable without an implicit upcast, which is always allowed. The reverse is not allowed without an explicit cast, and that cast can fail at runtime if the object is not actually an ArrayList.
List<String> list = new ArrayList<>(); ArrayList<String> arrayList = (ArrayList<String>) list; // works List<String> anotherList = new LinkedList<>(); ArrayList<String> invalidCast = (ArrayList<String>) anotherList; // ClassCastException
This is a fundamental aspect of Java's type safety. When you cast, you are telling the compiler that you know the runtime type, but if you are wrong, the JVM throws an exception. Avoid such casts unless you have explicit knowledge of the object's type, and prefer using the List interface to avoid them altogether.
Another compatibility note involves generic type erasure. At runtime, the JVM does not know whether a List was declared as List<String> or List<Integer>. This is true for both List and ArrayList. The reference type does not affect type erasure; only the generic type parameters are erased. So List<String> and ArrayList<String> both become raw List and ArrayList at runtime.
When you use List as the reference type, you also gain the ability to use Collections.unmodifiableList or Collections.synchronizedList to wrap the underlying collection. These wrappers implement List, so they can be assigned to a List variable. This is a powerful pattern for controlling access to a collection without changing its core behavior.
List<String> readOnly = Collections.unmodifiableList(new ArrayList<>(names));
Here, readOnly is a List, but attempts to modify it will throw UnsupportedOperationException. This is another reason to prefer List as the reference type: it allows you to swap in wrappers and decorators without altering the variable type.
In summary, the choice between List and ArrayList is not about which one is faster or more capable; it is about how much implementation detail you want to expose. By default, use List to keep your code flexible and maintainable. Use ArrayList only when you need its specific methods or when you are certain the implementation will never change. This approach reduces coupling and makes your code easier to evolve as requirements shift.