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

问题

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

但我真的不知道发生了什么事。


#1 热门回答(571 赞)

我必须回答一个问题:你的GenSet“已经检查”或“未选中”?那是什么意思?

  • Checked 强引用,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];
    }
}
  • Unchecked 弱引用。- >在这种情况下,你应该这样实现
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中已知且有意识的泛型弱点:它是使用擦除实现的,因此“泛型”类不知道它们在运行时创建的类型参数,因此无法提供类型 - 除非实施一些明确的机制(类型检查)。


#2 热门回答(158 赞)

你总是可以这样做:

E[] arr = (E[])new Object[INITIAL_ARRAY_LENGTH];

这是在有效的Java中实现泛型集合的建议方法之一;项目26.没有类型错误,不需要重复投射数组。但是,由于它有潜在危险,因此会触发警告,应谨慎使用。正如注释中详细说明的那样,这个Object []现在伪装成我们的E []类型,并且可能会导致意外的错误或ClassCastException`s(如果使用不安全)。

根据经验,只要在内部使用强制转换数组(例如,支持数据结构),并且不返回或暴露给客户端代码,此行为就是安全的。如果您需要将一个泛型类型的数组返回给其他代码,那么您提到的反射Array类是正确的路要走。

值得一提的是,如果你使用泛型,你可以在使用List而不是数组时更快乐。当然有时你没有选择,但使用集合框架更加健壮。


#3 热门回答(53 赞)

下面是如何在保留类型安全性的同时使用泛型来精确地获取所需类型的数组(或与其他答案相反,这会在编译时返回一个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转换为该类型的变量,这意味着该数组和数组中的值是正确的类型。

它通过使用类文字作为运行时类型标记来工作,如在425826011中所讨论的。编译器将类文字视为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实例的声明类型的数组(例如,GenSet <String>String [] .class) 。请注意,由于基元不能用于类型变量,因此您将无法获取原始数组。

在构造函数内部,调用方法cast将传递的Object参数强制转换为由调用该方法的Class对象表示的类。在java.lang.reflect.Array中调用静态方法newInstance将返回一个'Object作为第一个参数传递的Class对象表示的类型的数组和int指定的长度。作为第二个参数传递。调用方法getComponentType返回一个Class对象,表示由调用该方法的Class对象表示的数组的组件类型(例如String.class表示String [] .classnull 如果Class`对象不代表数组)。

最后一句话并不完全准确。调用String [] .class.getComponentType()返回表示类String的Class对象,但它的类型是Class <?>而不是Class <String>,这就是为什么你可以' t做类似下面的事情。

String foo = String[].class.getComponentType().cast("bar"); // won't compile

Class中的每个方法都会返回一个Class对象。

关于Joachim Sauer关于this answer的评论(我自己没有足够的声誉对其进行评论),使用强制转换为“T []”的示例将导致警告,因为在这种情况下编译器无法保证类型安全。

编辑关于Ingo的评论:

public static <T> T[] newArray(Class<T[]> type, int size) {
   return type.cast(Array.newInstance(type.getComponentType(), size));
}