集合遍歷有多種方式,但各種方式執(zhí)行效率上稍有差別,遍歷集合時(shí)刪除元素處理不當(dāng)會(huì)有一些問(wèn)題,這里詳細(xì)匯總一下。
遍歷集合
遍歷集合元素的方式主要有以下幾種:
- 使用一般的for循環(huán)遍歷集合
- 使用for-each循環(huán)遍歷集合
- 使用Iterator迭代器提供的hasNext、next方法遍歷集合
- 使用Java 8 為Iterator接口提供的forEachRemaining默認(rèn)方法遍歷集合
- 使用Java 8 為Iterable接口提供的forEach默認(rèn)方法遍歷集合
- 使用Java 8 提供的流式API遍歷集合
這里以ArrayList為例來(lái)測(cè)試以上幾種方式。
先創(chuàng)建一個(gè)集合元素類。
public class Student {
private int id;
private String name;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public Student(int id, String name) {
this.id = id;
this.name = name;
}
省略getter和setter方法
}
再創(chuàng)建一個(gè)遍歷集合的測(cè)試類:
public class IterateCollectionTest {
private static final int LIST_SIZE = 10000;
private static final int ITERATE_TIMES = 1000;
private static final int CONDITION_NUM = 2;
public static void main(String[] args) {
forIterate();
forEachIterate();
iteratorIterate();
iteratorForEachRemainingMethodIterate();
forEachMethodIterate();
streamIterate();
}
/**
* 使用一般的for循環(huán)遍歷集合元素
*/
public static void forIterate() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "student" + i));
}
long millionSeconds = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
Student student = list.get(i);
if (student.getId() % CONDITION_NUM == 0) {
for (int j = 0; j < ITERATE_TIMES; ) {
j++;
}
}
}
System.out.println("forIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用for-each循環(huán)遍歷集合元素
*/
public static void forEachIterate() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "student" + i));
}
long millionSeconds = System.currentTimeMillis();
for (Student student : list) {
if (student.getId() % CONDITION_NUM == 0) {
for (int j = 0; j < ITERATE_TIMES; ) {
j++;
}
}
}
System.out.println("forEachIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用Iterator迭代器來(lái)遍歷結(jié)合元素
*/
public static void iteratorIterate() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "student" + i));
}
long millionSeconds = System.currentTimeMillis();
Iterator<Student> iterator = list.iterator();
while (iterator.hasNext()) {
Student student = iterator.next();
if (student.getId() % CONDITION_NUM == 0) {
for (int j = 0; j < ITERATE_TIMES; ) {
j++;
}
}
}
System.out.println("iteratorIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用Java 8 為Iterator接口提供的forEachRemaining默認(rèn)方法來(lái)遍歷集合元素
* 該方法是使用Iterator的hasNext、next方法,以及函數(shù)式接口Consumer實(shí)現(xiàn)的
* 該方法可依據(jù)指定的迭代順序(如果指定了的話)來(lái)遍歷處理集合元素,所以效率較低
* 這里可以使用Java 8新增的Lambda表達(dá)式來(lái)簡(jiǎn)化編程
*/
public static void iteratorForEachRemainingMethodIterate() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "student" + i));
}
long millionSeconds = System.currentTimeMillis();
list.iterator().forEachRemaining(student -> {
if (student.getId() % CONDITION_NUM == 0) {
for (int j = 0; j < ITERATE_TIMES; ) {
j++;
}
}
});
System.out.println("iteratorForEachRemainingMethodIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用Java 8 為Iterable接口提供的forEach默認(rèn)方法來(lái)遍歷集合元素
* 該方法使用for-each循環(huán)來(lái)實(shí)現(xiàn)遍歷,在這幾種方法中速度最快
* 這里可以使用Java 8新增的Lambda表達(dá)式來(lái)簡(jiǎn)化編程
*/
public static void forEachMethodIterate() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "student" + i));
}
long millionSeconds = System.currentTimeMillis();
list.forEach(student -> {
if (student.getId() % CONDITION_NUM == 0) {
for (int j = 0; j < ITERATE_TIMES; ) {
j++;
}
}
});
System.out.println("forEachMethodIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用Java 8 提供的流式API來(lái)遍歷集合
* 流式API將集合轉(zhuǎn)換成流,遍歷速度僅次于Iterable接口提供的forEach默認(rèn)方法。
*/
public static void streamIterate() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "student" + i));
}
long millionSeconds = System.currentTimeMillis();
list.stream().forEach(student -> {
if (student.getId() % CONDITION_NUM == 0) {
for (int j = 0; j < ITERATE_TIMES; ) {
j++;
}
}
});
System.out.println("streamIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
}
在我的i5-6500
CPU電腦上多次測(cè)試取遍歷操作耗時(shí)的平均值,得出這幾種方法的遍歷速度從快到慢依次為:
forEachMethodIterate > streamIterate > iteratorIterate > forEachIterate > forIterate > iteratorForEachRemainingMethodIterate
所以如果遍歷一個(gè)集合中元素,建議優(yōu)先使用Java 8為Iterable接口提供的forEach默認(rèn)方法。如果你還未使用Java 8,則建議優(yōu)先使用Iterator接口的hasNex和next方法來(lái)實(shí)現(xiàn)遍歷。
遍歷集合時(shí)動(dòng)態(tài)刪除集合中的元素
遍歷集合刪除集合元素的方式有以下幾種:
- 使用一般的for循環(huán)遍歷刪除,同時(shí)手動(dòng)處理因刪除操作導(dǎo)致集合大小變化的問(wèn)題
- 使用一般的for循環(huán)逆序遍歷刪除,不用手動(dòng)處理因刪除操作導(dǎo)致集合大小變化的問(wèn)題
- 使用官方推薦的Iterator迭代器提供的Iterator.remove方法在遍歷集合時(shí)刪除集合元素
- 使用Java 8新增的removeIf方法在遍歷集合時(shí)刪除集合元素
- 使用Java 8提供的流式API來(lái)篩選元素,然后轉(zhuǎn)換成集合類型
這里以ArrayList為例來(lái)測(cè)試以上幾種方式。
public class RemoveElementInListTest {
private static final int LIST_SIZE = 20000;
private static final int CONDITION_NUM = 2;
public static void main(String[] args) {
// 以下三種方式都不能正常地遍歷刪除
// forEachRemove();
// forEachBreakRemove();
// forRemove();
// 以下幾種方式可以正常地遍歷刪除
forRemoveNoSkipping();
forReverseRemoveNoSkipping();
iteratorRemove();
ifRemove();
streamRemove();
}
/**
* 使用foreach遍歷刪除
* 在第一次循環(huán)時(shí)刪除List中的元素刪除不會(huì)出現(xiàn)問(wèn)題,但繼續(xù)循環(huán)List時(shí)會(huì)報(bào)ConcurrentModificationException
* 從打印的異常信息來(lái)看,forEach循環(huán)集合時(shí)使用了某種內(nèi)部索引器
* 可以使用線程安全的CopyOnWriteArrayList來(lái)代替ArrayList
* 但是當(dāng)List中元素很多時(shí)效率會(huì)大大折扣,還會(huì)造成資源浪費(fèi)
*/
public static void forEachRemove() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
for (Student student : list) {
if (student.getId() % CONDITION_NUM == 0) {
list.remove(student);
}
}
}
/**
* 使用foreach循環(huán)對(duì)List進(jìn)行遍歷刪除,但刪除之后馬上就跳出的就不會(huì)出現(xiàn)異常
* 但這種方式在需要?jiǎng)h除多個(gè)元素的情況下無(wú)法滿足要求
*/
public static void forEachBreakRemove() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
for (Student student : list) {
if (student.getId() % CONDITION_NUM == 0) {
list.remove(student);
break;
}
}
}
/**
* 一般的for循環(huán)遍歷有可能會(huì)遺漏某個(gè)元素,因?yàn)閯h除元素后List的size在變化,元素的索引也在變化
* 比如你循環(huán)到第2個(gè)元素的時(shí)候你把它刪了,接下來(lái)你去訪問(wèn)第3個(gè)元素,實(shí)際上訪問(wèn)到的是原先的第4個(gè)元素
* 當(dāng)訪問(wèn)的元素索引超過(guò)了當(dāng)前的List的size后還會(huì)出現(xiàn)數(shù)組越界的異常,當(dāng)然這里不會(huì)出現(xiàn)這種異常
* 因?yàn)檫@里每遍歷一次都重新獲取一次當(dāng)前List的size
*/
public static void forRemove() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getId() % CONDITION_NUM == 0) {
list.remove(i);
}
}
}
/**
* 手動(dòng)處理一般的for循環(huán)遍歷時(shí)刪除而導(dǎo)致的索引變化就可以安全地刪除
*/
public static void forRemoveNoSkipping() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
long millionSeconds = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getId() % CONDITION_NUM == 0) {
list.remove(i);
// 刪除某個(gè)元素會(huì)導(dǎo)致list的size減1,手動(dòng)使遍歷位置后移一個(gè)位置
// 這樣就不會(huì)漏掉被刪除元素后面的元素
i--;
}
}
System.out.println("forRemoveNoSkipping操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用反向的for循環(huán)遍歷時(shí)刪除就無(wú)需手動(dòng)處理索引變化的問(wèn)題
* 而且因?yàn)閯h除操作而導(dǎo)致的元素移動(dòng)也比正向遍歷要少
*/
public static void forReverseRemoveNoSkipping() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
long millionSeconds = System.currentTimeMillis();
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).getId() % CONDITION_NUM == 0) {
list.remove(i);
}
}
System.out.println("forReverseRemoveNoSkipping操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用Iterator的方式也可以順利刪除和遍歷,不會(huì)有任何問(wèn)題,這才是刪除變量List中元素的正確方式
*/
public static void iteratorRemove() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
Iterator<Student> iterator = list.iterator();
long millionSeconds = System.currentTimeMillis();
while (iterator.hasNext()) {
Student student = iterator.next();
if (student.getId() % CONDITION_NUM == 0) {
iterator.remove();
}
}
System.out.println("iteratorRemove操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 也可以使用Java 8新增的removeIf方法在遍歷時(shí)刪除List中的元素,該方法也使用Iterator了,所以刪除是安全的
*/
public static void ifRemove() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
long millionSeconds = System.currentTimeMillis();
list.removeIf(student -> student.getId() % CONDITION_NUM == 0);
System.out.println("ifRemove操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
/**
* 使用Java 8提供的流式API來(lái)篩選元素和轉(zhuǎn)換成集合類型
* 從Java 8開始,使用流式API遍歷集合是首選的方式,這種方式用于遍歷非常快
* 但由于創(chuàng)建了流,這種方式增大了空間開銷
*/
public static void streamRemove() {
List<Student> list = new ArrayList<>();
for (int i = 1; i <= LIST_SIZE; i++) {
list.add(new Student(i, "Student" + i));
}
long millionSeconds = System.currentTimeMillis();
list.stream()
.filter(student -> student.getId() % CONDITION_NUM == 0)
.collect(Collectors.toList());
System.out.println("streamRemove操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
}
}
在我的電腦上多次測(cè)試取耗時(shí)的平均值,得出這幾種方法的遍歷速度從快到慢依次為:
streamRemove > forReverseRemoveNoSkipping > iteratorRemove > forRemoveNoSkipping > ifRemove
其中,iteratorRemove和forRemoveNoSkipping的測(cè)試結(jié)果很接近,大家可以自行修改集合大小的常量親自測(cè)試,如有問(wèn)題歡迎反饋。
所以如果遍歷一個(gè)集合時(shí)刪除其中的元素,建議優(yōu)先使用Java 8提供的流式API來(lái)篩選集合元素。如果你還未使用Java 8,則建議優(yōu)先使用逆序的一般for循環(huán)來(lái)實(shí)現(xiàn)遍歷時(shí)刪除集合元素。
許多初學(xué)者容易使用上面示例中的前三種方式來(lái)在遍歷集合時(shí)刪除集合元素,但是得不到正確的結(jié)果,原因已經(jīng)在這三種方法的注釋中說(shuō)明了。對(duì)于使用for-each循環(huán)時(shí)拋出ConcurrentModificationException異常的原因可通過(guò)查看ArrayList.remove()方法的源碼來(lái)探明。for-each循環(huán)List集合時(shí)使用了一個(gè)實(shí)現(xiàn)了Iterator接口的ArrayList內(nèi)部類對(duì)象來(lái)實(shí)現(xiàn)遍歷,該內(nèi)部類源碼如下:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
使用for-each遍歷時(shí)調(diào)用該內(nèi)部類的next方法,進(jìn)而調(diào)用該方法中第一行的checkForComodification方法,ConcurrentModificationException異常就是在這個(gè)checkForComodification方法中拋出的:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
當(dāng)我們顯式調(diào)用remove方法來(lái)刪除集合中的元素時(shí)會(huì)修改modCount的值,使其與expectedModCount不一致:
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = l.remove(index+offset);
this.modCount = l.modCount;
size--;
return result;
}
官方教程也有說(shuō)在以下情況中可以使用Iterator來(lái)代替for-each循環(huán):
- 刪除集合元素時(shí)。for-each循環(huán)使用隱藏了迭代器,所以遍歷刪除失敗。
- 并行迭代多個(gè)集合對(duì)象時(shí)。