子列表只是原列表的一个视图
List接口提供了subList方法,其作用是返回一个列表的子列表,这与String类的subString有点类似,但它们的功能是否相同呢?我们来看如下代码:
package deep;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
// 定义一个包含两个字符串的列表
List<String> c = new ArrayList<String>();
c.add("A");
c.add("B");
// 构造一个包含c列表的字符串列表
List<String> c1 = new ArrayList<String>(c);
// subList生成与c相同的列表
List<String> c2 = c.subList(0, c.size());
// c2增加一个元素
c2.add("C");
System.out.println("c == c1? " + c.equals(c1));
System.out.println("c == c2? " + c.equals(c2));
System.out.println(c);
}
}
运行结果:
c == c1? false
c == c2? true
[A, B, C]
c2是通过subList方法从c列表中生成的一个子列表,然后c2又增加了一个元素,可为什么增加了一个元素还相等呢?我们来看subList的源码:
public List<E> subList(int fromIndex, int toIndex) {
return new SubList<>(this, fromIndex, toIndex);
}
class SubList<E> extends AbstractList<E> {
//原始列表
private final AbstractList<E> l;
//偏移量
private final int offset;
private int size;
//构造函数,注意list参数就是我们的原始列表
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
//传递原始列表
l = list;
offset = fromIndex;
//子列表的长度
size = toIndex - fromIndex;
this.modCount = l.modCount;
}
public E set(int index, E element) {
rangeCheck(index);
checkForComodification();
return l.set(index+offset, element);
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return l.get(index+offset);
}
public int size() {
checkForComodification();
return size;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
checkForComodification();
l.add(index+offset, element);
this.modCount = l.modCount;
size++;
}
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = l.remove(index+offset);
this.modCount = l.modCount;
size--;
return result;
}
protected void removeRange(int fromIndex, int toIndex) {
checkForComodification();
l.removeRange(fromIndex+offset, toIndex+offset);
this.modCount = l.modCount;
size -= (toIndex-fromIndex);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
int cSize = c.size();
if (cSize==0)
return false;
checkForComodification();
l.addAll(offset+index, c);
this.modCount = l.modCount;
size += cSize;
return true;
}
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
return new ListIterator<E>() {
private final ListIterator<E> i = l.listIterator(index+offset);
public boolean hasNext() {
return nextIndex() < size;
}
public E next() {
if (hasNext())
return i.next();
else
throw new NoSuchElementException();
}
public boolean hasPrevious() {
return previousIndex() >= 0;
}
public E previous() {
if (hasPrevious())
return i.previous();
else
throw new NoSuchElementException();
}
public int nextIndex() {
return i.nextIndex() - offset;
}
public int previousIndex() {
return i.previousIndex() - offset;
}
public void remove() {
i.remove();
SubList.this.modCount = l.modCount;
size--;
}
public void set(E e) {
i.set(e);
}
public void add(E e) {
i.add(e);
SubList.this.modCount = l.modCount;
size++;
}
};
}
通过阅读这段代码,我们就非常清楚subList方法的实现原理了;它返回的SubList类也是AbstractList的子类,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身并没有生成一个数组或是链表,也就是子列表自是原列表的一个视图(View),所有的修改动作都反应在了原列表上。所以最后打印的c为[A, B, C]。
推荐使用subList处理局部列表
我们来看这样一个简单的需求:一个列表有20个元素,现在要删除索引位置为6-10的元素。代码如下:
package deep;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>(20);
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
list.add(8);
list.add(9);
list.add(10);
list.add(11);
list.add(12);
list.add(13);
list.add(14);
list.add(15);
list.add(16);
list.add(17);
list.add(18);
list.add(19);
int offset = 0;
for (int i = 6; i <= 10; ++i) {
if (i < list.size()) {
list.remove(i - offset);
++offset;
}
}
System.out.println(list);
}
}
运行结果:
[0, 1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 16, 17, 18, 19]
或者:
int offset = 0;
for (int i = 0, size = list.size(); i < size; ++i) {
if (i > 5 && i <= 10) {
list.remove(i - offset);
++offset;
}
}
不过,还有没有其他方式呢?有没有“one-lining”一行代码就解决问题的方式呢?
有,直接使用ArrayList的removeRange方法不就可以了吗?等等,好像不可能呀,虽然JDK上有此方法,但是它有protected关键字修饰着,不能直接使用,那怎么办?看看如下代码:
package deep;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>(20);
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
list.add(8);
list.add(9);
list.add(10);
list.add(11);
list.add(12);
list.add(13);
list.add(14);
list.add(15);
list.add(16);
list.add(17);
list.add(18);
list.add(19);
// 删除指定范围的元素
list.subList(6, 11).clear();
System.out.println(list);
}
}
运行结果:
[0, 1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 16, 17, 18, 19]
因为subList上的所有操作都是在原始列表上进行的,那我们就用subList先取出一个子列表,然后清空。因为subList返回的List是原始列表的一个视图,删除这个视图中的所有元素,最终就会反映到原始字符串上,那么一行代码即解决问题了。
生成子列表后不要再操作原列表
在subList执行完后,如果修改了原列表的内容会怎样呢?视图是否会改变呢?如果是数据库视图,表数据变更了,视图当然会变了,至于subList生成的视图是否会改变,看如下代码:
package deep;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
List<String> subList = list.subList(0, 2);
// 原字符串增加一个元素
list.add("D");
System.out.println("原列表长度:" + list.size());
System.out.println("子列表长度:" + subList.size());
}
}
运行结果:
原列表长度:4
Exception in thread “main” java.util.ConcurrentModificationException
at java.util.ArrayList
SubList.checkForComodification(ArrayList.java:1169)atjava.util.ArrayList
SubList.size(ArrayList.java:998)
at deep.Client.main(Client.java:17)
什么?居然是subList的size方法出现了异常,而且还是并发修改异常?这没道理呀,这里根本就没有多线程操作,何来并发修改呢?这个问题很容易回答,那是因为subList取出的列表是原列表的一个视图,原数据集(代码中的list变量)修改了,但是subList取出的子列表不会重新生成一个新列表(这点与数据库视图是不相同的),后面在对子列表继续操作时,就会检测到修改计数器与预期的不相同,于是就抛出了并发修改异常。
出现这个问题的最终原因还是在子列表提供的size方法的检查上,还记得上面几个例子中经常提到的修改计数器吗?原因就在这里,我们来看看size的源代码:
public int size() {
checkForComodification();
return this.size;
}
其中的checkForComodification方法就是用于检测是否并发修改的,代码如下:
private void checkForComodification() {
//判断当前修改计算器是否与子列表生成时一致
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
this.modCount是在SubList子列表的构造函数中赋值的,其值等于生成子列表时的修改次数,如下:
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
//将原始列表的修改次数赋值给subList
this.modCount = l.modCount;
}
因此在生成子列表后再修改原始列表,ArrayList.this.modCount 必然比 this.modCount大1,不再保持相等了,于是也就抛出了ConcurrentModificationException异常。
subList的其他方法也会检测修改计数器,例如set、get、add、等方法,若生成子列表后,再修改原列表,这些方法也会抛出ConcurrentModificationException异常。
对于子列表操作,因为视图是动态生成的,生成子列表后再操作原列表,必然会导致“视图”的不稳定,最有效的方法就是通过Collections.unmodifiableList方法设置列表为只读状态,代码如下:
package deep;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class Client {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
List<String> subList = list.subList(0, 2);
// /设置原列表为只读状态
list = Collections.unmodifiableList(list);
// 原字符串增加一个元素
list.add("D");
System.out.println("原列表长度:" + list.size());
System.out.println("子列表长度:" + subList.size());
}
}
这在团队编码中特别有用,防御式编程就是教我们如此做的。
这里还有一个问题,数据库的一张表可以有很多视图,我们的List也可以有多个视图,也就是可以有多个子列表,但问题是只要生成的子列表多于一个,则任何一个子列表就都不能修改了,否则就会抛出ConcurrentModificationException异常。