我們經(jīng)常使用 subString 方法來(lái)對(duì) String 對(duì)象進(jìn)行分割處理,同時(shí)我們也可以使用 subList、subMap、subSet 來(lái)對(duì) List、Map、Set 進(jìn)行分割處理,但是這個(gè)分割存在某些瑕疵。
首先我們先看如下實(shí)例:
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer> ();
list1.add(1);
list1.add(2);
//通過(guò)構(gòu)造函數(shù)新建一個(gè)包含list1的列表 list2
List<Integer> list2 = new ArrayList<Integer>(list1);
//通過(guò)subList生成一個(gè)與list1一樣的列表 list3
List<Integer> list3 = list1.subList(0, list1.size());
//修改list3
list3.add(3);
System.out.println("list1 == list2:" + list1.equals(list2));
System.out.println("list1 == list3:" + list1.equals(list3));
}
這個(gè)例子非常簡(jiǎn)單,無(wú)非就是通過(guò)構(gòu)造函數(shù)、subList 重新生成一個(gè)與 list1 一樣的 list,然后修改 list3,最后比較 list1 == list2?、list1 == list3?。按照我們常規(guī)的思路應(yīng)該是這樣的:因?yàn)?list3 通過(guò) add 新增了一個(gè)元素,那么它肯定與 list1 不等,而 list2 是通過(guò) list1 構(gòu)造出來(lái)的,所以應(yīng)該相等,所以結(jié)果應(yīng)該是:
list1 == list2:true
list1 == list3: false
首先我們先不論結(jié)果的正確與否,我們先看 subList 的源碼:
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
subListRangeCheck 方式是判斷 fromIndex、toIndex 是否合法,如果合法就直接返回一個(gè) subList 對(duì)象,注意在產(chǎn)生該 new 該對(duì)象的時(shí)候傳遞了一個(gè)參數(shù) this ,該參數(shù)非常重要,因?yàn)樗碇?list。
/**
* 繼承AbstractList類(lèi),實(shí)現(xiàn)RandomAccess接口
*/
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent; //列表
private final int parentOffset;
private final int offset;
int size;
//構(gòu)造函數(shù)
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;
}
//set方法
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
//get方法
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
//add方法
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
//remove方法
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
}
該 SubLsit 是 ArrayList 的內(nèi)部類(lèi),它與 ArrayList 一樣,都是繼承 AbstractList 和實(shí)現(xiàn) RandomAccess 接口。同時(shí)也提供了 get、set、add、remove 等 list 常用的方法。但是它的構(gòu)造函數(shù)有點(diǎn)特殊,在該構(gòu)造函數(shù)中有兩個(gè)地方需要注意:
1、this.parent = parent;而 parent 就是在前面?zhèn)鬟f過(guò)來(lái)的 list,也就是說(shuō) this.parent 就是原始 list 的引用。
2、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同時(shí)在構(gòu)造函數(shù)中它甚至將 modCount(fail-fast機(jī)制)傳遞過(guò)來(lái)了。
我們?cè)倏?get 方法,在 get 方法中 return ArrayList.this.elementData(offset + index);這段代碼可以清晰表明 get 所返回就是原列表 offset + index位置的元素。同樣的道理還有 add 方法里面的:
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
remove 方法里面的
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
誠(chéng)然,到了這里我們可以判斷 subList 返回的 SubList 同樣也是 AbstractList 的子類(lèi),同時(shí)它的方法如 get、set、add、remove 等都是在原列表上面做操作,它并沒(méi)有像 subString 一樣生成一個(gè)新的對(duì)象。所以 subList 返回的只是原列表的一個(gè)視圖,它所有的操作最終都會(huì)作用在原列表上。
那么從這里的分析我們可以得出上面的結(jié)果應(yīng)該恰恰與我們上面的答案相反:
list1 == list2:false
list1 == list3:true
Java 細(xì)節(jié)(3.1):subList 返回的只是原列表的一個(gè)視圖,它所有的操作最終都會(huì)作用在原列表上
從上面我們知道 subList 生成的子列表只是原列表的一個(gè)視圖而已,如果我們操作子列表它產(chǎn)生的作用都會(huì)在原列表上面表現(xiàn),但是如果我們操作原列表會(huì)產(chǎn)生什么情況呢?
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer>();
list1.add(1);
list1.add(2);
//通過(guò)subList生成一個(gè)與list1一樣的列表 list3
List<Integer> list3 = list1.subList(0, list1.size());
//修改list3
list1.add(3);
System.out.println("list1'size:" + list1.size());
System.out.println("list3'size:" + list3.size ());
}
該實(shí)例如果不產(chǎn)生意外,那么他們兩個(gè) list 的大小都應(yīng)該都是 3,但是偏偏事與愿違,事實(shí)上我們得到的結(jié)果是這樣的:
list1'size:3
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(Unknown Source)
at java.util.ArrayList$SubList.size(Unknown Source)
at com.chenssy.test.arrayList.SubListTest.main(SubListTest.java:17)
list1 正常輸出,但是 list3 就拋出 ConcurrentModificationException 異常,看過(guò)我另一篇博客的同仁肯定對(duì)這個(gè)異常非常,fail-fast?不錯(cuò)就是 fail-fast 機(jī)制,在 fail-fast 機(jī)制中,LZ 花了很多力氣來(lái)講述這個(gè)異常,所以這里 LZ 就不對(duì)這個(gè)異常多講了(更多請(qǐng)點(diǎn)這里:Java 提高篇(三四)—–fail-fast 機(jī)制 )。我們?cè)倏?size 方法:
public int size() {
checkForComodification();
return this.size;
}
size 方法首先會(huì)通過(guò) checkForComodification 驗(yàn)證,然后再返回this.size。
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
該方法表明當(dāng)原列表的 modCount 與 this.modCount 不相等時(shí)就會(huì)拋出 ConcurrentModificationException。同時(shí)我們知道 modCount 在 new 的過(guò)程中 “繼承”了原列表 modCount,只有在修改該列表(子列表)時(shí)才會(huì)修改該值(先表現(xiàn)在原列表后作用于子列表)。而在該實(shí)例中我們是操作原列表,原列表的 modCount 當(dāng)然不會(huì)反應(yīng)在子列表的 modCount 上啦,所以才會(huì)拋出該異常。
對(duì)于子列表視圖,它是動(dòng)態(tài)生成的,生成之后就不要操作原列表了,否則必然都導(dǎo)致視圖的不穩(wěn)定而拋出異常。最好的辦法就是將原列表設(shè)置為只讀狀態(tài),要操作就操作子列表:
//通過(guò)subList生成一個(gè)與list1一樣的列表 list3
List<Integer> list3 = list1.subList(0, list1.size());
//對(duì)list1設(shè)置為只讀狀態(tài)
list1 = Collections.unmodifiableList(list1);
Java 細(xì)節(jié)(3.2):生成子列表后,不要試圖去操作原列表,否則會(huì)造成子列表的不穩(wěn)定而產(chǎn)生異常
在開(kāi)發(fā)過(guò)程中我們一定會(huì)遇到這樣一個(gè)問(wèn)題:獲取一堆數(shù)據(jù)后,需要?jiǎng)h除某段數(shù)據(jù)。例如,有一個(gè)列表存在 1000 條記錄,我們需要?jiǎng)h除 100-200 位置處的數(shù)據(jù),可能我們會(huì)這樣處理:
for(int i = 0 ; i < list1.size() ; i++){
if(i >= 100 && i <= 200){
list1.remove(i);
/*
* 當(dāng)然這段代碼存在問(wèn)題,list remove之后后面的元素會(huì)填充上來(lái),
* 所以需要對(duì)i進(jìn)行簡(jiǎn)單的處理,當(dāng)然這個(gè)不是這里討論的 問(wèn)題。
*/
}
}
這個(gè)應(yīng)該是我們大部分人的處理方式吧,其實(shí)還有更好的方法,利用 subList。在前面 LZ 已經(jīng)講過(guò),子列表的操作都會(huì)反映在原列表上。所以下面一行代碼全部搞定:
list1.subList(100, 200).clear();
簡(jiǎn)單而不失華麗?。。。?!
參考資料:編寫(xiě)高質(zhì)量代碼:改善 Java 程序的 151 個(gè)建議