由于 Java 泛型的实现,您不能拥有这样的代码:
public class GenSet<E> {
private E a[];
public GenSet() {
a = new E[INITIAL_ARRAY_LENGTH]; // error: generic array creation
}
}
如何在保持类型安全的同时实现这一点?
我在 Java 论坛上看到了这样的解决方案:
import java.lang.reflect.Array;
class Stack<T> {
public Stack(Class<T> clazz, int capacity) {
array = (T[])Array.newInstance(clazz, capacity);
}
private final T[] array;
}
但我真的不知道发生了什么。
我必须回答一个问题:您的GenSet
“已选中” 还是 “未选中”?那是什么意思?
检查 : 强类型 。 GenSet
明确知道它包含哪种类型的对象(即它的构造函数是使用Class<E>
参数显式调用的,并且当它们传递非E
类型的参数时,方法将抛出异常。请参阅Collections.checkedCollection
。
- > 在这种情况下,你应该写:
public class GenSet<E> {
private E[] a;
public GenSet(Class<E> c, int s) {
// Use Array native method to create array
// of a type only known at run time
@SuppressWarnings("unchecked")
final E[] a = (E[]) Array.newInstance(c, s);
this.a = a;
}
E get(int i) {
return a[i];
}
}
未选中 : 弱类型 。实际上没有对作为参数传递的任何对象进行类型检查。
- > 在这种情况下,你应该写
public class GenSet<E> {
private Object[] a;
public GenSet(int s) {
a = new Object[s];
}
E get(int i) {
@SuppressWarnings("unchecked")
final E e = (E) a[i];
return e;
}
}
请注意,数组的组件类型应该是类型参数的擦除 :
public class GenSet<E extends Foo> { // E has an upper bound of Foo
private Foo[] a; // E erases to Foo, so use Foo[]
public GenSet(int s) {
a = new Foo[s];
}
...
}
所有这些都源于 Java 中泛型的已知且有意识的弱点:它是使用擦除实现的,因此 “泛型” 类不知道它们在运行时创建的类型参数,因此无法提供类型 - 安全,除非实施一些显式机制(类型检查)。
你可以这样做:
E[] arr = (E[])new Object[INITIAL_ARRAY_LENGTH];
这是在Effective Java中实现泛型集合的建议方法之一; 项目 26 。没有类型错误,不需要重复强制转换数组。 但是,这会触发警告,因为它有潜在危险,应谨慎使用。正如注释中所详述的,此Object[]
现在伪装成我们的E[]
类型,如果使用不安全,可能会导致意外错误或ClassCastException
。
根据经验,只要在内部使用强制转换数组(例如,支持数据结构),并且不返回或暴露给客户端代码,此行为就是安全的。如果您需要将泛型类型的数组返回给其他代码,您提到的反射Array
类是正确的方法。
值得一提的是,如果你使用泛型,那么使用List
而不是数组你会更快乐。当然有时你没有选择,但使用集合框架更加健壮。
下面是如何使用泛型来获得一个正在寻找的类型的数组,同时保留类型安全性(与其他答案相反,后者将返回一个Object
数组或在编译时导致警告):
import java.lang.reflect.Array;
public class GenSet<E> {
private E[] a;
public GenSet(Class<E[]> clazz, int length) {
a = clazz.cast(Array.newInstance(clazz.getComponentType(), length));
}
public static void main(String[] args) {
GenSet<String> foo = new GenSet<String>(String[].class, 1);
String[] bar = foo.a;
foo.a[0] = "xyzzy";
String baz = foo.a[0];
}
}
那编译没有警告,并且你可以看到main
,不管是什么类型的声明的实例GenSet
作为,你可以指定a
该类型的数组,你可以指定一个元素a
该类型的变量,意味着数组和数组中的值具有正确的类型。
它的工作原理是使用类文字作为运行时类型标记,如Java 教程中所述 。编译器将类文字视为java.lang.Class
实例。要使用一个,只需使用.class
跟随类的名称。因此, String.class
充当表示类String
的Class
对象。这也适用于接口,枚举,任何维数组(例如String[].class
),基元(例如int.class
)和关键字void
(即void.class
)。
Class
本身是泛型的(声明为Class<T>
,其中T
代表Class
对象所代表的类型),这意味着String.class
的类型是Class<String>
。
因此,每当您调用GenSet
的构造函数时,都会为第一个参数传递一个类文字,表示GenSet
实例的声明类型的数组(例如, String[].class
为GenSet<String>
)。请注意,您将无法获取基元数组,因为基元不能用于类型变量。
在构造函数内部,调用方法cast
将传递的Object
参数强制转换为由调用该方法的Class
对象表示的Class
。在java.lang.reflect.Array
调用静态方法newInstance
作为Object
返回由作为第一个参数传递的Class
对象表示的类型的数组,以及作为第二个参数传递的int
指定的长度。调用该方法getComponentType
返回一个Class
表示组件类型由所表示的所述阵列的对象Class
的对象在其上调用的方法(例如String.class
为String[].class
, null
,如果Class
对象不表示一个数组) 。
最后一句话并不完全准确。调用String[].class.getComponentType()
返回一个表示类String
的Class
对象,但它的类型是Class<?>
,而不是Class<String>
,这就是为什么你不能执行以下操作的原因。
String foo = String[].class.getComponentType().cast("bar"); // won't compile
对于Class
中返回Class
对象的每个方法都是如此。
关于 Joachim Sauer 对此答案的评论(我自己没有足够的声誉对其进行评论),使用强制转换为T[]
的示例将导致警告,因为在这种情况下编译器无法保证类型安全。
关于 Ingo 的评论编辑:
public static <T> T[] newArray(Class<T[]> type, int size) {
return type.cast(Array.newInstance(type.getComponentType(), size));
}