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

时间:2009-02-09 17:30:44

标签: java arrays generics reflection instantiation

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

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

31 个答案:

答案 0 :(得分:647)

我必须回答一个问题:您的GenSet“已选中”或“未选中”? 这是什么意思?

  • 已选中强类型GenSet明确知道它包含的对象类型(即它的构造函数是使用Class<E>参数显式调用的,并且当传递的参数不是E类型时,方法将抛出异常见Collections.checkedCollection

    - &GT;在这种情况下,你应该写:

    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];
        }
    }
    
  • 未选中弱输入。实际上没有对作为参数传递的任何对象进行类型检查。

    - &GT;在这种情况下,你应该写

    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参数的erasure

    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中泛型的已知且有意识的弱点:它是使用擦除实现的,因此“泛型”类不知道它们在运行时创建的类型参数,因此不能除非实现一些显式机制(类型检查),否则提供类型安全性。

答案 1 :(得分:184)

你可以这样做:

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

这是在 Effective Java中实现泛型集合的建议方法之一;第26项。没有类型错误,不需要重复强制转换数组。 然而会触发警告,因为它有潜在危险,应谨慎使用。正如评论中所详述的,此Object[]现在伪装成我们的E[]类型,如果使用不安全,可能会导致意外错误或ClassCastException

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


值得一提的是,只要有可能,如果您使用泛型,那么使用List而不是数组会更快乐。当然有时候你没有选择权,但使用集合框架会更加健壮。

答案 2 :(得分:60)

以下是如何使用泛型来获取正在寻找的类型的数组,同时保留类型安全性(与其他答案相反,这会给你一个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 Tutorials中所述。编译器将类文字视为java.lang.Class的实例。要使用一个,只需使用.class的类名称。因此,String.class充当代表类Class的{​​{1}}对象。这也适用于接口,枚举,任何维数组(例如String),基元(例如String[].class)和关键字int.class(即void)。

void.class本身是通用的(声明为Class,其中Class<T>代表T对象所代表的类型),这意味着{{1}的类型是} Class

因此,无论何时调用String.class的构造函数,都会为第一个参数传递一个类文字,表示Class<String>实例的声明类型的数组(例如GenSet for {{ 1}})。请注意,您将无法获取基元数组,因为基元不能用于类型变量。

在构造函数中,调用方法GenSet会将传递的String[].class参数强制转换为由调用该方法的GenSet<String>对象表示的类。在cast中调用静态方法Object将返回Class作为第一个参数传递的newInstance对象所表示的类型的数组,并返回java.lang.reflect.Array指定的长度{1}}作为第二个参数传递。调用方法Object将返回一个Class对象,该对象表示调用该方法的int对象所表示的数组的组件类型(例如getComponentTypeClass },Class如果String.class对象不表示数组。)

最后一句话并不完全准确。调用String[].class会返回表示类null的{​​{1}}对象,但其类型为Class,而不是String[].class.getComponentType(),这就是为什么你不能做类似的事情以下。

Class

同样适用于String中返回Class<?>个对象的每个方法。

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


关于Ingo的评论编辑:

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

答案 3 :(得分:38)

这是唯一安全类型的答案

E[] a;

a = newArray(size);

@SafeVarargs
static <E> E[] newArray(int length, E... array)
{
    return Arrays.copyOf(array, length);
}

答案 4 :(得分:29)

要扩展到更多维度,只需将[]和尺寸参数添加到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);

有关详细信息,请参阅Array.newInstance()

答案 5 :(得分:12)

在Java 8中,我们可以使用lambda或方法引用来创建一种通用数组。这类似于反射方法(通过Class),但这里我们没有使用反射。

@FunctionalInterface
interface 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);
    }
}

例如,<A> A[] Stream.toArray(IntFunction<A[]>)使用它。

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

答案 6 :(得分:10)

Effective Java, 2nd Edition第5章(泛型),第25项...... 首选列表

