PAYATHON 2026

Java 中如何安全地将 List<T> 转换为 T[] 数组?

数据阿明

可以安全地转换,但需要提供数组的运行时类型。已有 T[] stuff 时,如果它已经初始化,且运行时组件类型符合预期,可以直接写:

stuff = moreStuff.toArray(Arrays.copyOf(stuff, 0));

需要导入 java.util.Arrays。不要用 (T[]) moreStuff.toArray() 获取正确运行时类型的数组:无参数 toArray() 返回 Object[],强制转换不会改变数组的实际类型。

为什么不能使用 new T[0]?

Java 的泛型通常通过类型擦除实现,而数组在运行时保留组件类型。例如,String[] 和 Object[] 是不同的运行时类型,向数组写入元素时也会检查类型。

在 WontCompile<T> 内,运行时通常无法仅凭类型参数 T 确定要创建哪种数组,因此 Java 禁止直接使用 new T[0]。

即使调用方创建了 WontCompile<String>,也不会因此生成一份内部可以直接创建 String[] 的专用类实现。

(T[]) 会在什么情况下抛出异常?

在原来的无上界声明 WontCompile<T> 中,T 擦除为 Object,T[] 擦除为 Object[]。因此,将 toArray() 返回的 Object[] 转换为 T[] 时,通常不会立即抛出异常。风险会在后续代码要求具体数组类型时暴露。

下面的示例演示了这个问题:

import java.util.List;

public class CastExample {
    @SuppressWarnings("unchecked")
    static <T> T[] unsafe(List<T> values) {
        return (T[]) values.toArray();
    }

    public static void main(String[] args) {
        String[] values = unsafe(List.of("a", "b"));
        // ClassCastException:实际返回 Object[],不能转换为 String[]
    }
}

异常发生在调用方取得 String[] 时。@SuppressWarnings("unchecked") 只隐藏警告,不能保证类型安全。即使列表为空,Object[] 也不会变成 String[]。

如果类型参数具有其他上界,异常还可能直接发生在方法内部:

@SuppressWarnings("unchecked")
static <T extends Number> T[] unsafeNumbers(List<T> values) {
    return (T[]) values.toArray();
}

这里 T[] 擦除为 Number[],而无参数 toArray() 返回 Object[],因此转换本身就会失败。

方案一:复用已有数组的运行时类型

原代码中已有 stuff,可以用它创建一个运行时类型相同的空数组:

public void addStuff(T... items) {
    List<T> moreStuff = new ArrayList<>(Arrays.asList(stuff));

    for (T item : items) {
        moreStuff.add(item);
    }

    // 其他添加或调整 moreStuff 的操作……

    stuff = moreStuff.toArray(Arrays.copyOf(stuff, 0));
}

Arrays.copyOf(stuff, 0) 保留 stuff 的运行时数组类型。随后,toArray(T[] a) 根据该类型创建足够长的数组并填充元素,不需要未检查的强制转换。

使用这个方案有两个条件:

  • stuff 已初始化,不能为 null。
  • stuff 的运行时组件类型能够容纳 moreStuff 中的所有元素。

第二个条件与数组协变有关。例如,一个声明为 Number[] 的变量实际可能引用 Integer[]。如果列表随后加入 Double,按该数组类型转换就会抛出 ArrayStoreException。

也可以使用 moreStuff.toArray(stuff),但传入的数组足够长时,方法会复用该数组,返回数组的长度可能大于列表大小。传入零长度副本可以让结果长度与列表大小一致。

方案二:让调用方提供数组工厂

如果类需要自行初始化数组,可以在构造时接收 IntFunction<T[]>。调用方通过 String[]::new 等方法引用提供实际数组类型:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.IntFunction;

public class WontCompile<T> {
    private final IntFunction<T[]> arrayFactory;
    private T[] stuff;

    public WontCompile(IntFunction<T[]> arrayFactory) {
        this.arrayFactory = arrayFactory;
        this.stuff = arrayFactory.apply(0);
    }

    @SafeVarargs
    public final void addStuff(T... items) {
        List<T> moreStuff = new ArrayList<>(Arrays.asList(stuff));

        for (T item : items) {
            moreStuff.add(item);
        }

        stuff = moreStuff.toArray(arrayFactory.apply(0));
    }
}

使用方式:

WontCompile<String> strings = new WontCompile<>(String[]::new);
strings.addStuff("a", "b");

WontCompile<Number> numbers = new WontCompile<>(Number[]::new);
numbers.addStuff(1, 2.5);

数组工厂应返回能够容纳所有合法 T 值的数组。例如,T 为 Number 时应使用 Number[]::new;如果提供更窄的 Integer[]::new,加入 Double 时仍可能失败。

这里的 @SafeVarargs 适用于示例中只读取可变参数元素的实现。它对方法声明有相应限制,因此示例将方法设为 final。后续修改方法时,也需要继续保证可变参数的使用安全。

还需要注意什么?

原代码中的 List.of(stuff) 不接受 null 元素;上面的示例使用 Arrays.asList(stuff),允许数组中包含 null。两者都要求 stuff 本身已经初始化。

如果类型实参本身也是参数化类型,例如 List<String>,数组工厂也不能绕过 Java 对泛型数组创建的限制。这类场景通常更适合继续使用集合。

对于当前问题,已有合适的 stuff 就使用零长度副本;需要由类初始化数组时,让调用方提供数组工厂。如果内部操作主要是增删元素,也可以直接保存 List<T>,只在确实需要数组的接口边界执行转换。

备注:内容仅供参考。