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<ArrayType>
作为工厂类型,作为参数传递。这种模式以前没有使用过的原因,例如对于Collection
接口,只有Java 8 允许像ElementType[]::new
一样简洁地实现它,例如String[] array = stream.toArray(String[]::new)
…
【参考方案1】:
一般信息在运行时为erased。 JVM 不知道你的列表是List<String>
还是List<Integer>
(在运行时List<T>
中的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
的第二种形式:<T> 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 <E> E[] toArray()
之类的签名,因为无法创建通用数组。
数组的创建发生在运行时,由于Type erasure,Java 运行时没有E
表示的类型的特定信息。到目前为止,唯一的解决方法是将所需的类型作为参数传递给方法,因此签名public <T> 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<String>
和 ArrayList<Integer>
在运行时绝对没有区别。这只是ArrayList
。
这就是为什么你不能只获取List<String>
并获得String[]
而不以某种方式单独传递组件类型的根本原因——你必须从没有的东西中获取组件类型信息组件类型信息。显然,这是不可能的。
【讨论】:
【参考方案7】:首先你要明白ArrayList
自己只是Object
的数组
transient Object[] elementData;
说到T[]
失败的原因,是因为没有Class<T>
就无法获得泛型类型的数组,这是因为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<T>
来创建一个具有这种类型的新数组)。你把E
放在ArrayList<E>
中,你可以得到一个E
和get()
。
【讨论】:
【参考方案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 泛型的主要内容,如果未能解决你的问题,请参考以下文章