1. 引言

在 Java 中,我们经常希望在支持泛型的类或方法中使用数组,但由于 Java 泛型机制的限制,这并不是一件容易的事。

在本篇文章中,我们将探讨在使用数组和泛型时的一些关键差异。接着,我们会通过一个示例来演示如何创建一个泛型数组。

最后,我们还会看看 Java 标准库是如何解决类似问题的。

2. 使用泛型数组时的注意事项

数组和泛型在类型检查机制上存在本质区别

  • 数组会在运行时保存并检查类型信息(即“具体类型”)。
  • 而泛型只在编译期进行类型检查,在运行时由于类型擦除机制,泛型信息会被抹除。

Java 的语法看起来似乎支持创建泛型数组:

T[] elements = new T[size];

但实际上,这段代码会抛出编译错误。

为了理解原因,考虑如下代码:

public <T> T[] getArray(int size) {
    T[] genericArray = new T[size]; // 假设这是合法的
    return genericArray;
}

在运行时,未绑定的泛型类型 T 会被擦除为 Object,因此上面的方法等价于:

public Object[] getArray(int size) {
    Object[] genericArray = new Object[size];
    return genericArray;
}

如果我们调用该方法并将结果赋值给一个 String[]

String[] myArray = getArray(5);

这段代码会编译通过,但在运行时会抛出 ClassCastException。这是因为我们将一个 Object[] 赋值给了 String[] 引用,而编译器无法将 Object[] 隐式转换为 String[]

虽然不能直接初始化泛型数组,但如果调用方提供了准确的类型信息,我们仍然可以通过一些手段来实现等价操作。

3. 创建一个泛型数组

我们以一个容量固定的栈结构 MyStack 为例。由于我们希望这个栈支持任意类型,因此使用泛型数组是一个合理的实现方式。

首先,我们定义一个用于存储栈元素的字段,它是一个泛型数组:

private E[] elements;

然后添加构造函数:

public MyStack(Class<E> clazz, int capacity) {
    elements = (E[]) Array.newInstance(clazz, capacity);
}

⚠️ 注意:我们使用了 java.lang.reflect.Array#newInstance 来初始化泛型数组。该方法需要两个参数:

  • 第一个参数指定数组元素的类型。
  • 第二个参数指定数组长度。

由于 Array.newInstance 返回的是 Object,我们需要将其强制转换为 E[] 类型。

📌 另外,注意参数命名习惯:使用 clazz 而不是 class,因为后者是 Java 关键字。

4. 使用 ArrayList 替代数组

4.1. 使用 ArrayList 简化实现

在大多数情况下,使用泛型 ArrayList 比直接使用泛型数组要简单得多。我们可以轻松地将 MyStack 改为使用 ArrayList 实现:

首先定义存储元素的字段:

private List<E> elements;

然后在构造函数中初始化:

elements = new ArrayList<>(capacity);

这种方式不需要使用反射,也不需要传递 Class 对象,代码更加简洁。

由于 ArrayList 支持设置初始容量,性能上也能达到与数组相当的效果。

因此,只有在特定场景(如与外部库交互要求使用数组)时才需要手动创建泛型数组。

4.2. ArrayList 内部实现

有趣的是,ArrayList 本身也是通过泛型数组实现的。我们来看看它的内部结构:

transient Object[] elementData;

可以看到,ArrayList 使用 Object[] 存储元素。因为泛型类型在运行时是未知的,只能使用 Object 作为所有类型的父类。

📌 几乎所有 ArrayList 的操作都可以使用这个泛型数组完成,因为它们不需要对外暴露强类型数组(除了 toArray 方法)。

5. 从集合中构建数组

5.1. LinkedList 示例

在 Java 集合框架中,我们经常需要从集合构建数组。以 LinkedList 为例:

List<String> items = new LinkedList<>();
items.add("first item");
items.add("second item");

然后构建数组:

String[] itemsAsArray = items.toArray(new String[0]);

