如何创建一个通用数组?

89

我不理解泛型和数组之间的联系。

我可以使用泛型类型创建数组引用:

private E[] elements; //GOOD

但无法使用泛型类型创建数组对象:

elements = new E[10]; //ERROR

但它有效:

elements = (E[]) new Object[10]; //GOOD

只使用 new ArrayList<E>() 吗? - micha
4个回答

211

您不应混淆数组和泛型。它们不能很好地搭配使用。数组和泛型类型强制类型检查的方式有所不同。我们说数组是实例化的,但泛型不是。因此,您会看到在处理数组和泛型时存在这些差异。

数组是协变的,而泛型不是:

什么意思?您现在必须知道以下赋值是有效的:

Object[] arr = new String[10];

基本上讲,Object[]String[] 的超类型,因为 ObjectString 的超类型。但泛型并非如此。因此,以下声明无效并且不会编译:

List<Object> list = new ArrayList<String>(); // Will not compile.

原因是泛型是不变的。

强制类型检查:

Java中引入了泛型来在编译时强制进行更强的类型检查。因此,由于类型擦除,泛型类型在运行时没有任何类型信息。因此,List<String> 的静态类型为 List<String> ,但动态类型为 List

然而,数组携带组件类型的运行时类型信息。在运行时,数组使用数组存储检查来检查您是否插入与实际数组类型兼容的元素。所以,以下代码:

Object[] arr = new String[10];
arr[0] = new Integer(10);

即使编译时不会出现问题,但由于ArrayStoreCheck的结果,在运行时将失败。对于泛型而言,这是不可能的,因为编译器会尝试通过提供编译时检查来避免运行时异常,方法是避免创建像上面所示的引用。

那么,泛型数组的创建有什么问题?

创建组件类型为类型参数具体参数化类型有界通配符参数化类型的数组是类型不安全的。

考虑下面的代码:

public <T> T[] getArray(int size) {
    T[] arr = new T[size];  // Suppose this was allowed for the time being.
    return arr;
}

由于在运行时不知道 T 的类型,所以创建的数组实际上是一个 Object[]。 因此,在运行时,上述方法将如下所示:

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

现在,假设您调用此方法如下:
Integer[] arr = getArray(10);

问题在于,你刚刚将一个Object[]赋值给了Integer[]的引用。上述代码可以编译通过,但会在运行时失败。

这就是为什么禁止泛型数组创建的原因。

为什么将new Object [10]强制转换为E []可行?

现在解决你最后的疑问,为什么下面的代码可行:

E[] elements = (E[]) new Object[10];

上面的代码与上面解释的内容相同。如果您注意到了,编译器会给您一个“未经检查的转换警告”,因为您正在将其强制转换为未知组件类型的数组。这意味着强制转换可能在运行时失败。例如,如果您将该代码放置在上述方法中:
public <T> T[] getArray(int size) {
    T[] arr = (T[])new Object[size];        
    return arr;
}

你可以这样调用它:

String[] arr = getArray(10);

这种方法在运行时会出现ClassCastException,因此并不总是有效。

那么创建一个List<String>[]类型的数组呢?

问题是一样的。由于类型擦除,List<String>[]本质上就是一个List[]。因此,如果允许创建这样的数组,可能会发生以下情况:

List<String>[] strlistarr = new List<String>[10];  // Won't compile. but just consider it
Object[] objarr = strlistarr;    // this will be fine
objarr[0] = new ArrayList<Integer>(); // This should fail but succeeds.

在上面的情况下,ArrayStoreCheck在运行时将成功,尽管应该抛出ArrayStoreException。这是因为List<String>[]List<Integer>[]都在运行时编译为List[]
那么我们可以创建无界通配符参数化类型的数组吗?
是的。原因是List<?>是可重用类型。这很有道理,因为根本没有关联的类型。因此,在类型擦除的结果中没有任何损失。因此,创建此类类型的数组是完全类型安全的。
List<?>[] listArr = new List<?>[10];
listArr[0] = new ArrayList<String>();  // Fine.
listArr[1] = new ArrayList<Integer>(); // Fine

