我不能在Java中创建通用数组类型的原因是什么?


273

Java不允许我们这样做的原因是什么

private T[] elements = new T[initialCapacity];

我可以理解.NET不允许我们这样做,因为.NET中的值类型在运行时可以具有不同的大小,但是在Java中,各种T都是对象引用,因此具有相同的大小(如我错了请纠正我)。

是什么原因?


29
你在说什么?您绝对可以在.NET中执行此操作。-我在这里试图弄清楚为什么我不能用Java做到这一点。
BrainSlugs83 2014年

@ BrainSlugs83-请添加指向某些代码示例或教程的链接,以显示该内容。
MasterJoe2


1
@ MasterJoe2 OP的问题中的上面的代码是我指的是什么。它在C#中可以正常工作,但在Java中则不能。-问题指出,这两种方法都不起作用,这是不正确的。-不确定进一步讨论是否有价值。
BrainSlugs83

Answers:


204

这是因为Java的数组(与泛型不同)在运行时包含有关其组件类型的信息。因此,在创建数组时必须知道组件类型。由于您不知道T运行时是什么,因此无法创建数组。


29
但是擦除呢?为什么不适用?
Qix-蒙尼卡(Monica)2013年

14
那怎么ArrayList <SomeType>办呢?
Thumbz 2014年

10
@Thumbz:你的意思是new ArrayList<SomeType>()?泛型类型在运行时不包含type参数。type参数未在创建中使用。new ArrayList<SomeType>()new ArrayList<String>()或生成的代码完全没有区别new ArrayList()
newacct 2014年

8
我在问更多关于它的ArrayList<T>工作原理的信息private T[] myArray。在代码中的某个地方,它必须具有通用类型T的数组,那又如何呢?
Thumbz 2014年

21
@Thumbz:它没有运行时类型数组T[]。它具有一个运行时类型数组Object[],或者:1)源代码包含一个变量Object[](这在最新的Oracle Java源代码中就是这样);或2)源代码包含type的变量T[],这是一个谎言,但不会由于T在类范围内被擦除而引起问题。
newacct 2014年

137

引用:

不允许使用通用类型的数组,因为它们没有声音。问题是由于Java数组(不是静态声音而是可以动态检查)与泛型(它们是静态声音而不是动态检查)的交互作用所致。这是利用漏洞的方法:

class Box<T> {
    final T x;
    Box(T x) {
        this.x = x;
    }
}

class Loophole {
    public static void main(String[] args) {
        Box<String>[] bsa = new Box<String>[3];
        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3); // error not caught by array store check
        String s = bsa[0].x; // BOOM!
    }
}

我们曾建议使用静态安全阵列(又称“方差”)解决问题,但Tiger拒绝了。

- gafter

(我相信是Neal Gafter,但不确定)

在上下文中查看它:http : //forums.sun.com/thread.jspa?threadID=457033&forumID=316


3
请注意,由于答案不是我的,所以将其设为CW。
巴特·基尔斯

10
这解释了为什么它可能不是类型安全的。但是编译器可能会警告类型安全问题。事实是,出于几乎相同的原因,您甚至无法执行此操作new T()。按照设计,Java中的每个数组都在其中存储组件类型(即T.class)。因此,在运行时需要T类来创建这样的数组。
newacct

2
您仍然可以使用new Box<?>[n],尽管有时对示例没有帮助,但有时可能就足够了。
Bartosz Klimek

1
@BartKiers我不明白...这仍然无法编译(java-8):Box<String>[] bsa = new Box<String>[3];我认为Java-8及以上版本有什么变化吗?
尤金(Eugene)

1
@Eugene,只允许使用特定泛型类型的数组,因为它们会导致类型安全性的损失,如示例中所示。在任何版本的Java中均不允许使用。答案开始于“不允许通用类型的数组,因为它们没有声音。”
garnet

47

由于未能提供一个体面的解决方案,您最终会遇到更糟糕的恕我直言。

常见的解决方法如下。

T[] ts = new T[n];

被替换为(假设T扩展了Object而不是另一个类)

T[] ts = (T[]) new Object[n];

我更喜欢第一个例子,但是更多的学术类型似乎更喜欢第二个例子,或者只是不想考虑它。

为什么不能仅使用Object []的大多数示例同样适用于List或Collection(受支持),因此我将它们视为非常差的参数。

注意:这是Collections库本身不会在没有警告的情况下进行编译的原因之一。如果您无法在没有警告的情况下支持此用例,则泛型模型IMHO会从根本上破坏某些东西。


