Java - Iterable 接口、迭代器 Iterator

古古

2018/12/05


  • 所有的集合類(List、Set…)都實現自 Collection 接口,而 Collection 接口又繼承於 Iterable 接口,因此可以說所有的集合類(List、Set…)都實現了 Iterable 接口

  • 當某個類實現 Iterable 接口時,我們就能稱這個類是一個 “可數” 的類,也就是可以使用 iterator() 獲取一個迭代器 Iterator,然後使用這個 Iterator 實例去遍歷這個類,因此所有的 Collection 類都能夠使用迭代器 Iterator 來遍歷

    • Iterable 接口

      public interface Iterable<T> {
          //當某個類實現Iterable接口的話,就能獲取到迭代器iterator,然後就能使用這個iterator去遍歷此類
          Iterator<T> iterator();
      }
      
    • Iterator 接口

      • 如果某個類實現了 Iterable 接口,那麼他也需要創建一個內部類去實現一個 Iterator 類,讓調用 Iterable 接口中的 iterator() 時,能夠獲取到一個 iterator 實例

        public interface Iterator<E> {
            //是否有下一個元素
            boolean hasNext();
        
            //取得下一個元素
            E next();
        
            //刪除最後一個獲取的元素,因此調用remove()前一定得先調用一次next()
            void remove();
        }
        
      • 至於此 Iterator 接口怎麼實現,就看各個集合實現類如何定義 “下一個元素”,像是 ArrayList 的下一個元素就是 element[index+1],而 HashMap 的下一個元素則是 hash table 數組中儲存的下一個 entry

      • 另外可以想像 Iterator 像是一個游標一樣,一開始停在最前面,然後不停的往後走(只能向後移動),且此游標每次都是停在元素和元素的中間,當調用 next 時,迭代器就越過下一個元素,並返回剛剛越過的那個元素的引用

    • 使用迭代器 Iterator 遍歷 ArrayList

      public class Main {
          public static void main(String[] args) {
              //ArrayList實現了Collection接口,因此他也實現了Iterable接口,所以他可以使用iterator迭代器來遍歷
              List<String> list = new ArrayList<>();
              list.add("hello");
              list.add("world");
      
              //調用Iterable接口中的iterator()取得此ArrayList的迭代器實例
              Iterator<String> its = list.iterator();
      
              //使用Iterator接口的hasNext()、next()來遍歷此ArrayList集合實現類
              while (true) {
                  if (its.hasNext()) {
                      String s = its.next();
                      System.out.println(s);
                  } else {
                      break;
                  }
              }
          }
      }
      
  • 而再進一步說,當某個類能使用迭代器 Iterator 來遍歷時,就能使用 java 提供的 foreach 語法糖來遍歷此類(foreach語法糖其實就是簡化的 iterator()

    • foreach 實際上會被編譯器編譯成使用迭代器 iterator() 去遍歷集合,因此能使用 foreach 的,都是得實現 Iterable 接口的集合類 Collection 們,像是 List、Set

    • 所以 Map 就沒有辦法直接使用 foreach(因為 Map 沒有實現 Iterable 接口),只有他的 map.entrySet()map.keySet()map.values() 這種返回一個集合類的方法,才能使用 foreach

      public class Main {
          public static void main(String[] args) {
              List<String> list = new ArrayList<>();
              list.add("hello");
              list.add("world");
      
              //原代碼,使用語法糖的foreach
              for (String s : list) {
                  System.out.println(s);
              }
      
              //實際上會被編譯成使用iterator去遍歷
              for (Iterator<String> its = list.iterator(); its.hasNext(); ) {
                  String s = its.next();
                  System.out.println(s);
              }
          }
      }
      
  • 為什麼 Iterator 要額外使用內部類去實現,而不是 ArrayList 直接實現此接口 ?

    • 如果看過 Collection 類的源碼(以ArrayList為例),可以發現 ArrayList 類並不是由 ArrayList 去實現 Iterator 接口,而是 ArrayList 有一個內部類 Itr,專門去實現 Iterator 接口,而 ArrayList 的 iterator() 方法,只是去創建一個內部類 ArrayList.Itr 的實例而已

      //ArrayList不實現Iterator接口,反而是由他的內部類進行實現
      public class ArrayList<E> extends AbstractList<E> {
          //調用list.iterator()可以取得此list的迭代器
          public Iterator<E> iterator() {
              return new Itr(); //實際上就是去創建一個內部類的實例
          }
      
          //ArrayList中的內部類Itr,專門實現Iterator接口
          private class Itr implements Iterator<E> {
              int cursor; //記錄當前迭代到哪裡
      
              public boolean hasNext() { ... }
              public E next() { ... }
              public void remove() { ... }
          }
      }
      
    • 要這樣設計是因為一個集合類可能同時有多個迭代器去遍歷他,而每個迭代器遍歷到集合的哪裡,是每個迭代器自己的事情,彼此不互相干涉,因此才需要額外使用一個內部類去實現迭代器的 Iterator 接口

      • 如此當需要用到 Iterator 來遍歷集合時,只需要調用 list.iterator(),就能取得一個全新的、不受別人影響的迭代器供自己使用,而迭代器彼此之間也不會互相干涉
      • 至於為什麼要特別使用內部類來實現 Iterator 接口,而不是創建一個 Iterator 公共類來供所有集合一起使用,是因為迭代器需要知道集合的內部結構,他才能知道要怎麼去實現 hasNext()next()remove() 方法,而使用內部類才能無條件的取用外部類的所有信息(包含 private 的變量和方法),因此才需要將 Iterator 提取成接口,讓每個集合自己使用內部類去實現 Iterator 接口
  • 為什麼 Iterator 接口,只有 hasNext()next()remove() 方法,而沒有 add(E) 方法 ?

    • 邏輯上來說,迭代器是一個一個去遍歷集合中的元素,而當前 iterator 停下的地方,就是迭代到一半的地方
      • 如果當迭代到一半時調用 iterator.add() 方法,理論上來說,應該是要在當前這個元素 E1 後面新增一個元素 E2,使得下次遍歷此集合時,E2 一定會出現在 E1 後面,也就是 [….E1, E2, ….]
      • 假設 add() 方法是以這個語意為前提的話,那麼迭代器不提供此方法是很合理的,對於有序的集合(像是ArrayList)來說,在此元素後面新增一個元素是一個很簡單的事情,但是對於無序的集合(像是HashSet)來說,不能保證新插入的這個元素 E2 一定會在 E1 後面(因為還得計算 HashCode),如此就違反了 add() 的語意了,這也就是為什麼 Iterator 接口不提供 add() 方法
    • 另一個說法是,在使用迭代器時,通常就是 “遍歷” 的場景,這種場景下很少會去使用 add() 方法,因此 Iterator 接口沒必要提供這個方法