Definition
toArray()(no argument) returnsObject[], even if every element is a String; casting(String[]) list.toArray()throwsClassCastExceptionbecause the runtime type isObject[]toArray(T[] a)accepts a typed array as a hint;toArray(new String[0])returnsString[]of the correct sizetoArray(IntFunction(Java 11) accepts a generator function:) toArray(String[]::new)is equivalent tosize -> new String[size]- If the provided array is large enough, elements are placed into it and any extra slots are set to null; if too small, a new array of the correct type and size is created
Imagine dumping all account cards from a drawer into a box. toArray() gives you a generic box labeled "Objects" – even though every card inside is an account card, the box label says "Objects" and you cannot relabel it. toArray(new String[0]) gives you a box labeled "Strings" from the start – the right label, the right size. toArray(String[]::new) is the same thing with cleaner handwriting on the label.
Understanding the Concepts
What does toArray() with no arguments return?
The no-argument toArray() method returns an Object[]. The runtime type of the returned array is Object[], regardless of what the collection actually contains.
This means you cannot cast the result to a more specific type. Even if your List contains only Strings, (String[]) list.toArray() throws ClassCastException at runtime because Object[] is not a subtype of String[].
package com.bank.demo;
import java.util.*;
public class ToArrayBasicDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
// toArray() returns Object[]
Object[] objArray = accounts.toArray();
System.out.println("Type: " + objArray.getClass().getSimpleName());
System.out.println("Element: " + objArray[0]);
// WRONG – casting Object[] to String[] throws ClassCastException
try {
String[] strArray = (String[]) accounts.toArray();
} catch (ClassCastException e) {
System.out.println("Cast failed: " + e.getMessage());
}
}
}
Type: Object[]
Element: SAV2001
Cast failed: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;
Why does the cast fail?
Java arrays carry their component type at runtime. When toArray() creates the array internally, it uses new Object[size]. The resulting array has runtime type Object[]. An Object[] is not the same thing as a String[], even if all its elements happen to be Strings.
In Java's type system, String[] is a subtype of Object[] (covariant arrays), but Object[] is NOT a subtype of String[]. You can widen String[] to Object[], but not narrow Object[] to String[].
This is different from generics, where type information is erased at runtime. Array types are reified – the JVM knows the actual component type.
package com.bank.demo;
public class ArrayTypeDemo {
public static void main(String[] args) {
String[] strings = new String[]{"SAV2001", "CUR3001"};
Object[] objects = strings; // OK – String[] widens to Object[]
System.out.println("Widened: " + objects.getClass().getSimpleName());
// Still String[] at runtime!
Object[] pureObjects = new Object[]{"SAV2001", "CUR3001"};
// String[] back = (String[]) pureObjects; // ClassCastException!
// pureObjects is Object[] at runtime, cannot narrow
System.out.println("Runtime type: " + pureObjects.getClass().getSimpleName());
}
}
Widened: String[]
Runtime type: Object[]
How does toArray(T[]) work?
The typed version toArray(T[] a) uses the provided array as a type hint. The method inspects the component type of the argument array and creates a new array of that type if needed.
The behavior depends on the size of the argument:
- If
a.length >= collection.size(): elements are copied intoa, any remaining slots are set to null, andais returned. - If
a.length < collection.size(): a new array of the same component type is created with the exact right size, elements are copied in, and the new array is returned.
The idiomatic pattern is toArray(new String[0]) – pass a zero-length array. The JVM creates a perfectly-sized array internally.
package com.bank.demo;
import java.util.*;
public class TypedToArrayDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
// Preferred: pass zero-length array
String[] arr1 = accounts.toArray(new String[0]);
System.out.println("Type: " + arr1.getClass().getSimpleName());
System.out.println("Length: " + arr1.length);
System.out.println("Content: " + Arrays.toString(arr1));
// Passing exact-size array
String[] arr2 = accounts.toArray(new String[3]);
System.out.println("Same content: " + Arrays.toString(arr2));
// Passing larger array – extra slots are null
String[] arr3 = accounts.toArray(new String[5]);
System.out.println("Oversized: " + Arrays.toString(arr3));
// [SAV2001, CUR3001, FD4001, null, null]
}
}
Type: String[]
Length: 3
Content: [SAV2001, CUR3001, FD4001]
Same content: [SAV2001, CUR3001, FD4001]
Oversized: [SAV2001, CUR3001, FD4001, null, null]
Why is new String[0] faster than new String[list.size()]?
This is counterintuitive, but JMH benchmarks consistently show that toArray(new String[0]) is about 10% faster than toArray(new String[list.size()]) on modern JVMs.
The reason is JIT compiler optimization. When you pass new String[0], the JVM knows the array is too small and will be replaced. The JIT compiler can use an intrinsic that allocates a perfectly-sized array and fills it in one operation, skipping the zero-initialization of the intermediate array.
When you pass new String[list.size()], the JVM allocates the array, zero-initializes all slots (fills with null), then copies the elements. That zero-initialization is wasted work because every slot gets overwritten.
This optimization was documented by Aleksey Shipilev (JVM performance engineer at Red Hat) and has been the recommended pattern since.
// Preferred – let the JVM optimize
String[] arr = accounts.toArray(new String[0]);
// Slower – JVM zero-initializes then overwrites
String[] arr2 = accounts.toArray(new String[accounts.size()]);
// Both produce the same result, but [0] is faster
How does toArray(IntFunction) work in Java 11?
Java 11 added a third overload: toArray(IntFunction. You pass a function that takes a size and returns an array of that size. The cleanest way is a method reference: String[]::new.
This is syntactic sugar over the toArray(T[]) version, but it reads better and avoids the visual noise of new String[0].
package com.bank.demo;
import java.util.*;
public class IntFunctionToArrayDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
// Java 11: method reference – cleanest syntax
String[] arr1 = accounts.toArray(String[]::new);
System.out.println("Method ref: " + Arrays.toString(arr1));
// Equivalent lambda
String[] arr2 = accounts.toArray(size -> new String[size]);
System.out.println("Lambda: " + Arrays.toString(arr2));
// Works with any type
Set<Integer> amounts = Set.of(50000, 25000, 75000);
Integer[] intArr = amounts.toArray(Integer[]::new);
System.out.println("Integers: " + Arrays.toString(intArr));
}
}
Method ref: [SAV2001, CUR3001, FD4001]
Lambda: [SAV2001, CUR3001, FD4001]
Integers: [75000, 50000, 25000]
What about converting to primitive arrays?
Collections cannot hold primitives (int, double, long). They hold wrapper objects (Integer, Double, Long). To convert a collection to a primitive array, use streams.
package com.bank.demo;
import java.util.*;
public class PrimitiveArrayDemo {
public static void main(String[] args) {
List<Integer> balances = List.of(50000, 25000, 75000, 10000);
// Convert to int[] using stream
int[] intArray = balances.stream().mapToInt(Integer::intValue).toArray();
System.out.println("int[]: " + Arrays.toString(intArray));
// Convert to double[] for interest calculations
double[] doubleArray = balances.stream().mapToDouble(Integer::doubleValue).toArray();
System.out.println("double[]: " + Arrays.toString(doubleArray));
// Convert to long[] for large amounts
long[] longArray = balances.stream().mapToLong(Integer::longValue).toArray();
System.out.println("long[]: " + Arrays.toString(longArray));
// Reverse: primitive array to List
int[] raw = {50000, 25000, 75000};
List<Integer> list = Arrays.stream(raw).boxed().toList();
System.out.println("List: " + list);
}
}
int[]: [50000, 25000, 75000, 10000]
double[]: [50000.0, 25000.0, 75000.0, 10000.0]
long[]: [50000, 25000, 75000, 10000]
List: [50000, 25000, 75000]
What are the edge cases?
Empty collection returns empty array, not null
List<String> empty = List.of();
String[] arr = empty.toArray(new String[0]);
System.out.println("Length: " + arr.length); // 0, not null
System.out.println("Is null: " + (arr == null)); // false
Null elements are preserved
List<String> withNull = new ArrayList<>();
withNull.add("SAV2001");
withNull.add(null);
withNull.add("CUR3001");
String[] arr = withNull.toArray(new String[0]);
System.out.println(Arrays.toString(arr)); // [SAV2001, null, CUR3001]
toArray on concurrent collection during modification
import java.util.concurrent.*;
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.add("SAV2001");
// toArray() on concurrent collections is safe but may reflect
// a snapshot that is immediately stale
String[] snapshot = queue.toArray(new String[0]);
What are common mistakes?
Mistake 1: Casting toArray() result
// WRONG
String[] arr = (String[]) list.toArray(); // ClassCastException!
// CORRECT
String[] arr = list.toArray(new String[0]);
Mistake 2: Using oversized array and not checking for nulls
List<String> accounts = List.of("SAV2001", "CUR3001");
String[] arr = accounts.toArray(new String[10]);
// arr = [SAV2001, CUR3001, null, null, null, null, null, null, null, null]
// If you iterate without checking length, you process null elements
for (String acc : arr) {
// acc.toUpperCase(); // NullPointerException on arr[2]!
if (acc != null) System.out.println(acc.toUpperCase());
}
Mistake 3: Forgetting that toArray returns a new array (not backed by the collection)
List<String> accounts = new ArrayList<>(List.of("SAV2001", "CUR3001"));
String[] arr = accounts.toArray(new String[0]);
arr[0] = "MODIFIED";
System.out.println(accounts.get(0)); // SAV2001 – original unchanged
// The array is a copy, not a view
What should a senior engineer know?
- Generic array creation limitation. You cannot write new T[size] in Java due to type erasure. That is exactly why toArray() without arguments returns Object[] – the method does not know the generic type at runtime. The typed overloads work around this by accepting an array argument that carries the component type.
- The Arrays.copyOf trick. Internally, toArray(T[]) uses Arrays.copyOf() or Array.newInstance() to create the correctly-typed array. This uses reflection under the hood, but the JIT compiler optimizes it to near-native speed.
- Stream.toArray vs Collection.toArray. stream().toArray(String[]::new) also works but is slower for simple conversions because it creates a stream pipeline. Use collection.toArray(String[]::new) (Java 11+) or collection.toArray(new String[0]) for direct conversion.
- Varargs and toArray. When passing an array to a varargs method, be aware that toArray() returns Object[], which may cause unintended behavior with varargs that expect String[].
- Legacy API integration. Some older banking APIs and JDBC methods require arrays. toArray(new String[0]) is the bridge between collections and array-based APIs.
What are common interview follow-up questions?
- "Why can't you cast Object[] to String[]?" – because array types are reified; the runtime type is Object[], not String[].
- "Why is new String[0] faster than new String[list.size()]?" – JIT intrinsic skips zero-initialization.
- "How do you convert a List to int[]?" –
list.stream().mapToInt(Integer::intValue).toArray(). - "Is the returned array backed by the collection?" – no, it is a copy; modifying the array does not affect the collection.
Visual Overview
toArray() variants
list.toArray() → Object[] (cannot cast to String[]!)
list.toArray(new String[0]) → String[] (type hint, JVM optimized)
list.toArray(String[]::new) → String[] (Java 11, cleanest)
How toArray(T[] a) decides:
a.length >= list.size()?
├── YES → copy elements into a, set remaining slots to null, return a
└── NO → create new T[list.size()], copy elements, return new array
Performance:
new String[0] new String[list.size()]
┌──────────┐ ┌──────────────────┐
│ JIT sees │ │ Allocate array │
│ size = 0 │ │ Zero-init slots │
│ Creates │ │ Copy elements │
│ optimal │ │ (zero-init was │
│ array │ │ wasted work) │
└──────────┘ └──────────────────┘
~10% faster slightly slower
Key Takeaway
Converting a collection to an array is one of those tasks that looks simple but has a subtle trap. The no-argument toArray() returns Object[], and casting it to String[] throws ClassCastException because Java arrays carry their component type at runtime. The solution is the typed overload: toArray(new String[0]) tells the JVM to create a String[], and the JVM handles the sizing internally.
In the National Bank of India system, Rajesh uses accounts.toArray(new String[0]) when passing a list of account numbers to a legacy JDBC API that expects a String[] parameter. He uses the zero-length array idiom because it is both correct and slightly faster than new String[accounts.size()] – the JIT compiler has a special optimization path for this pattern.
Since Java 11, the cleanest syntax is accounts.toArray(String[]::new), which uses a method reference to create the array. For primitive arrays, you need streams: list.stream().mapToInt(Integer::intValue).toArray() produces an int[].
Remember one sentence: never cast toArray() to a typed array – pass a zero-length typed array as a hint, and the JVM creates the correctly-typed result for you.
---