6
您必须小心第二个。如果将以这种方式创建的数组返回给期望的人String[](或者,如果将其存储在type可以公开访问的字段中T[],并且有人检索它),则他们将获得ClassCastException。
newacct 2010年

4
我投这个答案了,因为你的首选例子不是在Java和你的第二个例子允许可以抛出ClassCastException
何塞·罗伯托·阿劳霍儒尼奥尔

5
@JoséRobertoAraújoJúnior很显然,第一个示例需要替换为第二个示例。解释第二个示例为何会引发ClassCastException的原因,因为这对每个人来说都不是显而易见的,这将对您有所帮助。
彼得·劳瑞

3
@PeterLawrey我创建了一个自回答问题,说明为什么T[] ts = (T[]) new Object[n];:是一个坏主意stackoverflow.com/questions/21577493/...
何塞·罗伯托·阿劳霍儒尼奥尔

1
@MarkoTopolnik我应该获得一枚勋章,用于回答您的所有评论,以解释我已经说过的同一件事,唯一改变我原先理由的是,尽管我说他T[] ts = new T[n];是一个有效的例子。我将继续投票,因为他的回答可能会给其他开发人员带来问题和困惑,而且也是题外话。另外,我将停止对此发表评论。
2014年

38

数组是协变的

数组被认为是协变的,这基本上意味着,在Java的子类型化规则下,类型的数组T[]可以包含类型的元素T或类型的任何子类型T。例如

Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);

不仅如此,对Java也状态子类型规则的数组S[]是数组的一个亚型T[],如果S是的一个亚型T,因此,这样的事情也是正确的:

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

因为根据Java中的子类型化规则,因为Integer是Number 的子类型,所以数组Integer[]是数组Number[]的子类型。

但是,这种子类型化规则可能会引起一个有趣的问题:如果我们尝试这样做,会发生什么?

myNumber[0] = 3.14; //attempt of heap pollution

最后一行可以正常编译,但是如果运行此代码,则会得到一个an,ArrayStoreException因为我们试图将double放入整数数组中。这里我们通过Number引用访问数组的事实是无关紧要的,重要的是该数组是整数数组。

这意味着我们可以欺骗编译器,但不能欺骗运行时类型系统。之所以这样,是因为数组是我们所谓的可更改类型。这意味着在运行时,Java知道此数组实际上是作为整数数组实例化的,而该整数数组恰巧是通过type的引用来访问的Number[]

因此,正如我们所看到的,一件事是对象的实际类型,另一件事是我们用来访问它的引用的类型,对吗?

Java泛型的问题

现在,Java中的泛型类型的问题在于,在完成代码编译之后,编译器会丢弃类型参数的类型信息。因此,此类型信息在运行时不可用。此过程称为类型擦除。有充分的理由在Java中实现这样的泛型,但这是一个很长的故事,它与预先存在的代码的二进制兼容性有关。

这里的重点是,由于在运行时没有类型信息,因此无法确保我们不会造成堆污染。

现在让我们考虑以下不安全代码:

List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution

如果Java编译器没有阻止我们执行此操作,那么运行时类型系统也不会阻止我们执行此操作,因为在运行时无法确定该列表仅应为整数列表。Java运行时可以让我们将所需的所有内容放入此列表,因为它只应包含整数,因为在创建时将其声明为整数列表。这就是为什么编译器拒绝行号4的原因,因为这是不安全的,并且如果允许的话可能会破坏类型系统的假设。

因此,Java的设计人员确保了我们不会欺骗编译器。如果我们不能欺骗编译器(就像使用数组一样),那么我们也不能欺骗运行时类型系统。

因此,我们说通用类型是不可修正的,因为在运行时我们无法确定通用类型的真实性质。

我跳过了此答案的某些部分,您可以在此处阅读全文:https : //dzone.com/articles/covariance-and-contravariance


32

之所以不可能,是因为Java纯粹在编译器级别上实现其泛型,并且每个类仅生成一个类文件。这称为类型擦除

在运行时,已编译的类需要使用相同的字节码处理其所有使用。因此,new T[capacity]绝对不知道需要实例化什么类型。


17

答案已经给出,但是如果您已经有一个实例T,那么您可以这样做:

T t; //Assuming you already have this object instantiated or given by parameter.
int length;
T[] ts = (T[]) Array.newInstance(t.getClass(), length);

希望我能帮上忙,Ferdi265


1
这是一个很好的解决方案。但这将得到未经检查的警告(从Object广播到T [])。另一个“较慢”但“无警告”的解决方案是:T[] ts = t.clone(); for (int i=0; i<ts.length; i++) ts[i] = null;
Midnite

