如何在Java中创建一个通用数组?

1234
由于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;
}

但是我真的不明白正在发生什么。

21
这里真的需要使用数组吗?使用集合类(Collection)如何? - matt b
16
我也认为使用集合更优雅解决这个问题。但由于这是一项课程作业,必须使用它们。 - tatsuhirosatou
5
我不明白为什么这里需要一个反射。Java语法很奇怪:例如new java.util.HashMap<String,String>[10]是无效的,new java.util.HashMap<long,long>(10)也是无效的,new long[][10]是无效的,但是new long[10][]是有效的。这些内容使编写一个能够编写Java程序的程序比看起来更加困难。 - bronze man
非常令人惊讶的是,一个如此重要的功能(Java 20)竟然还没有一个优雅的解决方案。 - undefined
32个回答

6
这个方案怎么样?
@SafeVarargs
public static <T> T[] toGenericArray(T ... elems) {
    return elems;
}

它的运作和外观看起来太简单了,有什么缺点吗?

4
整齐,但只有在手动调用时才起作用,即逐个传递元素。如果无法创建 T[] 的新实例,则无法编程地构建 T[] elems 并将其传递到函数中。如果可以的话,你也不需要这个函数。 - orlade

5
传递值列表...
(注:保留了html标签)
public <T> T[] array(T... values) {
    return values;
}

5

这个例子使用Java反射来创建一个数组。通常情况下,这种方式并不被推荐,因为它不够安全。相反,你应该使用内部的List,避免使用数组。


14
第二个例子(使用Array.newInstance())实际上是类型安全的。这是因为Class对象的类型T需要与数组的T匹配。它基本上强制你提供Java运行时在泛型中舍弃的信息。 - Joachim Sauer

5

同时请看以下代码:

public static <T> T[] toArray(final List<T> obj) {
    if (obj == null || obj.isEmpty()) {
        return null;
    }
    final T t = obj.get(0);
    final T[] res = (T[]) Array.newInstance(t.getClass(), obj.size());
    for (int i = 0; i < obj.size(); i++) {
        res[i] = obj.get(i);
    }
    return res;
}

它可以将任何类型的对象列表转换为相同类型的数组。

是的,你返回了null,这不是预期的空数组。虽然这是你能做的最好的选择,但并不理想。 - Kevin Cox
如果List中有多种类型的对象,例如toArray(Arrays.asList("abc", new Object())),也会导致失败并抛出ArrayStoreException异常。 - Radiodef
我使用了这个简化版本;它是我能够使用的第一个有效解决方案,尽管我没有尝试一些更复杂的解决方案。为了避免使用“for”循环和其他方法,我使用了“Arrays.fill(res, obj);”,因为我想要每个索引都有相同的值。 - bbarker

5

我找到了一种适合我的快速简单方法。请注意,我仅在Java JDK 8上使用过此方法。我不知道它是否适用于早期版本。

虽然我们无法实例化特定类型参数的通用数组,但是我们可以将已创建的数组传递给通用类构造函数。

class GenArray <T> {
    private T theArray[]; // reference array

    // ...

    GenArray(T[] arr) {
        theArray = arr;
    }

    // Do whatever with the array...
}

现在在主函数中,我们可以这样创建数组:
class GenArrayDemo {
    public static void main(String[] args) {
        int size = 10; // array size
        // Here we can instantiate the array of the type we want, say Character (no primitive types allowed in generics)
        Character[] ar = new Character[size];

        GenArray<Character> = new Character<>(ar); // create the generic Array

        // ...

    }
}

为了使数组更加灵活,您可以使用链表,例如Java.util.ArrayList类中的ArrayList和其他方法。


3
我写了这段代码片段来反射实例化一个传递进来的类,用于一个简单的自动化测试工具。
Object attributeValue = null;
try {
    if(clazz.isArray()){
        Class<?> arrayType = clazz.getComponentType();
        attributeValue = Array.newInstance(arrayType, 0);
    }
    else if(!clazz.isInterface()){
        attributeValue = BeanUtils.instantiateClass(clazz);
    }
} catch (Exception e) {
    logger.debug("Cannot instanciate \"{}\"", new Object[]{clazz});
}

请注意这一段内容:
    if(clazz.isArray()){
        Class<?> arrayType = clazz.getComponentType();
        attributeValue = Array.newInstance(arrayType, 0);
    }

对于数组初始化,可以使用Array.newInstance(数组的类, 数组的大小)。类可以是基本类型(int.class)和对象类型(Integer.class)。

BeanUtils是Spring框架的一部分。


2

实际上,更简单的方法是创建一个对象数组,并将其转换为所需类型,如以下示例:

T[] array = (T[])new Object[SIZE];

其中SIZE是一个常量,T是一个类型标识符。


出现 java.lang.ClassCastException: [Ljava.lang.Object; 无法转换为 [Ljava.util.concurrent.TransferQueue;。 - weberjn
@weberjn 一个通用数组字段 private T[] a 在类模板声明中被类型擦除为 T 声明为扩展的第一个类。例如 class GenSet<T extends TransferQueue> 将会声明私有字段 aTransferQueue[] 而不是 Object[],这就是为什么会出现 ClassCastException 的原因。 - gary

2

其他人建议的强制转换对我不起作用,会抛出非法类型转换的异常。

然而,这种隐式转换却可以正常工作:

Item<K>[] array = new Item[SIZE];

在这里,Item是我定义的一个类,其中包含成员:

private K value;

这样做可以得到一个类型为K的数组(如果该项只有值),或者在Item类中定义任何通用类型。

1
在Java中,禁止使用通用数组创建,但您可以通过以下方式实现。
class Stack<T> {
private final T[] array;
public Stack(int capacity) {
    array = (T[]) new Object[capacity];
 }
}

1
没有其他人回答你发布的示例中发生了什么的问题。
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;
}

正如其他人所说,泛型在编译期间会被“擦除”。因此,在运行时,泛型实例不知道其组件类型是什么。这样做的原因是历史上的,Sun 希望在不破坏现有接口(源代码和二进制)的情况下添加泛型。
另一方面,数组在运行时确实知道其组件类型。
该示例通过让调用构造函数的代码(它知道类型)传递一个参数来解决问题,告诉类所需的类型。
因此,应用程序将使用类似以下方式构造类:
Stack<foo> = new Stack<foo>(foo.class,50)

构造函数现在知道(在运行时)组件类型是什么,并且可以使用该信息通过反射API构造数组。
Array.newInstance(clazz, capacity);

最后,我们有一种类型转换,因为编译器无法知道由 Array#newInstance() 返回的数组是否是正确的类型(尽管我们知道)。
这种风格有点丑陋,但有时它可能是创建需要在运行时知道其组件类型的通用类型的最佳方案(创建数组或创建其组件类型的实例等)中最好的解决方案。

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