Java迭代器須要知道的事兒

摘要

  1. Java集合框架總覽java

    Collection面試

    ​ List併發

    ​ ArrayList、Vector、LinkedList框架

    ​ Set源碼分析

    ​ LinkedHashSet、TreeSetpost

    Mapthis

    ​ HashTable、HashMap、TreeMap、LinkedHashMap、ConcurrentHashMapspa

  2. Iterator源碼分析3d

    • 實現Iterable接口可使用foreach,Iterable接口主要提供一個返回Iterator的方法。code

    • Iterator提供HasNext、next、remove等遍歷容器方法,其實現由子類(ArrayList、AbstractList等)實現。

    • ListIterator接口是Iterator的子接口,在原來迭代器接口的基礎上又增長了一些新的方法,容許向前、向後兩個方向遍歷

  3. foreach和迭代器

    foreach是java的語法糖,其實現是經過Iterator和wile實現的。

  4. for循環和foreach

    • 這兩個都是遍歷數據容器的方式,但二者有根本性的差異:for循環是經過循環的方式去遍歷數據,而foreach是經過while循環+迭代器的方式遍歷數據。
    • foreach遍歷容器的時候,若是對容器進行了add/remove操做,會拋出ConcurrentModificationException異常。但也不是全部容器,Java提供的fail-safe(java.util.concurrent包下的容器都是fail-safe)機制的容器就能夠在遍歷的時候add/remove
  5. ConcurrentModificationException異常出現的緣由

    • 爲何foreach遍歷ArrayList的時候,remove元素會拋出異常?

      ArrayList中有類變量modCount記錄了ArrayList變化的次數,每次在add/remove的時候會自增,當調用ArrayList.iterator()方法的時候會new一個內部類Itr,並在初始化Itr的時候賦值expectedModCount=modCount。

      當ArrayList再調用add/remove方法的時候modCount會再次自增,但expectedModCount並不會隨之改變。最終在遍歷時調用Itr.next()校驗modCount是否等於expectedModCount時拋出異常。

    • 爲何用迭代器遍歷時,remove元素就不會拋出異常?

      迭代器遍歷時,調用Itr.remove()時會再次令expectedModCount=modCount,因此在Itr.next()判斷時會經過校驗。

前言

集合是Java語言裏的重要部分,面試時一定會問的問題,都知道集合會問 ArrayList、LinkedList、HashMap等,但有時看的博客太過片斷,沒有對整個集合框架有較完整的認知,本文是Java集合部分的前序,包含如下幾個部分:

  1. 介紹Java集合框架中重要的接口和類。
  2. 對迭代器接口Iterator進行源碼分析。
  3. 對接口Collection和Map進行源碼分析。

Java集合框架總覽

集合框架主要3個:List、Set、Map,看一下類圖(圖中紅框框出的是在面試中最多問到的和平時使用最多的類

  1. Collection和Map都是在java.util包下的,而ConcurrentHashMap是在併發包java.util.concurrent下。
  2. Map和List、Set不一樣,不是Collection的實現類,而且也沒有實現Iterable,這也是爲何不能直接經過foreach的方式去遍歷Map的緣由

接口Iterator源碼分析

咱們知道集合是用來存放數據的容器,咱們常常也須要去遍歷整個容器,去獲取數據,操做數據,這就有了迭代器的意義。

Collection接口是Iterable的子接口,Iterable這個接口的做用在其類聲明中也能夠看到,也就是實現了這個接口的類可使用for-each的循環。

那麼爲何實現了這個接口就能夠遍歷了呢?

其實這個接口最主要的方法是返回一個Iterator接口,在Iterator接口中,主要方法是咱們熟悉的hasNext() next() remove()。

Iterable 可遍歷,Iterator遍歷器

1.8以後Iterator接口中新增了一個forEachRemaining方法,方法做用是將每一個元素做爲參數發給action來執行特定操做

既然這個迭代器是接口,那麼其具體實現是怎麼實現的呢?咱們以ArrayList爲例,在ArrayList中重寫了Iterator()方法(以下),能夠看到new了一個Itr對象,這個Itr是ArrayList的內部類,並實現了Iterator接口。

public Iterator<E> iterator() {
  return new Itr();
}

/** * An optimized version of AbstractList.Itr */
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;
  Itr() {}
  public boolean hasNext() {
    return cursor != size;
  }
  public E next() {
   ...
  }
  public void remove() {
   ...
  }
  public void forEachRemaining(Consumer<? super E> consumer) {
    ...
  }
}
複製代碼

有了上面的源碼分析,當試着去看LinkedList的迭代器實現,會發現其iterator()方法調用的是父抽象類AbstractList的iterator()方法。原來在抽象類AbstractList中也重寫了iterator()方法,並定義了一個Itr()的內部類,跟ArrayList很類似(還不知道爲何要在ArrayList中又重寫一次)。這裏整理了一下Java集合框架迭代器的實現。

迭代器實現 說明
ArrayList ArrayList定義的內部類
LinkedList AbstractList中定義的內部類
Vector Vector定義的內部類 方法級synchronized
Set map.keySet().iterator() TreeSet、LinkedHashSet一樣
Map Key、value、entry繼承AbstractSet,有實現iteratable接口

對於List其實現較爲類似,都是會在內部定義一個Itr的類,而後iterator()方法是返回一個Itr內部類的實例對象,但Set的iterator()方法實現使人驚訝,而且Map沒有繼承Iterable接口,卻也有iterator()方法(是在Map的內部類keySet等中有),這部分的源碼會在後面對List、Set、Map的深刻分析中展開。

