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

tat*_*tou 1045 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
    }
}
Run Code Online (Sandbox Code Playgroud)

如何在保持类型安全的同时实现这一点?

我在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;
}
Run Code Online (Sandbox Code Playgroud)

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

Var*_*han 676

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

  • 检查:强打字.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];
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 未选中:弱打字.实际上没有对作为参数传递的任何对象进行类型检查.

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

    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;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    请注意,数组的组件类型应该是类型参数的擦除:

    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];
        }
    
        ...
    }
    
    Run Code Online (Sandbox Code Playgroud)

所有这些都源于Java中泛型的已知且有意识的弱点:它是使用擦除实现的,因此"泛型"类不知道它们在运行时创建的类型参数,因此无法提供类型 - 安全,除非实施一些显式机制(类型检查).

  • 性能方面最好的选择是什么?我需要经常从这个数组中获取元素(在循环中).所以一个集合可能更慢,但这两个中哪一个最快? (6认同)
  • @AaronDigulla只是为了澄清那不是赋值,而是初始化局部变量.您无法注释表达式/语句. (5认同)
  • 如果泛型类型有界,则后备数组应该是边界类型. (3认同)
  • 对于那些想要使用泛型类型(我正在寻找的)方法的人,请使用:`public void <T> T [] newArray(Class <T> type,int length){...} ` (2认同)
  • @gozzilli 遗憾的是不是,因为 Java 泛型基本上是假的。如果没有类对象,你就无法做任何事情。 (2认同)

dim*_*414 201

你可以这样做:

E[] arr = (E[])new Object[INITIAL_ARRAY_LENGTH];
Run Code Online (Sandbox Code Playgroud)

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

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


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

  • 如果将数组视为任何类型的类型数组,例如上面`test()`方法中的`String [] s = b;`,这将不起作用.那是因为E的数组不是真的,它是Object [].这很重要,例如`List <String> []` - 你不能使用`Object []`,你必须具有一个`List []`.这就是您需要使用反射的Class <?>数组创建的原因. (42认同)
  • 基本上,只要您不返回数组或传递数组或将其存储在需要某种类型数组的类之外的某些位置,此方法就会起作用.只要你在课堂上就可以了,因为E被删除了.它是"危险的",因为如果你试图返回它或其他东西,你不会得到它不安全的警告.但是,如果你小心,那么它的工作原理. (17认同)
  • 角落/问题是你想做什么,例如,`public E [] toArray(){return(E [])internalArray.clone(); 当`internalArray`被输入为`E []`时,它实际上是一个`Object []`.这在运行时因类型转换异常而失败,因为无法将`Object []`分配给任何类型`E`的数组. (7认同)
  • 至少在Java 1.6中,这会生成一个警告:"Unchecked cast from Object [] to T []" (5认同)
  • 这很安全.在`E [] b =(E [])new Object [1];`你可以清楚地看到对创建数组的唯一引用是`b`,而`b`的类型是`E []`.因此,您不会意外地通过不同类型的不同变量访问同一阵列.相反,你有`Object [] a = new Object [1]; E [] b =(E [])a; 那么你需要对你如何使用`a`感到偏执. (3认同)
  • @ tgm1024如果您不想使用它,我没关系.你不应该首先混合泛型和数组,所以我认为问题不在于这个解决方案.它与使用原始类型一样安全,当然不推荐使用,但对于不暴露阵列的代码,通常不会有效.正如我所说,这并不理想,但很容易.有时,这种权衡是可以的. (2认同)

gde*_*ohn 61

下面是如何使用泛型来获得一个正在寻找的类型的数组,同时保留类型安全性(与其他答案相反,后者将返回一个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];  
    }  
}
Run Code Online (Sandbox Code Playgroud)

在没有警告的情况下进行编译,正如您所看到的main,对于您声明GenSetas 的实例的任何类型,您可以分配a给该类型的数组,并且可以将元素分配给a该类型的变量,这意味着该数组并且数组中的值具有正确的类型.

它的工作原理是使用类文字作为运行时类型标记,如Java教程中所述.编译器将类文字视为实例java.lang.Class.要使用一个,只需按照类的名称.class.因此,String.class充当Class表示类的对象String.这也适用于接口,枚举,任何维数组(例如String[].class),基元(例如int.class)和关键字void(即void.class).

Class本身是通用的(声明为Class<T>,T代表Class对象所代表的类型),意味着类型String.classClass<String>.

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

在构造函数内部,调用该方法cast将传递的Object参数强制转换为由调用该方法的Class对象表示的类.调用静态方法newInstancejava.lang.reflect.Array返回作为Object由所表示的类型的数组Class作为第一个参数,并通过指定的长度的传递的对象int通过作为第二个参数.调用该方法getComponentType返回一个Class表示组件类型由所表示的所述阵列的物体Class在其上的方法被调用(例如对象String.classString[].class,null如果Class对象不表示阵列).

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

