2

谁能解释为什么subList()不表现为subSet()方法,并抛出ConcurrentModificationException,而subSet不是。这两种方法都创建了一个支持集合,所以可能subList() 方法设计者创建了这个方法依赖于一个不可修改的原始列表,但是如果所有支持集合都具有相同的行为(如subSet() )会不会更好?

// 代码

public class ConcurrentModificationException {
public static void main(String[] args) {
    String[] array = {"Java","Python","Pearl","Ada","Javascript","Go","Clojure"};
    subListEx(array);
    subSetEx(array);
}

private static void subListEx(String[] array) {
    List<String> l = new ArrayList<String>(Arrays.asList(array));
    List<String> l2 = l.subList(2, 4);
    System.out.println(l.getClass().getName());

    // l.add("Ruby"); // ConcurrentModificationException
    // l.remove(2); // ConcurrentModificationException
    l2.remove("Ada"); // OK
    for (String s:l) { System.out.print(s+", "); } 
    System.out.println();
    for (String s:l2) { System.out.print(s+", "); }
}

private static void subSetEx(String[] array) {
    SortedSet<String> s1 = new TreeSet<String>(Arrays.asList(array));
    SortedSet<String> s2 = s1.subSet("Java", "Python");
    s1.remove("Ada");

    for (String s:s1) { System.out.print(s+", "); } 
    System.out.println();
    for (String s:s2) { System.out.print(s+", "); }
}}

提前致谢!

4

4 回答 4

3

已经很清楚,行为是按照记录的。但我认为您的主要问题是为什么ArrayListand的行为不同TreeSet。好吧,这与数据在两个集合中的内部存储方式有关。

一个内部使用一个数组来存储数据,随着大小的动态增加ArrayList而重新调整大小。ArrayList现在,当您创建subList给定列表的 a 时,具有指定索引的原始列表与subList. 因此,在原始列表中完成的任何结构更改(破坏原始数组的索引)都会使作为子列表一部分存储的索引毫无意义。这就是为什么在方法的情况下不允许任何结构更改的原因ArrayList#subList。subList 方法返回一个在inner类中命名SubList的类的实例,ArrayList如下所示:

private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;
    private final int parentOffset;
    private final int offset;
    int size;

    SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
        this.parent = parent;
        this.parentOffset = fromIndex;
        this.offset = offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = ArrayList.this.modCount;
    }

如您所见,SubList包含对原始列表的引用。这parentOffset只不过是subList您正在创建的起始索引。现在修改原始列表list可能会更改fromIndex原始列表中的值,但不会更改SubList. 在这种情况下,parentOffsetSubList类和fromIndex原始列表中,将指向不同的数组元素。也有可能在某些时候原始数组变得足够短以使存储在 中的索引无效SubList并使其OutOfRange. 这当然是不可取的,并且在对原始列表进行此类结构更改时,返回的子列表的语义被认为是未定义的。

另一方面, aTreeSet将其数据内部存储在 a 中TreeMap。现在由于 a 中没有索引的这种概念Map,因此不存在索引分解的问题。AMap只不过是键值对的映射。创建SubSet涉及创建原始Map. 修改原始文件Set只需要使相应的键值映射失效,从而将更改传播到subMapsubSet.

于 2013-08-21T21:07:57.623 回答
1

合同List.subList(int, int)涵盖了这一点。这是相关部分,强调我的。

返回的列表由该列表支持,因此返回列表中的非结构性更改会反映在该列表中,反之亦然。
...
如果后备列表(即此列表)以除通过返回列表之外的任何方式进行结构修改,则此方法返回的列表的语义变得未定义。(结构修改是改变这个列表的大小,或者以其他方式扰乱它,使得正在进行的迭代可能会产生不正确的结果。)

在您的示例中,您正在对支持列表进行结构更改,因此结果未定义。

于 2013-08-21T20:52:26.113 回答
0

文档很清楚: aList.subList()返回列表的视图。返回的对象原始列表支持,或者您应该假设。

Set关于s的文档也非常清楚:

此类的迭代器方法返回的迭代器是快速失败的:如果在创建迭代器后的任何时间修改集合,除了通过迭代器自己的 remove 方法之外,迭代器将抛出 ConcurrentModificationException。

为什么用 s 产生问题List比用Set's 更容易?因为在 a 中Set删除元素时,效果非常明显:该元素不再在集合中,无论是对于集合还是任何子集。与插入操作相同。但是,当您从 a 中删除一个元素时List,这是否意味着子列表会相应地调整其索引(可能还有大小),或者它们只是通过从右侧或左侧添加元素来保持其大小?而且,在第二种情况下,如果支持列表变得太短怎么办?行为太复杂而无法定义。而且对实现也有太多限制。

示例:一个列表是 [a,b,c,d],一个子列表是 list.subList(1,3) ([b,c])。 a从列表中删除。列表中的效果很明显。但是 sublist 是保持 [b,c] (将其范围更改为 (0,2)),还是变为 [c,d] (保留其范围在 (1,3) 中)?

示例:一个列表是 [a,b,c,d],一个子列表是 list.subList(1,3) ([b,c])。 b从列表中删除。同样,列表中的效果很明显。但是 sublist 会变成 [c] (1,2)、[a,c] (0,2) 还是 [c,d] (1,3)?

于 2013-08-21T20:53:54.400 回答
0

两者的 Javadoc 对此都非常清楚:

ArrayList.sublist()

如果后备列表(即此列表)以除通过返回列表之外的任何方式进行结构修改,则此方法返回的列表的语义将变为未定义。(结构修改是改变这个列表的大小,或者以其他方式扰乱它,使得正在进行的迭代可能产生不正确的结果。)

因此,虽然不能保证您会收到 a ConcurrentModificationException,但这并不是不可能的。如果您修改后备列表,则行为未定义。

然而...

TreeSet.subSet()

返回此集合部分的视图,其元素范围从 fromElement(包括)到 toElement(不包括)。(如果 fromElement 和 toElement 相等,则返回的集合为空。)返回的集合由该集合支持,因此返回集合中的更改会反映在该集合中,反之亦然。返回的集合支持该集合支持的所有可选集合操作。

这里没有警告,对支持集的修改很好,没有问题。

于 2013-08-21T20:55:18.950 回答