如何在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;}

但我真的不明白发生了什么。

955551 次浏览

这个例子是使用Java反射来创建一个数组。通常不建议这样做,因为它不是类型安全的。相反,你应该做的只是使用一个内部列表,根本避免数组。

您可以创建一个Object数组并将其强制转换为E。是的,这不是很干净的方法,但它至少应该可以工作。

这在有效Java,第2版的第5章(泛型)中有所介绍,第25项…首选列表而不是数组

您的代码将正常工作,尽管它会生成一个未经检查的警告(您可以使用以下注释抑制该警告):

@SuppressWarnings({"unchecked"})

但是,使用List而不是Array可能会更好。

关于这个bug有一个有趣的讨论 /featureOpenJDK项目站点

我必须反过来问一个问题:你的GenSet是“已检查”还是“未检查”?这是什么意思?

  • 检查强类型GenSet显式知道它包含什么类型的对象(即它的构造函数是用Class<E>参数显式调用的,当传递的参数不是E类型时,方法会抛出异常。参见#3

    ->在这种情况下,你应该写:

    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: weak typing. No type checking is actually done on any of the objects passed as argument.

    -> in that case, you should write

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

    注意数组的组件类型应该是type参数的删除

    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];}
    ...}

All of this results from a known, and deliberate, weakness of generics in Java: it was implemented using erasure, so "generic" classes don't know what type argument they were created with at run time, and therefore can not provide type-safety unless some explicit mechanism (type-checking) is implemented.

Java泛型通过在编译时检查类型并插入适当的转换来工作,但擦除编译文件中的类型。这使得泛型库可用于不理解泛型的代码(这是一个深思熟虑的设计决策),但这意味着您通常无法在运行时找出类型是什么。

公共Stack(Class<T> clazz,int capacity)构造函数要求你在运行时传递一个Class对象,这意味着类信息在运行时可用于需要它的代码。Class<T>形式意味着编译器将检查你传递的Class对象是否正是T类型的Class对象。不是T的子类,不是T的超类,而是T。

这意味着您可以在构造函数中创建适当类型的数组对象,这意味着您存储在集合中的对象的类型将在它们添加到集合时检查它们的类型。

你可以这样做:

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

这是在有效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充当表示类StringClass对象。这也适用于接口、枚举、任何维数组(例如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对象所表示的数组的组件类型(例如,Object1表示Object2,如果Class对象不表示数组,则为Object3)。

最后一句并不完全准确。调用String[].class.getComponentType()返回一个Class对象,表示类String,但它的类型是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));}

试试这个。

private int m = 0;private int n = 0;private Element<T>[][] elements = null;
public MatrixData(int m, int n){this.m = m;this.n = n;
this.elements = new Element[m][n];for (int i = 0; i < m; i++){for (int j = 0; j < n; j++){this.elements[i][j] = new Element<T>();}}}

虽然线程已死,但我想提请您注意这一点。

泛型在编译时用于类型检查。因此,目的是检查

  • 进来的就是你需要的。
  • 你得到的是消费者需要的。

检查这个:

输入图片描述

在编写泛型类时不要担心类型转换警告;在使用它时要担心。

这是唯一类型安全的答案

E[] a;
a = newArray(size);
@SafeVarargsstatic <E> E[] newArray(int length, E... array){return Arrays.copyOf(array, length);}

一个简单的,尽管混乱的解决方法是在主类中嵌套第二个“持有人”类,并使用它来保存您的数据。

public class Whatever<Thing>{private class Holder<OtherThing>{OtherThing thing;}public Holder<Thing>[] arrayOfHolders = new Holder<Thing>[10]}

我制作了这个代码片段来反射地实例化一个类,该类被传递给一个简单的自动化测试实用程序。

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.new实例(数组的类,数组的大小)。类可以是基元(int.class)和对象(Integer.class)。

BeanUtils是Spring的一部分。

再看看这段代码:

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

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