String foo = String[].class.getComponentType().cast("bar"); // won't compile
Run Code Online (Sandbox Code Playgroud)

Class返回Class对象的每个方法都是如此.

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


关于Ingo的评论编辑:

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

  • 这没用,只是编写新String [...]的复杂方法.但真正需要的是像public static <T> T [] newArray(int size){...}这样的东西,这在java noir中根本就不存在它可以用反射来模拟 - 原因是关于如何实例化的泛型类型在运行时不可用. (5认同)
  • @Ingo你在说什么?我的代码可用于创建任何类型的数组. (4认同)
  • @Charlatan:当然,新的[]也是如此.问题是:谁知道类型和时间.因此,如果您拥有的只是通用类型,则不能. (3认同)
  • 我毫不怀疑。关键是,在运行时不会获得泛型类型X的Class对象。 (2认同)
  • 几乎。我承认这比使用 new[] 可以实现的要多。在实践中,这几乎总是可以完成工作。然而,仍然不可能,例如,编写一个用 E 参数化的容器类,它有一个方法 E[] toArray() 并且确实返回一个真正的 E[] 数组。只有当集合中至少有一个 E 对象时才能应用您的代码。因此,通用的解决方案是不可能的。 (2认同)

irr*_*ble 39

这是唯一类型安全的答案

E[] a;

a = newArray(size);

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

  • 如果`E`是一个类型变量,这不起作用.当`E`是一个类型变量时,varargs会创建一个删除`E`的数组,这使它与`(E [])new Object [n]`没有多大区别.请参阅[http://ideone.com/T8xF91](http://ideone.com/T8xF91).它绝不是**比任何其他答案更安全. (7认同)
  • @Radiodef - 有一些不同之处.编译器检查此解决方案的正确性; 它不依赖于强制演员的人为推理.对于这个特定问题,差异并不显着.有些人只是喜欢有点花哨,这就是全部.如果有人被OP的措辞误导,那么你的评论和我的评论就会澄清. (2认同)

Jas*_*n C 29

要扩展到更多维度,只需添加[]'s和维度参数newInstance()(T是一个类型参数,cls是一个Class<T>,d1通过d5是整数):

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);
Run Code Online (Sandbox Code Playgroud)

详情Array.newInstance()请见.

  • +1有关多维数组创建的问题已经被关闭,因为这篇帖子已经被封闭了 - 但没有答案专门解决这个问题. (4认同)

Rad*_*def 13

在Java 8中,我们可以使用lambda或方法引用来创建一种通用数组.这类似于反射方法(通过a 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

可能也可以做预先的Java 8中使用匿名类,但它更繁琐.

  • @Lii和我的例子一样,它是`IntFunction <E []>`,但是是的,这是真的. (3认同)

Jef*_*son 11

这将在Effective Java的第5章(泛型),第2版, 25项中介绍... 首选列表到数组

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

@SuppressWarnings({"unchecked"})
Run Code Online (Sandbox Code Playgroud)

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

在OpenJDK项目网站上有一个关于这个bug /功能的有趣讨论.


Bil*_*ell 7

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

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

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


小智 6

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

  • 第二个示例(使用 Array.newInstance())实际上是类型安全的。这是可能的,因为 Class 对象的类型 T 需要与数组的 T 匹配。它基本上强制您提供 Java 运行时为泛型丢弃的信息。 (14认同)

小智 6

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

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

  • 因此,目的是检查您所需要的是什么.
  • 您返回的是消费者需要的东西.
  • 检查一下:

在此输入图像描述

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


Ben*_*n M 6

这个解决方案怎么样?

@SafeVarargs
public static <T> T[] toGenericArray(T ... elems) {
    return elems;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 整洁,但只有在你手动调用它时才有效,即单独传递元素.如果你不能创建`T []`的新实例,那么你不能以编程方式建立一个`T [] elems`来传递给函数.如果可以的话,你不需要这个功能. (2认同)

sak*_*029 6

您无需将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 + "]";
    }
}
Run Code Online (Sandbox Code Playgroud)

GenSet<Integer> intSet = new GenSet<>(3);
System.out.println(intSet);
System.out.println(new GenSet<String>(2));
Run Code Online (Sandbox Code Playgroud)

结果:

GenSet of java.lang.Integer[3]
GenSet of java.lang.String[2]
Run Code Online (Sandbox Code Playgroud)


Mat*_*imB 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;
}
Run Code Online (Sandbox Code Playgroud)

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


Nik*_*kos 5

我找到了一种适合我的快捷方式.请注意,我只在Java JDK 8上使用过它.我不知道它是否适用于以前的版本.

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

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

    // ...

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

    // Do whatever with the array...
}
Run Code Online (Sandbox Code Playgroud)

现在在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

        // ...

    }
}
Run Code Online (Sandbox Code Playgroud)

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


Rod*_*sio 5

传递值列表...

public <T> T[] array(T... values) {
    return values;
}
Run Code Online (Sandbox Code Playgroud)