1. 程式人生 > 實用技巧 >正向遍歷和反向遍歷

正向遍歷和反向遍歷

前言

之前搜尋面試題的時候,出現了一個題:一個ArrayList在迴圈過程中刪除,會不會出問題,為什麼?心裡想的答案是肯定會有問題但是又不知道是為什麼,在搜尋到答案後,發現裡面其實並不簡單,所以專門寫篇文章研究一下。

for迴圈正向刪除

先看示例,再解析原因:

  1. public static void main(String[] args){
  2. List<String> list = new ArrayList<String>();
  3. list.add("111");
  4. list.add("222");
  5. list.add("222");
  6. list.add("333");
  7. list.add("444");
  8. list.add("333");
  9. //for迴圈正向迴圈刪除
  10. for (int i = 0;i < list.size();i++){
  11. if (list.get(i).equals("222")){
  12. list.remove(i);
  13. }
  14. }
  15. System.out.println(Arrays.toString(list.toArray()));
  16. }

執行後,輸出結果:

[111, 222, 333, 444, 333]

發現,相鄰的字串“222”沒有刪除,這是為什麼呢?畫圖解釋:

解釋:刪除元素“222”,當迴圈到下標為1的元素的的時候,發現此位置上的元素是“222”,此處元素應該刪除,根據上圖中的元素移動可知,在刪除元素後面的所有元素都要向前移動一個位置,那麼移動之後,原來下標為2的元素“222”,此時下標為1,這是在i = 1,時的迴圈操作,在下一次的迴圈中,i = 2,此時就遺漏了第二個元素“222”。

那麼再做下一個測試,刪除元素“333”,結果將如何?

  1. public static void main(String[] args){
  2. List<String> list = new ArrayList<String>();
  3. list.add("111");
  4. list.add("222");
  5. list.add("222");
  6. list.add("333");
  7. list.add("444");
  8. list.add("333");
  9. //for迴圈正向迴圈刪除
  10. for (int i = 0;i < list.size();i++){
  11. if (list.get(i).equals("333")){
  12. list.remove(i);
  13. }
  14. }
  15. System.out.println(Arrays.toString(list.toArray()));
  16. }

執行結果:

[111, 222, 222, 444]

發現,沒有問題。原理在上一個測試已經說了,就不再贅述。

總結:for迴圈正向刪除,會遺漏連續重複的元素。

for迴圈反向刪除

  1. public static void main(String[] args){
  2. List<String> list = new ArrayList<String>();
  3. list.add("111");
  4. list.add("222");
  5. list.add("222");
  6. list.add("333");
  7. list.add("444");
  8. list.add("333");
  9. //for迴圈反向迴圈刪除
  10. for (int i = list.size() - 1;i >= 0;i--){
  11. if (list.get(i).equals("222")){
  12. list.remove(i);
  13. }
  14. }
  15. System.out.println(Arrays.toString(list.toArray()));
  16. }

執行結果:

[111, 333, 444, 333]

發現,沒有問題。還是畫圖解釋:

反向刪除的時候,迴圈遍歷完了的元素下標才有可能移動(已經遍歷的元素,下標變化了也沒有影響),所以沒有遍歷的下標不會移動,自反向刪除會遍歷到所有的元素,正向會跳過一些元素。

總結:反向遍歷刪除,沒有問題(單執行緒)。