📌 List.toArray(T[] a) 方法要求传入一个数组,用于获取类型信息,从而创建对应类型的数组。

在这个例子中,我们使用 new String[0] 作为输入,生成一个 String[] 类型的数组。

5.2. LinkedList.toArray 的实现

我们来看看 LinkedList.toArray 的源码实现:

public <T> T[] toArray(T[] a)

在需要创建新数组时,它使用反射来创建:

a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);

可以看到,这里也使用了 Array.newInstance,并通过传入的数组 a 获取组件类型(即数组元素类型),然后强制转换为 T[]

6. 从 Stream 中创建数组

6.1. 使用 toArray 创建数组

Java 8 的 Streams API 提供了从流中创建数组的功能,但需要注意类型擦除带来的问题。

Object[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
  .filter(string -> string.startsWith("A"))
  .toArray();

assertThat(strings).containsExactly("A", "AAA", "AAB");

⚠️ 注意:toArray() 默认返回的是 Object[],而不是 String[]

assertThat(strings).isNotInstanceOf(String[].class);

6.2. 使用 toArray 重载方法获取指定类型数组

为了避免类型问题,可以使用 toArray 的重载方法,传入一个生成器函数:

String[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
  .filter(string -> string.startsWith("A"))
  .toArray(String[]::new);

assertThat(strings).containsExactly("A", "AAA", "AAB");
assertThat(strings).isInstanceOf(String[].class);

这里的 String[]::new 是一个 IntFunction,接收一个整数(数组长度),返回一个指定大小的数组。

6.3. 泛型类型参数的处理

假设我们希望将 Stream<String> 转换为 Optional<String>[]

Stream<Optional<String>> stream = Stream.of("A", "AAA", "B", "AAB", "C")
  .filter(string -> string.startsWith("A"))
  .map(Optional::of);

尝试直接创建数组会失败:

// 编译错误
Optional<String>[] strings = new Optional<String>[1];

但可以通过类型转换解决:

Optional<String>[] strings = stream.toArray(Optional[]::new);

⚠️ 这会触发“未检查转换”警告,需要添加注解:

@SuppressWarnings("unchecked")

6.4. 使用辅助函数避免警告

为了减少重复的 @SuppressWarnings,可以封装一个辅助函数:

@SuppressWarnings("unchecked")
static <T, R extends T> IntFunction<R[]> genericArray(IntFunction<T[]> arrayCreator) {
    return size -> (R[]) arrayCreator.apply(size);
}

使用方式如下:

Optional<String>[] strings = Stream.of("A", "AAA", "B", "AAB", "C")
  .filter(string -> string.startsWith("A"))
  .map(Optional::of)
  .toArray(genericArray(Optional[]::new));

⚠️ 注意:此函数可能会被误用,例如:

ArrayList<String>[] lists = Stream.of(singletonList("A"))
  .toArray(genericArray(List[]::new));

虽然能编译通过,但会抛出 ClassCastException,因为 ArrayList[] 不是 List[] 的子类。

7. 总结

本文讨论了数组与泛型之间的关键差异,并演示了如何在 Java 中创建泛型数组。

✅ 我们还比较了使用 ArrayList 和泛型数组的优劣,并分析了 Java 集合框架中是如何处理泛型数组的。

最后,我们介绍了如何通过 Java Streams API 创建数组,以及如何处理包含泛型类型参数的数组创建问题。

📌 要点回顾:

  • ❌ 不能直接使用 new T[size] 创建泛型数组
  • ✅ 使用 Array.newInstance(clazz, size) + 强制转换
  • ArrayList 是更常用的替代方案
  • Stream.toArray() 需要传入类型生成器函数
  • ⚠️ 泛型数组创建涉及类型擦除和未检查转换,需谨慎处理

如需查看完整示例代码,请访问 GitHub 项目地址


原始标题:Creating a Generic Array in Java | Baeldung