将ArrayList<String>转换为String[]数组

1219

我在Android环境中工作,尝试了以下代码,但似乎没有起作用。

String [] stockArr = (String[]) stock_list.toArray();

如果我这样定义:

String [] stockArr = {"hello", "world"};

它有效。我是否遗漏了什么?


48
请参考Java文档中的内容,在代码中使用String [] stockArr = (String[]) stock_list.toArray(new String[0]);语句。 - Nishant
15
你不需要进行类型转换!String[] stockArr = stock_list.toArray(new String[0]);就足够了。 - Grim
已经使用更新的方法制作了这个答案,JDK-11引入了一个新的同样高效的API来处理toArray(T[])等语法,类似于Stream.toArray - Naman
6个回答

1834

像这样使用。

List<String> stockList = new ArrayList<String>();
stockList.add("stock1");
stockList.add("stock2");

String[] stockArr = new String[stockList.size()];
stockArr = stockList.toArray(stockArr);

for(String s : stockArr)
    System.out.println(s);

199
为了解释这里发生了什么,JVM不知道如何将Object[](toArray()方法的结果)盲目地向下转换为String[]。为了让它知道你想要的对象类型,你可以向toArray()方法传递一个带有类型的数组。这个类型化的数组可以是任意大小的(new String[1]是有效的),但如果太小,JVM会自动调整它的大小。 - dhackner
60
@dhackner说的是,JVM不知道如何将Object[]盲目地向下转换为String[]。更准确地说,它是不允许这样做的。如果它可以这样做,就会违反Java类型安全性。 - Stephen C
2
使用stock_list.toArray(stockArr)代替stockArr = stock_list.toArray(stockArr)。请参见https://dev59.com/dmkw5IYBdhLWcg3w6Opy#9572820。 - Eng.Fouad
3
对于新手来说,以下内容可能有用:如果您的数组列表包含double、float、int、long(原始类型),则必须将数组/数组列表定义为包含原始类型对象,例如Double、Float、Integer、Long。然后,您的常规数组必须使用此类型进行定义,例如Double[] myDoubleArray = list.toArray(new Double[listOfDoubles.size()]); - planty182
12
根据《Effective Java》一书中的Joshua Bloch所说,预先分配数组会影响性能。相反,应该提供一个长度为零的数组。可以使用stockList.toArray(new String[0])来实现。 - Crashh
显示剩余7条评论

979

试一下这个

String[] arr = list.toArray(new String[list.size()]);

Object[] obj = stock_list.toArray(); String[] stockArr = new String[obj.length]; for (int i = 0; i < stockArr.length; i++) { stockArr[i] = (String) obj[i]; } - Quan Nguyen
9
如果有人想知道@QuanNguyen的评论在说什么:他基本上通过.toArray()检索了Object[],然后手动将内容复制到一个新的String[]中,将每个元素强制转换为String。*这是一种不好的方法,相反你应该直接将新的String[]传递给.toArray()*。 - randers
25
不需要传递list.size(),这只会创建一个更大的临时数组开销,而该数组会立即被丢弃。可以直接使用String[] arr = list.toArray(new String[] {});。由于这个一行代码的简洁性仍然给出+1的赞扬。 - L. Holanda
11
如果参数中提供的数组适合列表,就会使用相同的数组;如果大小不合适,则会自行分配一个数组。在你的情况下,你最终创建了一个虚拟数组(大小为0)。 - st0le
19
注意,toArray(new String[0]) 实际上比 toArray(new String[list.size()]) 更快:https://dev59.com/Xm855IYBdhLWcg3w_5mm - Vadzim
我撤销了编辑以保持评论的相关性。其他答案已经报告了“new String [0]”版本。 - st0le

326

发生的情况是stock_list.toArray()创建了一个Object[]而不是String[],因此类型转换失败1

正确的代码应该是:

  String [] stockArr = stockList.toArray(new String[stockList.size()]);

甚至更多

  String [] stockArr = stockList.toArray(new String[0]);

请参考 List.toArray 的两个重载版本的 javadocs 获取更多细节。