反向遍歷刪除(多執行緒)

  1. public static void main(String[] args) {
  2. ArrayList<String> list = new ArrayList<String>();
  3. list.add("111");
  4. list.add("222");
  5. list.add("222");
  6. list.add("333");
  7. list.add("444");
  8. list.add("333");
  9. Thread thread1 = new Thread() {
  10. @Override
  11. public void run() {
  12. remove(list,"111");
  13. try {
  14. Thread.sleep(1000);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. };
  20. Thread thread2 = new Thread() {
  21. @Override
  22. public void run() {
  23. remove(list, "222");
  24. try {
  25. Thread.sleep(1000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. };
  31. Thread thread3 = new Thread() {
  32. @Override
  33. public void run() {
  34. remove(list, "333");
  35. try {
  36. Thread.sleep(1000);
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. };
  42. // 使各個執行緒處於就緒狀態
  43. thread1.start();
  44. thread2.start();
  45. thread3.start();
  46. // 等待前面幾個執行緒完成
  47. try {
  48. thread1.join();
  49. thread2.join();
  50. } catch (InterruptedException e) {
  51. e.printStackTrace();
  52. }
  53. System.out.println(Arrays.toString(list.toArray()));
  54. }
  55. public static void remove(ArrayList<String> list, String elem) {
  56. // 普通for迴圈倒序刪除,刪除過程中元素向左移動,不影響連續刪除
  57. for (int i = list.size() - 1; i >= 0; i--) {
  58. if (list.get(i).equals(elem)) {
  59. list.remove(list.get(i));
  60. }
  61. }
  62. }

執行結果:

[444]

總結:多執行緒反向遍歷刪除,沒有問題。

Iterator迴圈刪除

  1. public static void main(String[] args){
  2. List<String> list = new ArrayList<String>();
  3. list.add("111");
  4. list.add("222");
  5. list.add("222");
  6. list.add("333");
  7. list.add("444");
  8. list.add("333");
  9. //foreach迴圈刪除
  10. Iterator iterator = list.iterator();
  11. while (iterator.hasNext()){
  12. if (iterator.next().equals("222")){
  13. list.remove(iterator.next());
  14. }
  15. }
  16. System.out.println(Arrays.toString(list.toArray()));
  17. }

執行結果:

  1. Exception in thread "main" java.util.ConcurrentModificationException
  2. at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  3. at java.util.ArrayList$Itr.next(ArrayList.java:859)
  4. at joe.effective.Test.main(Test.java:20)

這個問題就要藉助原始碼來分析了(JDK1.8):

  1. public E remove(int index) {
  2. rangeCheck(index);
  3. modCount++;
  4. E oldValue = elementData(index);
  5. int numMoved = size - index - 1;
  6. if (numMoved > 0)
  7. System.arraycopy(elementData, index+1, elementData, index,
  8. numMoved);
  9. elementData[--size] = null; // clear to let GC do its work
  10. return oldValue;
  11. }
  12. public boolean remove(Object o) {
  13. if (o == null) {
  14. for (int index = 0; index < size; index++)
  15. if (elementData[index] == null) {
  16. fastRemove(index);
  17. return true;
  18. }
  19. } else {
  20. for (int index = 0; index < size; index++)
  21. if (o.equals(elementData[index])) {
  22. fastRemove(index);
  23. return true;
  24. }
  25. }
  26. return false;
  27. }
  28. private void fastRemove(int index) {
  29. modCount++;
  30. int numMoved = size - index - 1;
  31. if (numMoved > 0)
  32. System.arraycopy(elementData, index+1, elementData, index,
  33. numMoved);
  34. elementData[--size] = null; // clear to let GC do its work
  35. }

可以看出,ArrayList的remove方法,一種是根據下標刪除,一種是根據元素刪除。

發現即使看了remove方法的原始碼也不能找到報錯的原因,由於我們使用了Iterator迭代器,那麼再看看迭代器的原始碼,果不其然,就發現了問題所在:

  1. private class Itr implements Iterator<E>
  2. private class ListItr extends Itr implements ListIterator<E>
  1. public void remove() {
  2. if (lastRet < 0)
  3. throw new IllegalStateException();
  4. checkForComodification(); // 檢查修改次數
  5. try {
  6. ArrayList.this.remove(lastRet);
  7. cursor = lastRet;
  8. lastRet = -1;
  9. expectedModCount = modCount;
  10. } catch (IndexOutOfBoundsException ex) {
  11. throw new ConcurrentModificationException();
  12. }
  13. }
  14. final void checkForComodification() {
  15. if (modCount != expectedModCount)
  16. throw new ConcurrentModificationException();
  17. }

Itr和ListItr是ArrayList的兩個私有內部類,Itr實現了Iterator介面,ListItr繼承了Itr類和實現了ListIterator介面。Itr類中也有一個remove方法,迭代器實際呼叫的也正是這個remove方法,上述原始碼也就是這個方法的原始碼。

由原始碼的第二段程式碼可以看出,這個remove方法中呼叫了ArrayList中的remove方法,在這個方法中我們注意到了expectedModCount變數和modCount變數,modCount在前面的程式碼中也見到了,它記錄了ArrayList修改的次數,而前面的變數expectedModCount,這個變數的初值和modCount是相等的;同時在ArrayList.this.remove(lastRet);程式碼面前,呼叫了檢查次數的方法checkForComodification(),這個方法做的事情很簡單,就是如果expectedModCount和modCount不相等,那麼就丟擲異常ConcurrentModificationException。

我們在用Iterator迴圈刪除的時候,呼叫的是ArrayList裡面的remove方法,刪除元素後modCount會增加,expectedModCount則不變,這樣就造成了expectedModCount != modCount,那麼就丟擲異常了。

再用Iterator中的remove方法來測試:

  1. public static void main(String[] args){
  2. List<String> list = new ArrayList<String>();
  3. list.add("111");
  4. list.add("222");
  5. list.add("222");
  6. list.add("333");
  7. list.add("444");
  8. list.add("333");
  9. Iterator iterator = list.iterator();
  10. while (iterator.hasNext()){
  11. if (iterator.next().equals("222")){
  12. iterator.remove();
  13. }
  14. }
  15. System.out.println(Arrays.toString(list.toArray()));
  16. }
執行結果[111, 333, 444, 333]

發現,刪除成功且沒有報錯。

什麼原因呢?我們呼叫的了Iterator中的迭代器刪除元素,在這個方法中有:expectedModCount = modCount這樣一句程式碼,所以當我們每刪除一次元素,就同步一次,所以呼叫checkForComodification()時,就不會報錯。如果換到多執行緒中,這個方法不能保證兩個變數修改的一致性,結果具有不確定性,所以不推薦這種方法。

總結:Iterator呼叫ArrayList的刪除方法報錯,Iterator呼叫迭代器自己的刪除方法,單執行緒不會報錯,多執行緒會報錯。

forEach迴圈刪除

  1. public static void main(String[] args){
  2. List<String> list = new ArrayList<String>();
  3. list.add("111");
  4. list.add("222");
  5. list.add("222");
  6. list.add("333");
  7. list.add("444");
  8. list.add("333");
  9. //foreach迴圈刪除
  10. for (String str : list){
  11. if (str.equals("222")){
  12. list.remove(str);
  13. }
  14. }
  15. System.out.println(Arrays.toString(list.toArray()));
  16. }
  1. 執行結果
  2. Exception in thread "main" java.util.ConcurrentModificationException
  1. at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  2. at java.util.ArrayList$Itr.next(ArrayList.java:859)
  3. at joe.effective.Test.main(Test.java:20)

報錯。

foreach原理是因為這些集合類都實現了Iterable介面,該介面中定義了Iterator迭代器的產生方法,並且foreach就是通過Iterable介面在序列中進行移動。也就是說:在編譯的時候編譯器會自動將對for這個關鍵字的使用轉化為對目標的迭代器的使用

明白了原理就跟上述的Iterator刪除呼叫ArrayList中remove一樣了。

總結:forEach迴圈刪除報錯。