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 项目地址。