后者使用零长度数组来确定结果数组的类型。(令人惊讶的是,这样做比预先分配一个数组更快...至少对于最近的 Java 版本而言。有关详细信息,请参见https://dev59.com/Xm855IYBdhLWcg3w_5mm#4042464。)

从技术角度来看,这种 API 行为/设计的原因是 List<T>.toArray() 方法的实现在运行时不知道 <T> 是什么类型。它只知道原始元素类型是 Object。相比之下,在另一种情况下,数组参数给出了数组的基本类型。(如果提供的数组足够大以容纳列表元素,则使用该数组。否则,分配并返回一个相同类型且更大大小的新数组作为结果。)


1 - 在 Java 中,Object[] 不能与 String[] 进行赋值兼容。如果可以,那么可以这样做:

    Object[] objects = new Object[]{new Cat("fluffy")};
    Dog[] dogs = (Dog[]) objects;
    Dog d = dogs[0];     // Huh???

这显然是无意义的,这就是为什么数组类型通常不能相互赋值的原因。


3
这无法处理原始类型。 - Joehot200
4
如果你的意思是将List<Integer>转换为int[],那是不正确的。它会被转换为Integer[] - Stephen C

169

Java 8中的另一种选择:

String[] strings = list.stream().toArray(String[]::new);

9
IntelliJ IDEA 建议将其更改为 list.toArray(new String[0])。我不知道为什么。 - Jin Kwon
@JinKwon 你的项目的目标语言版本是什么? - whirlwin
@whirlwin 8,我相信是这样的。 - Jin Kwon
list.toArray(String[]::new); - Oleg Tretiakov

90

我看到很多答案都展示了如何解决问题,但只有 Stephen的答案 尝试解释了为什么会出现这个问题,所以我将尝试在这个主题上添加更多内容。这是一个关于为什么在引入泛型到Java时,Object[] toArray没有被改为T[] toArray的可能原因的故事。


为什么String[] stockArr = (String[]) stock_list.toArray();不能工作?

在Java中,泛型类型仅存在于编译时。在运行时,有关泛型类型的信息(例如您的情况下的<String>)被删除并替换为Object类型(请参阅类型擦除)。这就是为什么在运行时toArray()不知道使用什么精确类型来创建新数组,因此它使用Object作为最安全的类型,因为每个类都扩展自Object,所以它可以安全地存储任何类的实例。

现在的问题是,您无法将Object[]的实例强制转换为String[]

为什么?看这个例子(假设class B extends A):

//B extends A
A a = new A();
B b = (B)a;

虽然这样的代码可以编译,但在运行时,我们会看到抛出 ClassCastException,因为被引用的实例 a 不是实际上属于类型 B(或其子类型)。为什么会出现这个问题(为什么需要进行强制类型转换)?其中一个原因是,B 可能有新的方法/字段,而 A 没有,因此即使所持有的实例不支持这些新成员,也有可能有人会尝试通过 b 引用来使用它们。换句话说,我们最终可能会尝试使用不存在的数据,这可能会导致许多问题。为了防止这种情况发生,JVM 抛出异常,并停止进一步潜在危险的代码。
你现在可以问:“那么为什么我们甚至不能更早地停下来呢?涉及这种强制类型转换的代码为什么还能编译?难道编译器不应该停下来吗?”答案是:不应该,因为编译器无法确定由 a 引用持有的实例的实际类型,而且有可能它将持有 b 引用的接口支持的类 B 的实例。看看这个例子:
A a = new B(); 
      //  ^------ Here reference "a" holds instance of type B
B b = (B)a;    // so now casting is safe, now JVM is sure that `b` reference can 
               // safely access all members of B class

现在让我们回到数组。正如您在问题中看到的,我们无法将Object[]数组的实例强制转换为更精确的类型 String[]

Object[] arr = new Object[] { "ab", "cd" };
String[] arr2 = (String[]) arr;//ClassCastException will be thrown

这里的问题有些不同。现在我们确定 String[] 数组不会有额外的字段或方法,因为每个数组只支持:
  • [] 运算符,
  • length 字段,
  • 从 Object 祖先继承的方法,
所以它不是数组接口使其不可能。问题在于 Object[] 数组除了 Strings 可以存储任何对象 (例如 Integers),因此有可能某一天我们会试图在 Integer 实例上调用像 strArray[i].substring(1,3) 这样的方法,而该实例没有这样的方法。
因此,为了确保这种情况永远不会发生,在 Java 中,数组引用只能保存以下内容:
  • 与引用相同类型的数组实例 (引用 String[] strArr 可以保存 String[])
  • 子类型的数组实例 (Object[] 可以保存 String[],因为 StringObject 的子类型),
但不能保存以下内容:
  • 引用的数组类型的超类型数组 (String[] 无法保存 Object[])
  • 与引用类型不相关的类型数组 (Integer[] 无法保存 String[])
换句话说,像这样是可以的:
Object[] arr = new String[] { "ab", "cd" }; //OK - because
               //  ^^^^^^^^                  `arr` holds array of subtype of Object (String)
String[] arr2 = (String[]) arr; //OK - `arr2` reference will hold same array of same type as 
                                //     reference

你可以说解决这个问题的一种方法是在运行时找到所有列表元素中最常见的类型,并创建该类型的数组,但在所有列表元素都是从通用类型派生出来的同一类型的情况下,这种方法将不起作用。看一下

//B extends A
List<A> elements = new ArrayList<A>();
elements.add(new B());
elements.add(new B());

当前最常见的类型是B,而不是A,因此使用toArray()方法。

A[] arr = elements.toArray();

返回一个由“B”类组成的数组,“new B[]”是该数组的表示形式。 这个数组的问题在于,虽然编译器允许您通过添加“new A()”元素来编辑其内容,但您会得到“ArrayStoreException”,因为“B []”数组只能容纳类“B”或其子类的元素,以确保所有元素都支持“B”的接口,但“A”的实例可能没有所有方法/字段。“B”的解决方案并不完美。
最好的解决办法是明确告诉“toArray()”应该返回什么类型的数组,通过将此类型作为方法参数传递。
String[] arr = list.toArray(new String[list.size()]);

或者

String[] arr = list.toArray(new String[0]); //if size of array is smaller then list it will be automatically adjusted.

3
这一切都是正确的,但问题的根源比一般的类型更深/更久远。toArray 方法自集合框架推出以来就一直表现如此。它比泛型早了几年。 (你可以说,泛型并没有解决这个问题...) - Stephen C
@StephenC 没错,我只关注了泛型问题,忘记了这个工具并不总是集合框架的一部分。我会尝试重写这个答案来包含你的信息,但现在还做不到。如果你有时间,请随意编辑它。如果你认为它不属于这个问题,请随意给它投反对票 :) - Pshemo

