ArrayList.toArray() 中的 Java 泛型

Posted

技术标签:

【中文标题】ArrayList.toArray() 中的 Java 泛型【英文标题】:Java generics in ArrayList.toArray() 【发布时间】:2016-08-04 13:55:41 【问题描述】:

假设你有一个如下定义的数组列表:

ArrayList<String> someData = new ArrayList<>();

稍后在您的代码中,由于泛型,您可以这样说:

String someLine = someData.get(0);

并且编译器完全知道它将得到一个字符串。耶泛型!但是,这将失败:

String[] arrayOfData = someData.toArray();

toArray() 将始终返回一个对象数组,而不是定义的泛型。为什么get(x)方法知道返回的是什么,而toArray()默认是Objects?

【问题讨论】:

你在说什么?哪个类有toArray() 方法? ArrayList 有一个toArray() 方法,但是即使定义了泛型,toArray() 方法也会返回Object[],而不是E[],这是使用泛型隐含的。 您可以使用toArray(T[] a) 方法来获取适当的数组,而不是覆盖toArray() 是的,我知道我可以到toArray(T[] a),但为什么它不直接内置在toArray() 中。我不明白为什么get(x) 知道它发出了什么,而toArray() 却不知道。 @JimmyB:另一种选择是动态类型的某种工厂,例如Java 8’s Stream.toArray 使用 IntFunction&lt;ArrayType&gt; 作为工厂类型,作为参数传递。这种模式以前没有使用过的原因,例如对于Collection 接口,只有Java 8 允许像ElementType[]::new 一样简洁地实现它,例如String[] array = stream.toArray(String[]::new) 【参考方案1】:

一般信息在运行时为erased。 JVM 不知道你的列表是List&lt;String&gt; 还是List&lt;Integer&gt;(在运行时List&lt;T&gt; 中的T 被解析为Object),所以唯一可能的数组类型是Object[]

您可以使用toArray(T[] array) - 在这种情况下,JVM 可以使用给定数组的类,您可以在ArrayList 实现中看到它:

public <T> T[] toArray(T[] a) 
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());

【讨论】:

您的答案是正确的,但对于非 Java 专家来说并不容易理解。重要的事实是返回的数组必须在运行时创建(-> 缺少什么类型的信息),其中 get(..) 只返回一个现有对象。【参考方案2】:

如果您查看Javadoc for the List interface,您会注意到toArray 的第二种形式:&lt;T&gt; T[] toArray(T[] a)

事实上,Javadoc 甚至给出了一个例子来说明如何做你想做的事:

String[] y = x.toArray(new String[0]);

【讨论】:

【参考方案3】:

数组的类型与数组的类型不同。它是一种 StringArray 类,而不是 String 类。

假设有可能,通用方法 toArray() 看起来像

private <T> T[] toArray() 
    T[] result = new T[length];
    //populate
    return result;

现在在编译期间,类型 T 被删除。 new T[length]这部分应该怎么换?泛型类型信息不可用。

如果您查看(例如)ArrayList 的源代码,您会看到相同的结果。 toArray(T[] a) 方法要么填充给定数组(如果大小匹配),要么使用参数的 type 创建一个新数组,该参数是 Generic Type T 的数组类型。 p>

【讨论】:

get(x) 方法如何解决这个问题?它知道它在回馈什么 它没有,在运行时 get(x) 返回 Object。编译器隐式插入强制转换只是对您隐藏了这一点。【参考方案4】:

如果你看ArrayList<E>类的toArray(T[] a)的实现,是这样的:

public <T> T[] toArray(T[] a) 
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;

此方法的问题是您需要传递相同泛型类型的数组。现在考虑如果此方法不带任何参数,那么实现将类似于:

public <T> T[] toArray() 
    T[] t = new T[size]; // compilation error
    return Arrays.copyOf(elementData, size, t.getClass());

但这里的问题是 您不能在 Java 中创建泛型数组,因为编译器并不确切知道 T 代表什么。换句话说,创建不可具体化类型的数组 (JLS §4.7)在 Java 中是不允许的

Array Store Exception (JLS §10.5) 的另一个重要引述:

如果数组的组件类型不可具体化(第 4.7 节),Java 虚拟机将无法执行 前段。这就是为什么数组创建表达式带有 禁止不可具体化的元素类型 (§15.10.1)。

这就是Java提供重载版本toArray(T[] a)的原因。

我将重写 toArray() 方法告诉它它将返回一个 E 数组。

因此,您应该使用toArray(T[] a),而不是覆盖toArray()

来自 Java Doc 的Cannot Create Instances of Type Parameters 可能对您来说也很有趣。

【讨论】:

