泛型数组
正如之前在Erase.java
中所见,一般的解决方法是在任何想要创建泛型的地方使用ArrayList
public class ListOfGenerics<T> {
private List<T> array = new ArrayList<>();
public void add(T item) {
array.add(item);
}
public T get(int index) {
return array.get(index);
}
}
这里将获得数组的行为,以及由泛型提供的编译器的类型安全。
但有时,仍然希望创建泛型类型的数组(例如,ArrayList
内部使用的是数组)。但有趣的是,可以按照编译器喜欢的方式来定义一个引用。
class Generic<T> {
}
public class ArrayOfGenericReference {
static Generic<Integer> gia;
}
上面的代码可以使得编译器通过编译,并且不会产生任意的警告。但是却永远无法创建这个确切类型的数组(包括类型参数),因此这会令人困惑。既然所有的数组不论它们持有的类型如何,都具有相同的结构(每个数组的槽位的尺寸和数组的布局),那么看起来也应该可以创建一个Object
数组,并将其转型为所希望的数组类型。事实上这可以编译,但是不能运行,因为它将产生ClassCaseException
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
// ClassCastException
gia = (Generic<Integer>[]) new Object[SIZE];
gia = (Generic<Integer>[]) new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
// compile error
// gia[1] = new Object();
// gia[2] = new Generic<Double>();
}
}
上面ClassCastException
出现的原因是数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此,即使gia
已经被转型为Generic<Integer>[]
,但是这个信息也只存在编译期(假如没@SuppressWarnings("unchecked")
,还会获得关于这条转型的一条警告)。在运行时,它仍旧是Object
数组,这将引发问题。所以,创建泛型数组的方式是创建一个被擦除类型的新数组,然后对其进行转型。
泛型数组包装器
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int size) {
array = (T[]) new Object[size];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<>(10);
// ClassCastException
Integer[] ia = gai.rep();
// This is Ok
Object[] pa = gai.rep();
}
}
与前面相同,不能直接在代码中声明T[] array = new T[size]
,因此创建了一个Object
数组,然后转型为T[]。
rep()
方法将返回T[]
,它在main()
中将用于gai
,因此应该是Integer[]
,但是如果调用它,并尝试赋值给Integer[]
的引用,就会抛出ClassCastException
,这是因为实际的运行时类型是Object[]
。
当去除掉@SuppressWarnings("unchecked")
,编译器就会对Object[]
转型为T[]
提出异常。
因为警告会变得令人迷惑,所以一旦验证出某个特定警告是可预期,那么上策就是用@SuppressWarnings("unchecked")
关闭它。通过这种方式,当警告确实出现的之后,就可以真正对它展开调查了。
因为擦除的存在,数据的运行时类型就只能是Object[]
。如果我们立即转型为T[]
,那么在编译期该数组的实际类型将丢失,而编译器可能会错过某些潜在的错误检查。所以正因为是这样,最好是在集合内部使用Object[]
,然后当使用数组元素的时候,添加一个对T
的转型。以下是示例代码。
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int size) {
array = new Object[size];
}
public void put(int index, T item) {
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) array[index];
}
@SuppressWarnings("unchecked")
public T[] rep() {
return (T[]) array;
}
public static void main(String[] args) {
GenericArray2<Integer> gai = new GenericArray2<>(10);
for (int i = 0; i < 10; i++) {
gai.put(i, i);
}
for (int i = 0; i < 10; i++) {
System.out.print(gai.get(i) + " ");
}
System.out.println();
try {
Integer[] a = gai.rep();
} catch (Exception e) {
System.out.println(e);
}
}
}
// Outputs
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
上面的代码中,和之前代码不同的地方在于,转型的地方发生了变化,如果没有@SuppressWarnings("unchecked")
注解,仍然得到unchecked
警告。但是,现在的内部的表示是Object[]
,而不是T[]
。当get()
被调用后,它将对象转型为T
,这实际上是正确的类型,因此这是安全的。然而,当调用rep()
,它还是尝试着将Object[]
转化为T[]
,这仍然是不正确的,并在编译期发出警告,在运行时产生异常。
因此,没有任何方式可以推翻底层的数组类型,它只能是Object[]
,在内部将array
当做Object[]
而不是T[]
处理的优势是:编程中不太可能忘记这个数组的运行时类型,从而意外地引入缺陷(尽管其中大多数也可能是所有这类缺陷都可以在运行时快速地探测到)。
对于新代码,应该传递一个类型标记。在这种情况下,GenericArray
应该像下面这样的。
public class GenericArrayWithTypeToken<T> {
private T[] array;
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
Integer[] ia = gai.rep();
}
}
类型标记ClassT>
被传递到构造器中,以便从擦除中恢复,使得我们可以在创建需要恢复的实际类型的数组,尽管可以从转型中产生的警告必须用SuppressWarnings
压制住。一旦我们获得了实际类型,就可以返回它,并获得想要的结果,就像在main()
中看到的那样,该数组的运行时类型是T[]
。
关于Java容器库
如果查看Java容器库的代码,就会看到从Object
数组到参数化的类型的转型遍及各处。下面是经过整理和简化之后,从Colletion
中复制ArrayList
的构造器。``
public ArrayList(Collection c) {
size = c.size();
elementData = (E[])new Object[size];
c.toArray(elementData);
}
ArrayList
类中到处充满着这种转型,编译后,会发现其中会发出大量的警告。
这虽然是Java类库里面的一些惯用法,但是也不能表示这就是正确的解决之道,当查看类库代码时,你不能认为它就是在代码中应该遵循的示例。
网友评论