ListItr

在List中,不管是AbstractList仍是ArrayList、Vector都有一個ListItr的內部類。

private class ListItr extends Itr implements ListIterator<E> {
  ListItr(int index) {
    cursor = index;
  }
  ...
}
複製代碼

ListIterator接口是Iterator的子接口,在原來迭代器接口的基礎上又增長了一些新的方法,容許向前、向後兩個方向遍歷 List

foreach和迭代器

foreach實際上是Java提供的語法糖,咱們看下用foreach的循環反編譯以後的是如何的。

原代碼:

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
    }
}
複製代碼

反編譯以後的代碼:

public static void main(String[] args) {
    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("H");
    }};

    Iterator iterator = userNames.iterator();
    do
    {
        if(!iterator.hasNext())
            break;
        String userName = (String)iterator.next();
        if(userName.equals("Hollis"))
            userNames.remove(userName);
    } while(true);
    System.out.println(userNames);
}
複製代碼

原來foreach的實現是經過Iterator和wile實現的

for循環和foreach

這兩個都是能夠實現遍歷數據容器的方式,但二者有根本性的差異:for循環是經過循環的方式去遍歷數據,而foreach是經過while循環+迭代器的方式遍歷數據。

若是在遍歷集合的時候,對集合進行了add/remove操做,那麼for循環和foreach循環會有什麼不一樣呢?咱們看下下面這個例子:

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        new Main().run2(list);
        System.out.println(list);
    }

    private void run1(List list) {
        for (int i = 0; i < list.size(); i++) {
            list.remove(i);
        }
    }

    private void run2(List<Integer> list) {
        for (Integer i : list) {
            list.remove(i);
        }
    }
}
複製代碼

結果一目瞭然:

run1運行結果:[2, 4]

run2運行結果:Exception in thread "main" java.util.ConcurrentModificationException(記住這個異常,後文會進行分析)

上面的結果應該都沒什麼疑問,foreach中不要對遍歷對象進行add/remove這幾乎是常識了(但也不是全部的容器,fail-safe的容器就能夠遍歷的時候add/remove),但若是咱們將輸入的list的長度改成2時,輸出結果是否會報異常呢?

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
// list.add(3);
// list.add(4);
        new Main().run2(list);
        System.out.println(list);
    }

    private void run2(List<Integer> list) {
        for (Integer i : list) {
            list.remove(i);
        }
    }
複製代碼

輸出結果:[2] , 輸出結果並無拋出ConcurrentModificationException異常

這是爲何呢? 咱們從源碼的角度去分析一下這個問題。

ConcurrentModificationException異常出現的緣由

咱們用反編譯以後的代碼看一下:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<String>() {{
        add(1);
        add(2);
    }};

    Iterator iterator = list.iterator();
    do
    {
        if(!iterator.hasNext())                  // 2
            break;
        Integer i = (Integer)iterator.next();    // 3
        list.remove(i);                           // 1
    } while(true);
    System.out.println(list);
}
複製代碼

在ArrayList中搜索一下ConcurrentModificationException異常,發現與上述代碼相關的就是迭代器的next()方法。對於ArrayList,其迭代器是內部實現的Itr內部類,在next方法中會首先調用checkForComodification方法,代碼以下:

public E next() {
  checkForComodification();
	...
}

final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}
複製代碼

哦!原來modCount != expectedModCount就會拋出ConcurrentModificationException異常。

分析到這裏,咱們能夠先回答一下上節的問題:

爲何當list的長度爲2是,使用foreach遍歷集合,並對集合進行remove操做,並不會拋出ConcurrentModificationException異常?

答案就是,拋出異常是在迭代器的next()方法中,會先校驗modCount 是否 expectedModCount。而集合長度爲2時,在第一次remove以後再調用迭代器hasNext()就直接跳出循環了。

如今咱們接着將modCount和expectedModCount是什麼。modCount表明對ArrayList的操做次數,每次add/remove都會使modCount++,而expectedModCount是ArrayList內部迭代器的修改次數,當調用ArrayList的iterator方法時,會new Itr,並在此時對expectedModCount賦值爲modCount。

//調用ArrayList的iterator()方法會new Itr
public Iterator<E> iterator() {
  return new Itr();
}
//Itr初始化的時候,expectedModCount會賦值爲modCount
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;


複製代碼

因此,expectedModCount只在Itr初始化(也就是調用ArrayList的Iterator方法)的時候會賦值爲modCount ,若是後面又對ArrayList修改了(add/remove),那麼modCount++,但expectedModCount並不會改變,因此就形成了modCount != expectedModCount

咱們在看下面的代碼:

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        Iterator<Integer> iterator = list.iterator();
        do{
            if(!iterator.hasNext())  {
                break;
            }
            Integer i = iterator.next();
            iterator.remove();  //和上面的代碼只是改變了這行! list換成了iterator
        }while (true);
        System.out.println(list);
    }

複製代碼

這是用迭代器去遍歷的一般實現,結果一目瞭然,輸出結果:[]

但爲何第12行代碼,list.remove()換成iterator.remove()就不會有異常了呢?

public void remove() {
  if (lastRet < 0)
    throw new IllegalStateException();
  checkForComodification();

  try {
    ArrayList.this.remove(lastRet);
    cursor = lastRet;
    lastRet = -1;
    expectedModCount = modCount;   //LOOK AT ME!
  } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
  }
}
複製代碼

在代碼第10行,會對再次expectedModCount賦值爲modCount。因此使用iterator.remove()並不會拋出ConcurrentModificationException異常

相關文章
相關標籤/搜索