为什么我在尝试从List中删除元素时会收到UnsupportedOperationException异常?

617

我有这段代码:

public static String SelectRandomFromTemplate(String template,int count) {
   String[] split = template.split("|");
   List<String> list=Arrays.asList(split);
   Random r = new Random();
   while( list.size() > count ) {
      list.remove(r.nextInt(list.size()));
   }
   return StringUtils.join(list, ", ");
}

我得到了这个:

06-03 15:05:29.614: ERROR/AndroidRuntime(7737): java.lang.UnsupportedOperationException
06-03 15:05:29.614: ERROR/AndroidRuntime(7737):     at java.util.AbstractList.remove(AbstractList.java:645)

这应该是正确的方式吗?Java.15


1
使用 LinkedList。 - Lova Chittumuri
1
这个错误发生是因为我试图修改 collection.unmodifiablelist 列表。 - beginner
17个回答

1210

你的代码存在一些问题:

关于Arrays.asList返回固定大小列表的问题

从API中可以看到:

Arrays.asList: 返回由指定数组支持的固定大小列表。

您无法addremove其中的元素。 也就是说,您不能结构性地修改List

修复方法

创建一个LinkedList,它支持更快的remove操作。

List<String> list = new LinkedList<String>(Arrays.asList(split));

关于使用正则表达式的 split 方法

根据API:

String.split(String regex): 使用给定的正则表达式拆分此字符串。

| 是一个正则表达式的元字符;如果你想要以字面上的 | 字符进行拆分,你必须将它转义为 \|,在Java字符串文字中是"\\|"

修复:

template.split("\\|")

有关更好的算法

不要使用随机索引逐个调用remove函数,最好的方法是在指定范围内生成足够数量的随机数,并使用listIterator()遍历List一次,在适当的索引上调用remove()函数。stackoverflow上有关于如何在给定范围内生成随机但不同的数字的问题。

通过这种方式,您的算法复杂度将为O(N)


7
@Pentium: 还有一件事:你不应该每次都创建一个新的 Random 实例。把它设为静态字段,只需要进行一次种子化。 - polygenelubricants
7
链表真的更快吗?链表和数组列表都具有O(n)的删除操作时间复杂度:在大多数情况下,直接使用数组列表通常更好。 - gengkev
3
这里有来自Ryan的性能测试图表。LinkedList在删除方面更快。 - torno
@Alexander 是的,对于数组也可以这么说。 删除元素的第一步是遍历n次,直到找到该元素,除非它是一个映射或表,否则您不能跳过此步骤。 然而,在链表中,删除过程是O(1),因为一旦删除节点,其引用节点只需更改其引用到下一个现有节点。而在数组中,每当从数组中删除一个元素后,您需要将每个元素向左移动n次。 - 6rchid
1
@drmrbrewer,您需要使用类的类型参数。可以使用new LinkedList<String>(...)或者new LinkedList<>(...)(让编译器自动推断类型参数)。只使用new LinkedList(...)会创建一个“原始”的(未参数化的)链表,应该避免使用。 - Paŭlo Ebermann
显示剩余6条评论

178

这个问题曾经让我吃过不少亏。 Arrays.asList 创建的是一个不可修改的列表。 从 Javadoc 中可以看到:返回一个由指定数组支持的固定大小列表。

创建一个具有相同内容的新列表:

newList.addAll(Arrays.asList(newArray));

这样做会产生一些额外的垃圾,但你将能够对其进行改变。


8
细节问题,但你没有“包装”原始列表,而是创建了一个全新的列表(这就是为什么它起作用的原因)。 - Jack Leow
是的,我在我的JUnit测试用例中使用了Arrays.asList(),然后将其存储在我的map中。 我修改了代码,将传入的列表复制到我的ArrayList中。 - cs94njw
你的解决方案在我的情况下不起作用,但感谢你的解释。你提供的知识帮助我找到了解决方案。 - SMBiggs
这也适用于使用 new ArrayList<>(Arrays.asList(...)) - Donald Duck

77

可能是因为你正在使用不可修改的包装器.

更改此行代码:

List<String> list = Arrays.asList(split);

到这一行:

List<String> list = new LinkedList<>(Arrays.asList(split));

11
Arrays.asList() 不是一个不可修改的包装器。 - Dimitris Andreou
@polygenelubricants:看起来你混淆了“unmodifiable”和“immutable”。 “unmodifiable” 的意思是“可修改的,但不可结构化”。 - Roman
3
我刚试着创建了一个unmodifiableList的包装器,并尝试进行一次set操作,但是它抛出了UnsupportedOperationException异常。我相当确定Collections.unmodifiable*实际上意味着完全不可变性,而不仅仅是结构上的不可变性。 - polygenelubricants
1
阅读这些评论7年后,我允许自己指出这个链接:https://dev59.com/-Gox5IYBdhLWcg3w6odg 可能会解决在此处讨论的不可变和不可修改之间的区别。 - Nathan Ripert
是的,Array.asList不是一个不可修改的包装器,而“可修改但非结构性”的确不同于“不可修改”。 - Paŭlo Ebermann