1
另外,如果我们保留的是T[] t,那么它将是(T[]) Array.newInstance(t.getClass().getComponentType(), length);。我确实花了一些时间来弄清楚getComponentType()。希望这对其他人有帮助。
Midnite 2013年

1
@midnite t.clone()不会返回T[]。因为t不是这个答案中的Array。
xmen 2014年

6

主要原因是由于Java中的数组是协变的。

有一个很好的概述这里


我不知道即使使用不变数组,您如何也不能支持“ new T [5]”。
迪米特里斯·安德烈

2
@DimitrisAndreou嗯,整件事都是Java设计中的错误喜剧。这一切都始于数组协方差。然后,一旦有了数组协方差,就可以将其强制String[]转换Object并存储Integer在其中。因此,他们必须为数组存储(ArrayStoreException)添加运行时类型检查,因为在编译时无法发现问题。(否则,Integer实际上可能会卡在中String[],并且在尝试检索时会出现错误,这将是可怕的。)……
Radon Rosborough 2014年

2
@DimitrisAndreou…然后,一旦您将运行时检查替换为更可靠的编译时检查,就会遇到类型擦除(这也是一个不幸的设计缺陷,仅出于向后兼容性而包含)。类型擦除意味着您不能对泛型类型进行运行时类型检查。因此,为避免阵列存储类型问题,您根本不能拥有通用阵列。如果他们只是简单地使数组不变,那么我们就可以进行编译时类型检查,而不必担心擦除。
Radon Rosborough 2014年

……我刚刚发现了五分钟的评论编辑期。Object应该是Object[]我的第一条评论。
Radon Rosborough 2014年

3

我喜欢Gafter间接给出的答案。但是,我认为这是错误的。我对Gafter的代码做了些改动。它会编译并运行一段时间,然后在Gafter预测会爆炸的地方炸弹

class Box<T> {

    final T x;

    Box(T x) {
        this.x = x;
    }
}

class Loophole {

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

    public static void main(String[] args) {

        Box<String> a = new Box("Hello");
        Box<String> b = new Box("World");
        Box<String> c = new Box("!!!!!!!!!!!");
        Box<String>[] bsa = array(a, b, c);
        System.out.println("I created an array of generics.");

        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3);
        System.out.println("error not caught by array store check");

        try {
            String s = bsa[0].x;
        } catch (ClassCastException cause) {
            System.out.println("BOOM!");
            cause.printStackTrace();
        }
    }
}

输出是

I created an array of generics.
error not caught by array store check
BOOM!
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at Loophole.main(Box.java:26)

因此在我看来,您可以在Java中创建通用数组类型。我误会了这个问题吗?


您的示例与我所要求的不同。您所描述的是数组协方差的危险。

希望您从编译器收到类型安全警告,是吗?
马特·麦克亨利

1
是的,我收到类型安全警告。是的,我看到我的例子对这个问题没有回应。
Emory

实际上,由于a,b,c的草率初始化,您会收到多个警告。同样,这是众所周知的,并且会影响核心库,例如<T> java.util.Arrays.asList(T ...)。如果您为T传递了任何非可验证的类型,则会收到警告(因为所创建的数组的类型比代码假装的精度低),而且它非常丑陋。考虑到该方法本身是安全的,因此不会将数组暴露给用户,如果此方法的作者得到警告,而不是在使用现场发出警告,则更好。
Dimitris Andreou

1
您未在此处创建通用数组。编译器为您创建了一个(非通用)数组。
newacct 2010年

2

我知道我在这里参加聚会有点晚了,但是我想我可以为将来的任何Google员工提供帮助,因为这些答案都无法解决我的问题。Ferdi265的回答尽管有很大帮助。

我正在尝试创建自己的链接列表,因此以下代码对我有用:

package myList;
import java.lang.reflect.Array;

public class MyList<TYPE>  {

    private Node<TYPE> header = null;

    public void clear() {   header = null;  }

    public void add(TYPE t) {   header = new Node<TYPE>(t,header);    }

    public TYPE get(int position) {  return getNode(position).getObject();  }

    @SuppressWarnings("unchecked")
    public TYPE[] toArray() {       
        TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());        
        for(int i=0 ; i<size() ; i++)   result[i] = get(i); 
        return result;
    }


    public int size(){
         int i = 0;   
         Node<TYPE> current = header;
         while(current != null) {   
           current = current.getNext();
           i++;
        }
        return i;
    }  

在toArray()方法中为我创建通用类型数组的方法:

TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());    

2

就我而言,我只是想要一个堆栈数组,像这样:

Stack<SomeType>[] stacks = new Stack<SomeType>[2];