您的代码可以使用,但它会生成未经检查的警告(您可以使用以下注释来抑制该代码:

@SuppressWarnings({"unchecked"})

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

the OpenJDK project site对此错误/功能进行了有趣的讨论。

答案 7 :(得分:7)

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

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

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

答案 8 :(得分:6)

嗨虽然线程已经死了,但我想提请你注意这个:

泛型用于在编译期间进行类型检查:

  • 因此,目的是检查所需要的是什么。
  • 您的回报是消费者的需求。
  • 检查:

enter image description here

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

答案 9 :(得分:5)

这个解决方案怎么样?

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

它的工作原理看起来太简单了。有什么缺点吗?

答案 10 :(得分: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;
}

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

答案 11 :(得分:5)

我找到了一种适合我的快捷方式。请注意,我只在Java JDK 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类中的其他方法。

答案 12 :(得分:4)

该示例使用Java反射来创建数组。通常不建议这样做,因为它不是类型安全的。相反,你应该做的只是使用内部List,并完全避免使用数组。

答案 13 :(得分:4)

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

public class GenSet<T> {
    private final T[] array;
    @SuppressWarnings("unchecked")
    public GenSet(int capacity, T... dummy) {
        if (dummy.length > 0)
            throw new IllegalArgumentException(
              "Do not provide values for dummy argument.");
        Class<?> c = dummy.getClass().getComponentType();
        array = (T[])Array.newInstance(c, capacity);
    }
    @Override
    public 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]

答案 14 :(得分: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(数组类,数组大小)。 Class可以是primitive(int.class)和object(Integer.class)。

BeanUtils是Spring的一部分。

答案 15 :(得分:3)

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

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

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

答案 16 :(得分:3)

传递值列表......

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

答案 17 :(得分:1)

其他人建议的强迫演员对我不起作用,抛出非法演员的例外。

然而,这种隐式演员工作得很好:

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

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

private K value;

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

答案 18 :(得分:1)

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

以下行会抛出通用数组创建错误

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公开PersonList类中的人员。下面的代码将为您提供一个数组,每个元素都有一个List<Person>。换句话说,List<Person>

的数组
PersonList[] personLists=new PersonList[10];

在我正在处理的一些代码中,我需要这样的东西,这就是我为了让它工作而做的事情。到目前为止没有任何问题。

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

这种风格有点难看,但它有时可能是创建通用类型的最不好的解决方案,这些类型需要在运行时因任何原因知道它们的组件类型(创建数组或创建其组件类型的实例等)

答案 20 :(得分:0)

我实际上找到了一个非常独特的解决方案来绕过无法启动通用数组。你要做的是创建一个类,它接受泛型变量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] .variable:

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 array
    arrays[size] = new GenericInvoker(element);
    size++;
}

答案 21 :(得分:0)

根据vnportnoy语法

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

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

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

这是安全类型。

答案 22 :(得分:0)

你可以使用演员:

public class GenSet<Item> {
    private Item[] a;

    public GenSet(int s) {
        a = (Item[]) new Object[s];
    }
}

答案 23 :(得分:0)

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

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代码那样通用。

答案 24 :(得分:0)

可能与此问题无关但我在使用

时收到“generic array creation”错误
Tuple<Long,String>[] tupleArray = new Tuple<Long,String>[10];

我用@SuppressWarnings({"unchecked"})找到了以下作品(并为我工作):

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

答案 25 :(得分:0)

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

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

答案 26 :(得分:0)

试试这个。

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

答案 27 :(得分:0)

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

答案 28 :(得分:-1)

private E a[];
private int size;

public GenSet(int elem)
{
    size = elem;
    a = (E[]) new E[size];
}

答案 29 :(得分:-1)

java中不允许创建通用数组,但您可以像

那样进行创建
class Stack<T> {
private final T[] array;
public Stack(int capacity) {
    array = (T[]) new Object[capacity];
 }
}

答案 30 :(得分:-1)

如果您真的想包装一个固定大小的通用数组,则可以使用一种方法将数据添加到该数组中,因此您可以在此处正确地初始化该数组,如下所示:

import java.lang.reflect.Array;

class Stack<T> {
    private T[] array = null;
    private final int capacity = 10; // fixed or pass it in the constructor
    private 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.newInstance创建数组,该数组将不是Object [],而是真正的T []。 您不必担心它不是最终的,因为它是在班级内部管理的。 请注意,您需要在push()上使用一个非null对象才能获取要使用的类型,因此我对您所推送的数据进行了检查,并在其中抛出异常。

这仍然没有意义:您通过push存储数据,这是方法的签名,可确保仅T元素会进入。因此,数组是Object []还是T []几乎无关紧要。