如何在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个回答

778
我也需要问一个问题:你的GenSet是“checked”还是“unchecked”?那是什么意思呢?
  • 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中泛型的已知和故意的缺陷:它使用擦除实现,因此“泛型”类在运行时不知道它们创建时使用的类型参数,因此除非实现了某些显式机制(类型检查),否则无法提供类型安全性。

所有这一切都源于Java中泛型的已知且故意的缺点:它是通过类型擦除来实现的,因此“泛型”类在运行时不知道它们被创建时使用的类型参数,因此除非实现了某些显式机制(类型检查),否则无法提供类型安全性。


8
从性能角度来看,哪种选项最好?我需要经常从这个数组中获取元素(在循环内)。因此集合可能会更慢,但这两种选项哪一种最快? - user1111929
4
如果泛型类型被限定,那么后备数组应该是受限定类型的。 - Mordechai
5
只是澄清一下,这不是赋值语句,而是本地变量的初始化。你不能注释一个表达式/语句。 - kennytm
2
对于那些想要使用通用类型创建方法的人(这就是我正在寻找的),请使用以下代码:public void <T> T[] newArray(Class<T> type, int length) { ... } - Daniel Kvist
2
很遗憾,Java泛型基本上是虚假的,所以你不能什么都不用类对象。 - Nyerguds
显示剩余4条评论

270
你可以这样做:
E[] arr = (E[])new Object[INITIAL_ARRAY_LENGTH];

这是在Effective Java; Item 26中实现通用集合的建议方法之一。没有类型错误,也不需要重复转换数组。然而这会触发警告,因为它可能存在潜在的危险,并且应该谨慎使用。正如注释中所述,这个Object[]现在冒充我们的E[]类型,如果不安全地使用它,就可能导致意料之外的错误或者ClassCastException

一般来说,只要转换后的数组仅在内部使用(例如作为数据结构的后端),并且不会被返回或暴露给客户端代码,这种行为是安全的。如果你需要将泛型类型的数组返回给其他代码,你提到的反射Array类是正确的选择。


值得一提的是,如果你正在使用泛型,尽可能使用List而不是数组可以让你更轻松愉快。当然有时候你别无选择,但使用集合框架会更加健壮。


51
еҰӮжһңе°Ҷж•°з»„и§Ҷдёәд»»дҪ•зұ»еһӢзҡ„зұ»еһӢеҢ–ж•°з»„пјҢдҫӢеҰӮеңЁдёҠйқўзҡ„test()ж–№жі•дёӯзҡ„String[] s=b;пјҢеҲҷжӯӨж–№жі•е°Ҷж— жі•иө·дҪңз”ЁгҖӮиҝҷжҳҜеӣ дёәEж•°з»„дёҚжҳҜзңҹжӯЈзҡ„Eж•°з»„пјҢиҖҢжҳҜObject[]ж•°з»„гҖӮеҰӮжһңдҪ жғіиҰҒдёҖдёӘList<String>[]пјҢиҝҷеҫҲйҮҚиҰҒ - дҪ дёҚиғҪдҪҝз”ЁдёҖдёӘObject[]пјҢдҪ еҝ…йЎ»е…·дҪ“дҪҝз”ЁдёҖдёӘList[]гҖӮиҝҷе°ұжҳҜдёәд»Җд№ҲдҪ йңҖиҰҒдҪҝз”ЁеҸҚе°„зҡ„Class<?>ж•°з»„еҲӣе»әгҖӮ - Lawrence Dol
8
如果你想要执行例如 public E[] toArray() { return (E[])internalArray.clone(); } 这样的代码,而 internalArray 被定义为 E[] 类型,实际上是一个 Object[],那么就会出现问题。这种情况在运行时会因为类型转换异常而失败,因为 Object[] 不能赋值给任何类型为 E 的数组。 - Lawrence Dol
19
只要不把数组返回、传递或存储到需要特定类型数组的类之外的某个地方,这种方法就能起作用。只要在类内部,由于 E 被擦除了,所以没有问题。这种方法是“危险”的,因为如果尝试返回数组或其他操作,就没有任何安全警告。但只要小心谨慎,它就能运行。 - newacct
3
非常安全。在E[] b = (E[])new Object[1];这个语句中,你可以清晰地看到所创建的数组唯一被引用的是变量b,且b的类型为E[]。因此不存在通过其他类型的变量意外访问该数组的危险。如果改为 Object[] a = new Object[1]; E[]b = (E[])a;,那么你需要对如何使用变量a非常小心。 - Aaron McDaid
5
至少在Java 1.6中,这会生成一个警告:"从Object[]到T[]的未经检查的转换"。 - Quantum7
显示剩余14条评论

71

以下是如何使用泛型来获取精确类型的数组,同时保持类型安全(与其他答案相反,它们将返回一个Object数组或在编译时产生警告):

import java.lang.reflect.Array;  

public class GenSet<E> {  
    private E[] a;  
    
