The Subtle Differences in toArray() Behavior Between ArrayList and Arrays.asList
Consider the following code snippet:
List<String> list = new ArrayList<>();
list.add("1");
Object[] array = list.toArray();
array[0] = 1;
System.out.println(Arrays.toString(array));
This executes without error. However, modifying the code to use Arrays.asList leads to an ArrayStoreException:
List<String> list = Arrays.asList("1");
Object[] array = list.toArray();
array[0] = 1;
System.out.println(Arrays.toString(array));
Both toArray() methods are declared to return Object[], but their runtime behavior differs due to underlying implementations.
In java.util.ArrayList, the toArray() method creates a new Object[] array by copying the internal elementData array, which is initialized as an Object[]. Therefore, assigning a Integer to an element is permissible.
// Simplified view of ArrayList.toArray()
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
In contrast, the ArrayList returned by Arrays.asList is a fixed-size wrapper around the provided array. Its toArray() method returns a clone of the backing array. Due to type erasure and the varargs mechanism, the backing array's actual type at runtime is String[], not Object[].
// Internal representation in Arrays.asList
String[] backingArray = {"1"};
List<String> wrappedList = Arrays.asList(backingArray);
Object[] result = wrappedList.toArray(); // Actually returns String[]
Thus, attempting to store an Integer in this array causes an ArrayStoreException because the runtime type is String[].
This discrepancy was identified as a bug because the Collection.toArray() contract specifies returning Object[], but the implementation for Arrays.asList violated this by returning an array of a more specific type. The issue was reported in OpenJDK around 2005 and finally resolved in JDK 9 (2015).
The fix in JDK 9 modified Arrays.asList's toArray() to always return a new Object[] array, aligning it with java.util.ArrayList:
// JDK 9+ implementation for Arrays.asList ArrayList
public Object[] toArray() {
return Arrays.copyOf(a, a.length, Object[].class);
}
This change, however, introduced a backward compatibility issue. Code that relied on the previous behavior, such as casting the returned array to a more specific type, would break:
// Works in JDK 8, throws ClassCastException in JDK 9+
String[] strings = (String[]) Arrays.asList("foo", "bar").toArray();
To mitigate this, some JDK 8 implementations included special handling in java.util.ArrayList constructors that accept collections, ensuring compatibility. For example, Oracle JDK 8 and Eclipse Temurin OpenJDK 8 had different internal workarounds.
When using the parameterized toArray(T[] a) method, performance considerations arise. The common idiom toArray(new T[0]) is generally efficient, as modern JVMs optimize zero-length array allocations. Pre-sizing the array (e.g., toArray(new T[list.size()])) can avoid an extra array copy but may waste memory if the list size changes concurrently in a non-thread-safe manner.
// Example of toArray with a pre-sized array
List<String> items = new ArrayList<>();
items.add("A");
String[] output = items.toArray(new String[0]);
Since ArrayList is not thread-safe, concurrent modifications leading to size changes should be managed externally, not relied upon for array sizing optimizations.