要扩展到更多维度,只需将[]和维度参数添加到newInstance()T是类型参数,clsClass<T>d1d5是整数):

T[] array = (T[])Array.newInstance(cls, d1);T[][] array = (T[][])Array.newInstance(cls, d1, d2);T[][][] array = (T[][][])Array.newInstance(cls, d1, d2, d3);T[][][][] array = (T[][][][])Array.newInstance(cls, d1, d2, d3, d4);T[][][][][] array = (T[][][][][])Array.newInstance(cls, d1, d2, d3, d4, d5);

详情见#0

也许与此问题无关,但当我使用“generic array creation”错误时

Tuple<Long,String>[] tupleArray = new Tuple<Long,String>[10];

我发现以下作品(并为我工作)与@SuppressWarnings({"unchecked"})

 Tuple<Long, String>[] tupleArray = new Tuple[10];

其他人建议的强制铸造对我不起作用,抛出非法铸造的例外。

但是,这个隐式转换工作得很好:

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

其中Item是我定义的包含成员的类:

private K value;

通过这种方式,您可以获得一个类型为K的数组(如果项目只有值)或您希望在Item类中定义的任何泛型类型。

在Java8中,我们可以使用lambda或方法引用创建一种泛型数组。这类似于反射方法(传递Class),但这里我们不使用反射。

@FunctionalInterfaceinterface ArraySupplier<E> {E[] get(int length);}
class GenericSet<E> {private final ArraySupplier<E> supplier;private E[] array;
GenericSet(ArraySupplier<E> supplier) {this.supplier = supplier;this.array    = supplier.get(10);}
public static void main(String[] args) {GenericSet<String> ofString =new GenericSet<>(String[]::new);GenericSet<Double> ofDouble =new GenericSet<>(Double[]::new);}}

例如,这被#0使用。

这个可以也可以在8Java之前使用匿名类完成,但它更麻烦。

我想知道这段代码是否会创建一个有效的泛型数组?

public T [] createArray(int desiredSize){ArrayList<T> builder = new ArrayList<T>();for(int x=0;x<desiredSize;x++){builder.add(null);}return builder.toArray(zeroArray());}
//zeroArray should, in theory, create a zero-sized array of T//when it is not given any parameters.
private T [] zeroArray(T... i){return i;}

编辑:如果您需要的大小已知且较小,也许创建此类数组的另一种方法是简单地将所需数量的“null”输入zeroArray命令?

尽管显然这不像使用createArray代码那样通用。

你可以使用Cast:

public class GenSet<Item> {private Item[] a;
public GenSet(int s) {a = (Item[]) new Object[s];}}
private E a[];private int size;
public GenSet(int elem){size = elem;a = (E[]) new E[size];}

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

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

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

没有人回答你发布的示例中发生了什么的问题。

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()返回的数组是正确的类型(即使我们知道)。

这种风格有点难看,但它有时可能是创建泛型类型的最不坏的解决方案,这些泛型类型确实需要在运行时知道其组件类型,无论出于何种原因(创建数组或创建其组件类型的实例等)。

这个解决方案呢?

@SafeVarargspublic static <T> T[] toGenericArray(T ... elems) {return elems;}

它很有效,看起来太简单了,不可能是真的。有什么缺点吗?

我找到了一种解决这个问题的方法。

下面的行抛出通用数组创建错误

List<Person>[] personLists=new ArrayList<Person>()[10];

但是,如果我将List<Person>封装在一个单独的类中,它就可以工作。

import java.util.ArrayList;import java.util.List;

public class PersonList {
List<Person> people;
public PersonList(){people=new ArrayList<Person>();}}

你可以通过一个getter暴露类中的人。下面的行将给你一个数组,每个元素都有一个List<Person>。换句话说,List<Person>的数组。

PersonList[] personLists=new PersonList[10];

我在一些代码中需要这样的东西,这就是我所做的让它工作。到目前为止没有问题。