谢谢。这黑白分明地解释了它失败的原因。 简而言之,Java 不允许方法在(仅)其返回值中是多态(即泛型)。 确实支持此功能的语言示例是 Haskell。 我想你抓住了问题的症结,那就是 Java 不能创建泛型类型的数组。 @jpaugh Java 确实允许这样做,它甚至可以根据 LHS 推断分配的 RHS 类型。您还可以在调用泛型方法时显式声明类型参数。例如,请参阅ideone.com/AObtRR。 @JohannesD 我是正确的。 (谢谢!)toArray 之类的示例和您指向getnull 的链接表明 Java 来如此接近,只是错过了标记。再说一次,Haskell 甚至没有尝试子类型化,所以很难比较他们的泛型支持一对一。【参考方案5】:

我可以并且有时会使用迭代器而不是创建数组,但这对我来说总是很奇怪。为什么 get(x) 方法知道它返回的是什么,而 toArray() 默认是 Objects?设计到一半,他们决定这里不需要??

由于这个问题的意图似乎不仅仅是为了解决使用泛型的toArray(),而是为了理解ArrayList 类中方法的设计,我想补充一下:

ArrayList 是一个泛型类,因为它被声明为

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

这使得在类中使用诸如public E get(int index) 之类的通用方法成为可能。

但是,如果像toArray() 这样的方法没有返回E,而是E[],那么事情就会变得有些棘手。无法提供诸如public &lt;E&gt; E[] toArray() 之类的签名,因为无法创建通用数组。

数组的创建发生在运行时,由于Type erasure,Java 运行时没有E 表示的类型的特定信息。到目前为止,唯一的解决方法是将所需的类型作为参数传递给方法,因此签名public &lt;T&gt; T[] toArray(T[] a) 强制客户端传递所需的类型。

但另一方面,它适用于public E get(int index),因为如果您查看该方法的实现,您会发现即使该方法使用相同的 Object 数组来返回指定索引处的元素,它被转换为E

E elementData(int index) 
    return (E) elementData[index];

Java 编译器在编译时将E 替换为Object

【讨论】:

【参考方案6】:

需要注意的是,Java 中的数组在运行时知道它们的组件类型。 String[]Integer[] 在运行时是不同的类,您可以在运行时向数组询问它们的组件类型。因此,在运行时需要一个组件类型(通过在编译时使用new String[...] 硬编码一个可具体化的组件类型,或者使用Array.newInstance() 并传递一个类对象)来创建一个数组。

另一方面,泛型中的类型参数在运行时不存在。 ArrayList&lt;String&gt;ArrayList&lt;Integer&gt; 在运行时绝对没有区别。这只是ArrayList

这就是为什么你不能只获取List&lt;String&gt; 并获得String[] 而不以某种方式单独传递组件类型的根本原因——你必须从没有的东西中获取组件类型信息组件类型信息。显然,这是不可能的。

【讨论】:

【参考方案7】:

首先你要明白ArrayList自己只是Object的数组

   transient Object[] elementData;

说到T[]失败的原因,是因为没有Class&lt;T&gt;就无法获得泛型类型的数组,这是因为java的类型擦除(there is a more explanation和how to create one)。并且堆上的array[] 动态地知道它的类型,你不能将int[] 转换为String[]。同样的原因,您不能将Object[] 转换为T[]

   int[] ints = new int[3];
   String[] strings = (String[]) ints;//java: incompatible types: int[] cannot be converted to java.lang.String[]

   public <T> T[] a() 
      Object[] objects = new Object[3];
      return (T[])objects;
   
   //ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
   Integer[] a = new LearnArray().<Integer>a();

但是您放入array的只是一个类型为E的对象(由编译器检查),因此您可以将其转换为安全且正确的E

  return (E) elementData[index];

简而言之,你无法通过演员获得没有的东西。你只有Object[],所以toArray() 可以返回Object[](否则,你必须给它一个Class&lt;T&gt; 来创建一个具有这种类型的新数组)。你把E放在ArrayList&lt;E&gt;中,你可以得到一个Eget()

【讨论】:

【参考方案8】:

可以创建给定(已知)类型的“通用”数组。通常我在我的代码中使用这样的东西。

public static <T> T[] toArray(Class<T> type, ArrayList<T> arrList) 
    if ((arrList == null) || (arrList.size() == 0)) return null;
    Object arr = Array.newInstance(type, arrList.size());
    for (int i=0; i < arrList.size(); i++) Array.set(arr, i, arrList.get(i));
    return (T[])arr;

【讨论】:

以上是关于ArrayList.toArray() 中的 Java 泛型的主要内容,如果未能解决你的问题,请参考以下文章

Java ArrayList toArray避免复制

Java转换Arraylist 漂浮[]

整数对最小和

int[ ] 和 Integer [ ] 有啥区别 [重复]

Java 后台sql注入

ja对象属性—枚举检查删除