26
< p > Arrays.asList() 返回的列表可能是不可变的。 你可以试试。

List<String> list = new ArrayList<>(Arrays.asList(split));

1
他正在删除,ArrayList不是最好的数据结构来删除其值。LinkedList更适合他的问题。 - Roman
4
LinkedList 的使用有误。因为他是通过索引访问元素,所以LinkedList会花费大量时间通过迭代查找元素。请参考我的回答,使用ArrayList更好。 - Dimitris Andreou

16

我认为替换:

List<String> list = Arrays.asList(split);

随着

List<String> list = new ArrayList<String>(Arrays.asList(split));
解决了这个问题。

7
问题在于你正在使用Arrays.asList()方法创建具有固定长度的List,这意味着返回的List是固定大小的,我们无法添加/删除元素。
请参阅下面的代码块:
由于这是由asList()创建的迭代列表,因此删除和添加都不可能,它是一个固定的数组。
List<String> words = Arrays.asList("pen", "pencil", "sky", "blue", "sky", "dog"); 
for (String word : words) {
    if ("sky".equals(word)) {
        words.remove(word);
    }
}   

这将很好地工作,因为我们正在使用一个新的ArrayList,可以在迭代时进行修改。
List<String> words1 = new ArrayList<String>(Arrays.asList("pen", "pencil", "sky", "blue", "sky", "dog"));
for (String word : words) {
    if ("sky".equals(word)) {
        words.remove(word);
    }
}

你好,这个答案基本上是正确的,但似乎没有为已经提供了10年的现有答案/解决方案增加任何重要价值。 - Nowhere Man

6

刚刚阅读了asList方法的JavaDoc:

返回指定数组中的对象的{@code List}。{@code List}的大小不能被修改,即添加和删除不受支持,但可以设置元素。设置元素会修改底层数组。

这是来自Java 6的内容,但看起来在Android Java中也是一样的。

编辑

结果列表的类型是Arrays.ArrayList,它是Arrays.class中的一个私有类。实际上,它只是您使用Arrays.asList传递的数组上的List视图。因此,如果更改数组,则列表也会更改。由于数组不可调整大小,因此必须不支持删除和添加操作。


4
我有另一个解决方案:
List<String> list = Arrays.asList(split);
List<String> newList = new ArrayList<>(list);

newList 上工作 ;)


4

替换

List<String> list=Arrays.asList(split);

为了

List<String> list = New ArrayList<>();
list.addAll(Arrays.asList(split));

或者

List<String> list = new ArrayList<>(Arrays.asList(split));

或者

List<String> list = new ArrayList<String>(Arrays.asList(split));

或者(更适合删除元素)
List<String> list = new LinkedList<>(Arrays.asList(split));

4

Arrays.asList() 返回的列表不允许改变其大小(请注意,这与“不可修改”不同)。

您可以使用 new ArrayList<String>(Arrays.asList(split)); 创建一个真正的副本,但是考虑到您要做什么,这里有一个额外的建议(您在下面有一个 O(n^2) 算法)。

您想从列表中删除 list.size() - count (我们称之为 k)个随机元素。只需选择相同数量的随机元素并将它们交换到列表的末尾 k 个位置,然后删除整个范围(例如,在该范围上使用 subList() 和 clear())。这将使其成为一个精简而高效的 O(n) 算法(O(k) 更为准确)。

更新:如下所述,如果元素是无序的,则此算法才有意义,例如,如果 List 表示 Bag。另一方面,如果 List 有有意义的顺序,则此算法将不保留它(polygenelubricants' 算法会保留它)。

更新2: 回过头来看,更好的(线性,维护顺序,但具有 O(n) 随机数)算法可能是这样的:

LinkedList<String> elements = ...; //to avoid the slow ArrayList.remove()
int k = elements.size() - count; //elements to select/delete
int remaining = elements.size(); //elements remaining to be iterated
for (Iterator i = elements.iterator(); k > 0 && i.hasNext(); remaining--) {
  i.next();
  if (random.nextInt(remaining) < k) {
     //or (random.nextDouble() < (double)k/remaining)
     i.remove();
     k--;
  }
}

1
算法加1,尽管OP说只有10个元素。使用随机数和ArrayList的好方法。比我的建议简单得多。我认为这将导致元素重新排序。 - polygenelubricants

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