我找到了一种适合我的快速简便的方法。请注意,我只在JavaJDK 8上使用过这个。我不知道它是否适用于以前的版本。

尽管我们不能实例化特定类型参数的泛型数组,但我们可以将已经创建的数组传递给泛型类构造函数。

class GenArray <T> {private T theArray[]; // reference array
// ...
GenArray(T[] arr) {theArray = arr;}
// Do whatever with the array...}

现在在main中,我们可以像这样创建数组:

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
// ...
}}

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

我实际上找到了一个非常独特的解决方案来绕过无法启动泛型数组。你必须做的是创建一个接受泛型变量T的类,如下所示:

class GenericInvoker <T> {T variable;public GenericInvoker(T variable){this.variable = variable;}}

然后在你的数组类中,让它像这样开始:

GenericInvoker<T>[] array;public MyArray(){array = new GenericInvoker[];}

启动new Generic Invoker[]将导致未经检查的问题,但实际上不应该有任何问题。

要从数组中获取,您应该像这样调用数组[i].变量:

public T get(int index){return array[index].variable;}

其余的,例如调整数组的大小可以使用Arrays.copyOf()完成,如下所示:

public void resize(int newSize){array = Arrays.copyOf(array, newSize);}

添加函数可以像这样添加:

public boolean add(T element){// the variable size below is equal to how many times the add function has been called// and is used to keep track of where to put the next variable in the arrayarrays[size] = new GenericInvoker(element);size++;}

您不需要将Class参数传递给构造函数。试试看

public class GenSet<T> {
private final T[] array;
@SafeVarargspublic GenSet(int capacity, T... dummy) {if (dummy.length > 0)throw new IllegalArgumentException("Do not provide values for dummy argument.");this.array = Arrays.copyOf(dummy, capacity);}
@Overridepublic String toString() {return "GenSet of " + array.getClass().getComponentType().getName()+ "[" + array.length + "]";}}

GenSet<Integer> intSet = new GenSet<>(3);System.out.println(intSet);System.out.println(new GenSet<String>(2));

结果:

GenSet of java.lang.Integer[3]GenSet of java.lang.String[2]

传递一个值列表…

public <T> T[] array(T... values) {return values;}

在java中不允许创建泛型数组,但您可以像这样做

class Stack<T> {private final T[] array;public Stack(int capacity) {array = (T[]) new Object[capacity];}}

如果你真的想包装一个固定大小的通用数组,你将有一个方法来向该数组添加数据,因此你可以在那里正确初始化数组,如下所示:

import java.lang.reflect.Array;
class Stack<T> {private T[] array = null;private final int capacity = 10; // fixed or pass it in the constructorprivate int pos = 0;
public void push(T value) {if (value == null)throw new IllegalArgumentException("Stack does not accept nulls");if (array == null)array = (T[]) Array.newInstance(value.getClass(), capacity);// put logic: e.g.if(pos == capacity)throw new IllegalStateException("push on full stack");array[pos++] = value;}
public T pop() throws IllegalStateException {if (pos == 0)throw new IllegalStateException("pop on empty stack");return array[--pos];}}

在这种情况下,您使用java.lang.reflect.Array.new实例来创建数组,它将不是一个Object[],而是一个真正的T[]。你不应该担心它不是最终的,因为它是在你的类内部管理的。请注意,您需要在ush()上有一个非空对象才能获取要使用的类型,因此我添加了对您推送的数据的检查并在那里抛出异常。

这仍然有些毫无意义:你通过推送存储数据,并且它是方法的签名,保证只有T元素会进入。因此数组是Object[]还是T[]或多或少无关紧要。

根据vnportnoy的语法

GenSet<Integer> intSet[] = new GenSet[3];

创建一个空引用数组,填充为

for (int i = 0; i < 3; i++){intSet[i] = new GenSet<Integer>();}

这是类型安全的。