Java|List.subList 踩坑小记
很久以前在使用 Java 的 List.subList 方法时踩过一个坑,当时记了一条待办,要写一写这事,今天完成它。
我们先来看一段代码:
1 | // 初始化 list 为 { 1, 2, 3, 4, 5 } |
输出是 5
还是 6
?
没踩过坑的我,会回答是 5
,理由是:往一个 List 里加元素,关其它 List 什么事?
而掉过坑的我,口中直呼 666。
好了不绕弯子,我们直接看下 List.subList 方法的注释文档:
1 | /** |
这里面有几个要点:
subList 返回的是原 List 的一个 视图,而不是一个新的 List,所以对 subList 的操作会反映到原 List 上,反之亦然;
如果原 List 在 subList 操作期间发生了结构修改,那么 subList 的行为就是未定义的(实际表现为抛异常)。
第一点好理解,看到「视图」这个词相信大家就都能理解了。我们甚至可以结合 ArrayList 里的 SubList 子类源码进一步看下:
1 | private class SubList extends AbstractList<E> implements RandomAccess { |
可以看到几乎所有的读写操作都是映射到 ArrayList.this、或者 parent(即原 List)上的,包括 size
、add
、remove
、set
、get
、removeRange
、addAll
等等。
第二点,我们在文首的示例代码里加上两句代码看现象:
1 | list.add(0, 0); |
System.out.println
会抛出异常 java.util.ConcurrentModificationException
。
我们还可以试下,在声明 subList 后,如果对原 List 进行元素增删操作,然后再读写 subList,基本都会抛出此异常。
因为 subList 里的所有读写操作里都调用了 checkForComodification()
,这个方法里检验了 subList 和 List 的 modCount
字段值是否相等,如果不相等则抛出异常。
modCount
字段定义在 AbstractList 中,记录所属 List 发生 结构修改 的次数。结构修改 包括修改 List 大小(如 add、remove 等)、或者会使正在进行的迭代器操作出错的修改(如 sort、replaceAll 等)。
好了小结一下,这其实不算是坑,只是 不应该仅凭印象和猜测,就开始使用一个方法,至少花一分钟认真读完它的官方注释文档。