    public GenSet(Class<E[]> type, int length) {  
        a = type.cast(Array.newInstance(type.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对象和由第二个参数表示的长度指定的类型数组。调用方法getComponentType将返回表示由调用该方法的Class对象表示的数组的组件类型的Class对象(例如,对于String[].class,返回String.class,如果Class对象不表示数组,则返回null)。

最后一句话并不完全准确。调用String[].class.getComponentType()将返回表示类StringClass对象,但其类型为Class<?>,而不是Class<String>,这就是为什么您不能执行以下操作的原因。

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

6
这些只是一些复杂的写法,和新的String[...]作用相同,实际上需要的是类似于public static <T> T[] newArray(int size) { ... }的方法,但Java中并不存在这种方法,也无法通过反射模拟。原因是在运行时不可获取有关泛型类型如何实例化的信息。 - Ingo
4
@Ingo 你在说什么?我的代码可以用来创建任何类型的数组。 - gdejohn
3
@Charlatan:当然可以,但是新的[]也可以。问题在于:谁知道类型以及何时知道。因此,如果您只有一个通用类型,那么无法实现。 - Ingo
2
我不怀疑这一点。关键是,在运行时你无法获得泛型类型X的类对象。 - Ingo
2
几乎。我承认这比使用new[]能实现的更多。在实践中,这几乎总是可以胜任工作。然而,例如,仍然不可能编写一个参数化为E的容器类,该类具有方法E[] toArray()并且确实返回真正的E[]数组。只有在集合中至少有一个E对象时才能应用您的代码。因此,通用解决方案是不可能的。 - Ingo
显示剩余8条评论

44

这是唯一一个类型安全的答案

E[] a;

a = newArray(size);

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

12
如果E是一个类型变量,那么这种方法是不可行的。当E是一个类型变量时,varargs会创建一个E擦除后的数组,从而使其与(E[])new Object[n]没有太大的区别。请参见http://ideone.com/T8xF91。它绝对不比其他任何答案更加类型安全。 - Radiodef
1
@Radiodef - 这个解决方案在编译时可以证明是类型安全的。请注意,擦除并不完全属于语言规范;规范被精心编写,以便将来我们可以完全实现具体化 - 然后这个解决方案在运行时也将完美地工作,而不像其他解决方案。 - ZhongYu
@Radiodef - 类型安全是一个编译时的概念。众所周知,即使在泛型出现之前,一个类型安全的Java程序也可能在运行时失败,例如数组向上转型。你可能对“类型安全”有不同的概念,因此这只是定义上的差异。但是OP称其为类型安全并没有错。 - ZhongYu
2
@Radiodef - 有一些差异。这个解决方案的正确性是由编译器检查的,它不依赖于强制转换的人类推理。对于这个特定的问题来说,这种差异并不重要。有些人只是喜欢有点花哨,仅此而已。如果有人被 OP 的措辞误导了,那么你和我的评论会澄清这一点。 - ZhongYu
1
@irreputable 我喜欢这个,但我认为你不需要 length,你可以写成 return Arrays.copyOf(Objects.requireNonNull(array), array.length); - Eugene
显示剩余4条评论

36

如果要扩展到更多维度,只需在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(),了解详情。

6
有一些关于多维数组创建的问题被关闭,并作为这篇帖子的重复问题,但是还没有回答明确解决这个问题。 - Paul Bellora

16
您不需要将Class参数传递给构造函数。 尝试这个。
public class GenSet<T> {

    private final T[] array;

    @SafeVarargs
    public 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);
    }

    @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]

这个答案很棒,为了一个未使用的可变参数,你可以获得完全的数组类型兼容性,而无需涉及类对象或反射。希望JDK能够节省在每个调用点构造新的空数组的成本,并重复使用相同的空数组,否则缺点就是稍微增加了对象的创建。 - gary
请注意,尽管编译器警告可能存在堆污染,但可以通过@SafeVarargs注解安全地忽略这个警告,因为可变参数中不会有任何"坏值"(因为根本没有任何值)。 - STh

16
在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 之前也可以使用匿名类来完成,但更加麻烦。


你其实不需要像 ArraySupplier 这样的特殊接口,你可以将构造函数声明为 GenSet(Supplier<E[]> supplier) { ... 并使用与你现有代码相同的行来调用它。 - Lii
5
若要与我的示例相同,应该是 IntFunction<E[]>,但没错。 - Radiodef

13

这个主题在Effective Java, 2nd Edition的第五章(泛型)中有详细介绍,第25条...

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

@SuppressWarnings({"unchecked"})

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

OpenJDK项目网站上有一个关于这个bug/feature的有趣讨论。


7
虽然这个帖子已经没有人回复了,但我想引起你的注意。
泛型用于在编译时进行类型检查。因此,目的是检查:
  • 输入的内容是否是所需的。
  • 返回的内容是否符合消费者的需求。
请看下面的示例:

enter image description here

当您编写泛型类时,请不要担心类型转换警告;当您使用它时才需要关注。

7

Java泛型通过在编译时检查类型并插入适当的转换来工作,但会在编译后擦除文件中的类型信息。这使得通常无法在运行时找出类型,尽管它对于不理解泛型的代码是一个明确的设计决策,但也让泛型库可被这些代码使用。

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

这意味着你可以在构造函数中创建一个适当类型的数组对象,这意味着向集合添加对象时将检查对象的类型。


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