以上两种情况都是可以的,因为List<?>是所有泛型类型实例化的超类型。所以,在运行时它不会发出ArrayStoreException。原始类型数组也是同样的情况。由于原始类型也是可实例化的类型,你可以创建一个List[]数组。

因此,你只能创建可实例化类型的数组,而不能创建不可实例化类型的数组。请注意,在上述所有情况中,声明数组是正确的,问题在于使用new操作符创建数组。但是,声明那些引用类型的数组没有任何意义,因为它们除了指向null之外什么都指不上(忽略无界类型)。

E[]有没有任何解决方法?

有,你可以使用Array#newInstance()方法创建数组:

public <E> E[] getArray(Class<E> clazz, int size) {
    @SuppressWarnings("unchecked")
    E[] arr = (E[]) Array.newInstance(clazz, size);

    return arr;
}

需要进行类型转换,因为该方法返回一个 Object 对象。但你可以确定这是一次安全的转换。因此,你甚至可以在该变量上使用 @SuppressWarnings。


吹毛求疵:“创建一个组件类型为通配符参数化类型的数组是不安全的。” 实际上,例如实例化 new List<?>[] { } 是有效的 - 只是通配符不能被限定。 - Paul Bellora
此外,“这将在运行时导致ClassCastException失败。”并不完全正确。 当转换未经检查时,这意味着它不会快速失败。 相反,在擦除过程中编译器插入转换的其他地方可能会抛出ClassCastExceptions。 此问题是一个很好的例子。 - Paul Bellora
@PaulBellora。实际上,我是指有界的。漏掉了这个词。谢谢我会编辑 :) - Rohit Jain
@PaulBellora。至于转换部分,我写的是将其转换为String[],这肯定会失败。我已经编辑了那一部分,使其更加清晰明了。 - Rohit Jain
1
由于在运行时无法确定T的类型,因此创建的数组实际上是一个Object[]。为什么在运行时无法确定它的类型?我错了吗,假设即使在编译时也已知其类型? - Klaus
显示剩余6条评论

3

这是 LinkedList<T>#toArray(T[]) 的实现:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
                            a.getClass().getComponentType(), size);
    int i = 0;
    Object[] result = a;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;

    if (a.length > size)
        a[size] = null;

    return a;
}

简而言之,你只能通过使用Array.newInstance(Class, int)来创建通用数组,其中int是数组的大小。

3

问题在于,运行时擦除了泛型类型,所以new E[10]将等同于new Object[10].

这样做是危险的,因为可能会在数组中放入其他类型而不是E类型的数据。这就是为什么你需要通过以下方式明确指定你想要的类型:


但是 (E[]) 不会通过类型擦除转换为 (O[]) 吗? - user2693979
它会。泛型是编译器工具而不是运行时工具。我不太明白你试图展示什么问题... - Pshemo
@user2693979 如果你认为Rohit的答案更好,那就接受它。没有压力,只是因为我稍早发布了我的回答。 - Pshemo
但是如果E[]将是Object[],(E[])将是(Object[]),那么为什么(e = new E[10])和(e = (E[]) Object[10])之间会有区别呢? 难道它们都不是e = new Object[10]吗? - user2693979
@user2693979 我怀疑 new E[size] 不被允许是为了防止认为我们实际上正在创建 E 类型的数组而不是 Object 类型的数组。我在答案中提到的方法清楚地显示了正在发生什么,而 new E[size] 可能会被错误解释。但再次强调,这只是我的猜测。 - Pshemo
简单来说,编译器会阻止@user2693979使用new E[size],因为它不会创建E[]数组(仅包含E类型的数组),而是Object[]数组。 - Pshemo

1

选中:

public Constructor(Class<E> c, int length) {

    elements = (E[]) Array.newInstance(c, length);
}

或未选中:

public Constructor(int s) {
    elements = new Object[s];
}

很抱歉,我忘了解释他应该像这样更改元素类型:private Object[] elements; 而不是使用泛型类型。 - Melih Altıntaş

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接