由于这是不可能的,因此我将以下内容用作解决方法:

  1. 在Stack周围创建了一个非通用包装类(例如MyStack)
  2. MyStack []堆栈=新的MyStack [2]完美运行

丑陋,但是Java很高兴。

注意:正如BrainSlugs83在对该问题的评论中提到的那样,.NET中完全可能具有泛型数组


2

Oracle教程

您不能创建参数化类型的数组。例如,以下代码无法编译:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

以下代码说明了将不同类型插入到数组中时发生的情况:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

如果您对通用列表尝试相同的操作,则会出现问题:

Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.

如果允许使用参数化列表的数组,则先前的代码将无法引发所需的ArrayStoreException。

对我来说,这听起来很微弱。我认为任何对泛型有足够了解的人都将很好,甚至期望在这种情况下不会引发ArrayStoredException。


0

当然,肯定有一个好的方法(也许使用反射),因为在我看来这正是这样ArrayList.toArray(T[] a)做的。我引用:

public <T> T[] toArray(T[] a)

返回以正确顺序包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。如果列表适合指定的数组,则在列表中返回。否则,将使用指定数组的运行时类型和此列表的大小分配一个新数组。

因此,解决该问题的一种方法是使用此函数,即ArrayList在数组toArray(T[] a)中创建所需的对象,然后用于创建实际的数组。不会很快,但是您没有提到您的要求。

那么有人知道如何toArray(T[] a)实现吗?


3
List.toArray(T [])之所以起作用,是因为您实际上是在运行时给它提供了组件类型T(给它提供了所需数组类型的实例,可以从中获取数组类,然后从中获取组件类T)。 )。对于运行时的实际组件类型,您始终可以使用创建该运行时类型的数组Array.newInstance()。您会发现许多问题中提到的问题,这些问题询问如何在编译时创建类型未知的数组。但是OP专门询问为什么不能使用new T[]语法,这是一个不同的问题
newacct

0

这是因为泛型是在Java创建后添加到Java的,所以它有点笨拙,因为Java的原始制造商认为在创建数组时会在创建数组时指定类型。因此,这不适用于泛型,因此您必须执行E [] array =(E [])new Object [15]; 这会编译,但是会发出警告。


0

如果我们无法实例化通用数组,为什么该语言具有通用数组类型?没有对象的类型有什么意义?

我能想到的唯一原因是varargs- foo(T...)。否则,它们可能已经彻底清除了通用数组类型。(好吧,他们并不需要为argargs使用数组,因为varargs在1.5之前就不存在。这可能是另一个错误。)

所以这是一个谎言,您可以通过varargs实例化通用数组!

当然,通用数组的问题仍然存在,例如

static <T> T[] foo(T... args){
    return args;
}
static <T> T[] foo2(T a1, T a2){
    return foo(a1, a2);
}

public static void main(String[] args){
    String[] x2 = foo2("a", "b"); // heap pollution!
}

我们可以使用该示例实际演示通用数组的危险。

另一方面,我们使用通用varargs已经有十年了,天空还没有落下。因此,我们可以说问题被夸大了。这没什么大不了的。如果允许显式创建通用数组,那么我们到处都会有错误;但是我们已经习惯了擦除的问题,并且可以忍受。

我们可以指出这一点,foo2即该规范使我们脱离了他们声称使我们无法解决的问题。如果Sun在1.5上有更多的时间和资源,我相信它们可以达到更令人满意的分辨率。


0

正如其他人已经提到的,您当然可以通过一些技巧来创建。

但是不建议这样做。

因为类型擦除,更重要的covariance是,仅允许子类型数组的in数组可以分配给超类型数组,这迫使您在尝试取回导致运行时的值时使用显式类型强制转换ClassCastException,这是主要目标之一泛型试图消除的问题:在编译时进行更强的类型检查

Object[] stringArray = { "hi", "me" };
stringArray[1] = 1;
String aString = (String) stringArray[1]; // boom! the TypeCastException

一个更直接的示例可以在有效Java:项目25中找到


协方差:如果S是T的子类型,则类型为S []的数组是T []的子类型


0

如果该类用作参数化类型,则可以声明T []类型的数组,但不能直接实例化此类数组。相反,一种常见的方法是实例化类型为Object []的数组,然后将其范围缩小为类型T [],如下所示:

  public class Portfolio<T> {
  T[] data;
 public Portfolio(int capacity) {
   data = new T[capacity];                 // illegal; compiler error
   data = (T[]) new Object[capacity];      // legal, but compiler warning
 }
 public T get(int index) { return data[index]; }
 public void set(int index, T element) { data[index] = element; }
}

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.