22
正确的做法是:
String[] stockArr = stock_list.toArray(new String[stock_list.size()]);

我想补充其他优秀答案并解释如何使用Javadocs来回答您的问题。

toArray()(没有参数)的Javadoc在这里。正如您所看到的,该方法返回一个Object[]而不是您列表的运行时类型的数组String[]

public Object[] toArray()

返回包含此集合中所有元素的数组。如果集合对其迭代器返回的元素顺序有任何保证,则此方法必须以相同的顺序返回元素。返回的数组将是“安全”的,因为集合不维护对它的任何引用。(换句话说,即使集合由数组支持,此方法也必须分配一个新的数组)。因此,调用者可以自由修改返回的数组。

不过,在该方法的下面,还有toArray(T[] a)Javadoc。正如您所看到的,该方法返回一个T[],其中T是您传入的数组的类型。起初,这似乎是您要寻找的,但不清楚您为什么要传入一个数组(您是在添加到它中,仅使用它作为类型等)。文档明确指出传递的数组的目的实际上是定义要返回的数组类型(这正是您的用例):

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

返回包含此集合中所有元素的数组;返回数组的运行时类型与指定数组的类型相同。 如果集合适合指定的数组,则将其返回。否则,将分配一个新数组,该数组的运行时类型为指定数组的类型,并且大小为此集合的大小。如果集合适合带有多余空间的指定数组(即,数组具有比集合更多的元素),则紧接在集合结束后的数组中的元素将设置为 null。仅当调用者知道集合不包含任何null元素时,才能确定集合的长度。

如果此集合保证其迭代器返回其元素的顺序,那么此方法必须以相同的顺序返回元素。

这个实现会检查数组是否足够大来容纳集合;如果没有,它将使用反射分配具有正确大小和类型的新数组。然后,它遍历集合,将每个对象引用存储在连续元素的下一个位置的数组中,从元素0开始。如果数组大于集合,则在集合末尾后的第一个位置上存储null。

当然,要真正理解这两种方法的区别,需要先掌握泛型(如其他答案中所述)。尽管如此,如果您首先查看Javadocs,通常会找到答案,然后自己看看还需要学习什么(如果确实需要的话)。

还要注意,在这里阅读Javadocs可以帮助您了解传入的数组的结构应该是什么样的。虽然它可能实际上并不重要,但是您不应该像这样传入一个空数组:

String [] stockArr = stockList.toArray(new String[0]);  

因为根据文档,这个实现会检查数组是否足够大以容纳集合;如果不够大,它会使用反射来分配一个正确大小和类型的新数组。 在你可以很容易地传递大小的情况下,创建新数组的额外开销是不必要的。
通常情况下,Javadocs 会为您提供丰富的信息和指导。
等等,什么是反射?

Object[] obj = stock_list.toArray(); 将股票列表转换为对象数组:Object[] obj = stock_list.toArray(); - Quan Nguyen
2
@QuanNguyen:你在说什么?你想表达什么观点? - ToolmakerSteve
5
toArray(new String[0])更快:http://shipilev.net/blog/2016/arrays-wisdom-ancients/(说明:这是一篇关于Java编程语言中数组转换的技巧的博客文章,作者认为使用toArray(new String[0])比其他方式更快。) - ZhekaKozlov

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