Back to Collections Framework Overview
Q4Question 4 of 4

How does Collection.toArray() work?

Key Concepts
  • toArray() with no argument returns Object[]; casting it to String[] throws ClassCastException because the runtime array type is Object[]
  • toArray(new String[0]) returns a properly typed String[] because the JVM uses the argument as a type hint
  • Java 11 added toArray(IntFunction): toArray(String[]::new) is the cleanest syntax using a method reference
  • Passing new String[0] is faster than new String[list.size()] because the JVM's JIT compiler optimizes zero-length array creation
  • toArray() on an empty collection returns an empty array, not null
  • For primitive arrays, use stream().mapToInt().toArray() because collections cannot hold primitives directly

Definition

toArray() returns Object[] that cannot be cast to a typed array; toArray(new String[0]) returns String[]; toArray(String[]::new) (Java 11) is the cleanest form.
  • toArray() (no argument) returns Object[], even if every element is a String; casting (String[]) list.toArray() throws ClassCastException because the runtime type is Object[]
  • toArray(T[] a) accepts a typed array as a hint; toArray(new String[0]) returns String[] of the correct size
  • toArray(IntFunction) (Java 11) accepts a generator function: toArray(String[]::new) is equivalent to size -> 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[].

JAVA
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());
        }
    }
}
TEXT
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.

JAVA
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());
    }
}
TEXT
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 into a, any remaining slots are set to null, and a is 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.

JAVA
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]
    }
}
TEXT
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.

JAVA
// 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 generator). 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].

JAVA
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));
    }
}
TEXT
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.

JAVA
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);
    }
}
TEXT
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

JAVA
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

JAVA
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

JAVA
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

JAVA
// 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

JAVA
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)

JAVA
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate