迭代器筆試題,居然難倒很多人
有位小朋友最近正在為年后換工作做準備,但是遇到一個問題,覺得很不可思議的一道筆試題。然后我把這道題發到技術群里,發現很多人居然不知道,很多都是連蒙帶猜的說。感覺很有必要寫一篇文章來說道說道。
奇怪的筆試題閱讀下面這段代碼,請寫出這段代碼的輸出內容:
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.*;
- public class Test {
- public static void main(String[] args) {
- List<String> list = new ArrayList<>();
- list.add("1");
- list.add("2");
- list.add("3");
- Iterator iterator = list.iterator();
- while (iterator.hasNext()) {
- String str = (String) iterator.next();
- if (str.equals("2")) {
- iterator.remove();
- }
- }
- while (iterator.hasNext()) {
- System.out.println(iterator.next());
- }
- System.out.println("4");
- }
- }
他寫出來的答案是:
- 1
- 3
- 4
奇怪的是,你把這道題目發給你身邊人,讓他們回答這道面試題輸出結果是什么,說這個結果的人非常多。不行你試試。
答案明顯不對,因為在第一個while里的 iterator.hasNext()==false后才會到第二個while里來,同一個Iterator對象,前面調一次iterator.hasNext()==false,再判斷一次結果不還是一樣嗎?,
所以第二個while判斷為false,也就不會再去遍歷iterator了,由此可知本體答案是:4。
下面我們來分析一下為什么是具體底層是怎么實現的。
這里的Iterator是什么?
- 迭代器是一種模式、詳細可見其設計模式,可以使得序列類型的數據結構的遍歷行為與被遍歷的對象分離,即我們無需關心該序列的底層結構是什么樣子的。只要拿到這個對象,使用迭代器就可以遍歷這個對象的內部
- Iterable 實現這個接口的集合對象支持迭代,是可以迭代的。實現了這個可以配合foreach使用~
- Iterator 迭代器,提供迭代機制的對象,具體如何迭代是這個Iterator接口規范的。
Iterator說明
- public interface Iterator<E> {
- //每次next之前,先調用此方法探測是否迭代到終點
- boolean hasNext();
- //返回當前迭代元素 ,同時,迭代游標后移
- E next();
- /*刪除最近一次已近迭代出出去的那個元素。
- 只有當next執行完后,才能調用remove函數。
- 比如你要刪除第一個元素,不能直接調用 remove() 而要先next一下( );
- 在沒有先調用next 就調用remove方法是會拋出異常的。
- 這個和MySQL中的ResultSet很類似
- */
- default void remove() {
- throw new UnsupportedOperationException("remove");
- }
- default void forEachRemaining(Consumer<? super E> action) {
- Objects.requireNonNull(action);
- while (hasNext())
- action.accept(next());
- }
- }
這里的實現類是ArrayList的內部類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
- //modCountshi ArrayList中的屬性,當添加或刪除的時候moCount值會增加或者減少
- //這里主要是給fail-fast使用,避免一遍在遍歷,一遍正在修改導致數據出錯
- //此列表在結構上被修改的次數。結構修改是指改變結構尺寸的修改列表,
- //或者以這樣的方式對其進行擾動,進步可能會產生錯誤的結果。
- int expectedModCount = modCount;
- public boolean hasNext() {
- //cursor初始值為0,沒掉一次next方法就+1
- //size是ArrayList的大小
- return cursor != size;
- }
- @SuppressWarnings("unchecked")
- public E next() {
- checkForComodification();
- int i = cursor;
- if (i >= size)
- throw new NoSuchElementException();
- //把ArrayList中的數組賦給elementData
- Object[] elementData = ArrayList.this.elementData;
- if (i >= elementData.length)
- throw new ConcurrentModificationException();
- //每調用一次next方法,游標就加1
- //cursor=lastRet+1
- cursor = i + 1;
- //返回ArrayList中的元素
- return (E) elementData[lastRet = i];
- }
- public void remove() {
- if (lastRet < 0)
- throw new IllegalStateException();
- checkForComodification();
- try {
- //調用ArrayList中remove方法,溢出該元素
- ArrayList.this.remove(lastRet);
- //cursor=lastRet+1,
- //所以此時相當于cursor=cursor-1
- cursor = lastRet;
- lastRet = -1;
- expectedModCount = modCount;
- } catch (IndexOutOfBoundsException ex) {
- throw new ConcurrentModificationException();
- }
- }
- final void checkForComodification() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- }
- }
再回到上面題目中:
第一個iterator.hasNext()
- 第1次循環hasNext方法中:cursor==0, size==3,所以cursor != size返回true。
- next方法中:cursor=0+1。返回"1"。
第2次循環
- hasNext方法中:cursor==1, size==3,所以cursor != size返回true。
- next方法中:cursor=1+1。返回"2"。
- remove方法中:cursor==cursor-1==2-1=1,把ArrayList中的"2"給刪除了,所以size==2。
第3次循環
- hasNext方法中:cursor==1, size==2,那么cursor != size返回true。
- next方法中:cursor=1+1==2;返回"3"。
第4次循環
- hasNext方法中:cursor==2, size==2,那么cursor != size返回false。
第二個iterator.hasNext()
hasNext方法中:cursor==2, size==2,所以cursor != size返回false。
所以,最后只輸出"4",即答案為4.
Iterator與泛型搭配
- Iterator對集合類中的任何一個實現類,都可以返回這樣一個Iterator對象。可以適用于任何一個類。
- 因為集合類(List和Set等)可以裝入的對象的類型是不確定的,從集合中取出時都是Object類型,用時都需要進行強制轉化,這樣會很麻煩,用上泛型,就是提前告訴集合確定要裝入集合的類型,這樣就可以直接使用而不用顯示類型轉換.非常方便.
foreach和Iterator的關系
- for each以用來處理集合中的每個元素而不用考慮集合定下標。就是為了讓用Iterator簡單。但是刪除的時候,區別就是在remove,循環中調用集合remove會導致原集合變化導致錯誤,而應該用迭代器的remove方法。
使用for循環還是迭代器Iterator對比
- 采用ArrayList對隨機訪問比較快,而for循環中的get()方法,采用的即是隨機訪問的方法,因此在ArrayList里,for循環較快
- 采用LinkedList則是順序訪問比較快,iterator中的next()方法,采用的即是順序訪問的方法,因此在LinkedList里,使用iterator較快
- 從數據結構角度分析,for循環適合訪問順序結構,可以根據下標快速獲取指定元素.而Iterator 適合訪問鏈式結構,因為迭代器是通過next()和Pre()來定位的.可以訪問沒有順序的集合.
- 而使用 Iterator 的好處在于可以使用相同方式去遍歷集合中元素,而不用考慮集合類的內部實現(只要它實現了 java.lang.Iterable 接口),如果使用 Iterator 來遍歷集合中元素,一旦不再使用 List 轉而使用 Set 來組織數據,那遍歷元素的代碼不用做任何修改,如果使用 for 來遍歷,那所有遍歷此集合的算法都得做相應調整,因為List有序,Set無序,結構不同,他們的訪問算法也不一樣.(還是說明了一點遍歷和集合本身分離了)。
總結
- 迭代出來的元素都是原來集合元素的拷貝。
- Java集合中保存的元素實質是對象的引用,而非對象本身。
- 迭代出的對象也是引用的拷貝,結果還是引用。那么如果集合中保存的元素是可變類型的,那么可以通過迭代出的元素修改原集合中的對象。
本文轉載自微信公眾號「Java后端技術全棧」,可以通過以下二維碼關注。轉載本文請聯系Java后端技術全棧公眾號。