把書讀薄 | 《設計模式之美》設計模式與範式(行爲型-迭代器模式)

這是我參與8月更文挑戰的第5天,活動詳情查看: 8月更文挑戰java

0x0、引言

😪 早上困困,啃下設計模式之美提提神,本文對應設計模式與範式:行爲型(65-67),迭代器模式 (Iterator Pattern),又稱 遊標模式,用於 解耦容器代碼和遍歷代碼算法

不過,不少編程語言都將迭代器做爲一個基礎類庫,直接提供出來了。平常業務開發,不多本身實現一個迭代器,固然,弄懂原理能幫助咱們更好地使用這些工具類~編程

Tips:二手知識加工不免有所紕漏,感興趣有時間的可自行查閱原文,謝謝。設計模式


0x一、定義

原始定義數組

迭代器提供一種對容器對象中各個元素進行訪問的方法,而又不須要暴露該對象的內部細節。安全

定義很好理解,上構成該模式的四個角色:markdown

  • Iterator (抽象迭代器類) → 定義統一的迭代器方法hasNext()和next(),用於判斷當前集合是否還有其餘對象及按順序讀取集合中的當前對象;
  • ConcreteIterator (具體迭代器) → 實現抽象迭代器聲明的方法,處理具體集合對象中對對象位置的偏移及具體對象數據的傳輸;
  • Container (抽象容器類) → 抽象及建立迭代器類關聯的方法,同時可添加其餘集合類須要的方法;
  • ConcreteContainer (具體容器類) → 實現抽象容器類中聲明的方法,建立對應具體的迭代器類;

其實就是兩類角色:容器迭代器,寫個簡單示例幫助理解~數據結構

0x二、寫個簡單例子

// 歌曲實體
public class Music {
    private String name;
    private String singer;
    private long createTime;

    public Music(String name, String singer, long createTime) {
        this.name = name;
        this.singer = singer;
        this.createTime = createTime;
    }

    public String getName() { return name; }
    public String getSinger() { return singer; }
    public long getCreateTime() { return createTime; }
    
    @Override
    public String toString() { 
        return "【" + name + "】- " + singer + " - " + createTime; 
    }
}

// 抽象迭代器
public interface Iterator {
    // 最基本的兩個方法
    Music next();
    boolean hasNext();
    // 按需添加
    Music currentItem();
    Music first();
}

// 抽象容器
public interface Container {
    Iterator createIterator();
}

// 具體迭代器
public class ConcreteIterator implements Iterator {
    private Music[] musics;
    private int pos = 0;

    // 待遍歷容器經過依賴注入傳遞到具體迭代器類中
    public ConcreteIterator(Music[] musics) { this.musics = musics; }

    @Override public Music next() { return musics[pos++]; }
    @Override public boolean hasNext() { return pos < musics.length; }
    @Override public Music currentItem() { return musics[pos]; }
    @Override public Music first() { return musics[0]; }
}

// 具體容器
public class ConcreteContainer implements Container {
    private Music[] musics;

    public ConcreteContainer(Music[] musics) { this.musics = musics; }

    @Override public Iterator createIterator() { return new ConcreteIterator(musics); }
}


// 測試用例
public class IteratorTest {
    public static void main(String[] args) {
        Music[] musics = new Music[5];
        musics[0] = new Music("We Sing. We Dance. We Steal Things.", "Jason Mraz", 20080513);
        musics[1] = new Music("Viva La Vida Death And All His Friends", "Coldplay", 20080617);
        musics[2] = new Music("華麗的冒險 ", "陳綺貞", 20050923);
        musics[3] = new Music("范特西 Fantasy", "周杰倫", 20010914);
        musics[4] = new Music("後。青春期的詩 後青春期的詩", "五月天", 20081023);
        Container container = new ConcreteContainer(musics);
        Iterator iterator = container.createIterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.currentItem());
            iterator.next();
        }
    }
}
複製代碼

代碼運行結果以下多線程

代碼很是簡單:併發

  • 具體迭代器實現next()、hasNext()方法;
  • 待遍歷容器對象經過依賴注入傳遞到迭代器中;
  • 容器經過createIterator()方法建立迭代器;

你可能或說過分設計了,上面的遍歷操做,本身經過 for循環foreach循環 均可以實現。

的確如此,那爲啥還要給容器設計對應的迭代器呢?三個緣由:

  • 複雜數據結構(如圖、樹),有各類複雜的遍歷方式(樹的前中後序遍歷、圖的深廣度優先遍歷等),若是讓客戶端來實現這些遍歷算法,勢必會增長開發成本,並且容易出錯;
  • ② 把遍歷邏輯放容器類裏無疑增長了容器類的複雜性,應對複雜性的方法就是 拆分,可把遍歷操做拆分到迭代類中;
  • 每一個迭代器獨享遊標信息,建立多個不一樣迭代器,同時對同一個容器遍歷而不互相影響;

在舉個例子,如今須要按照歌曲時間升序遍歷,只須要實現一個迭代器類:

public class OrderTimeIterator implements Iterator {
    private final Music[] musics;
    private int pos;

    public OrderTimeIterator(Music[] musics) {
        this.musics = new Music[musics.length];
        System.arraycopy(musics, 0, this.musics, 0, musics.length);
        sortByTimeAsc(this.musics, 0, this.musics.length - 1);
        this.pos = 0;
    }

    // 快速排序
    private void sortByTimeAsc(Music[] arr, int low, int high) {
        if(low > high) return;
        int i = low;
        int j = high;
        Music temp;
        Music anchor = arr[low];
        while (i < j) {
            while (arr[j].getCreateTime() >= anchor.getCreateTime() && i < j) {
                j--;
            }
            while (arr[i].getCreateTime() <= anchor.getCreateTime() && i < j) {
                i++;
            }
            if(i < j) {
                temp = arr[j];
                arr[j] = arr[i];
                arr[i] = temp;
            }
        }
        arr[low] = arr[i];
        arr[i] = anchor;
        sortByTimeAsc(arr, low, j -1);
        sortByTimeAsc(arr, j + 1, high);
    }
    ... // 其餘實現方法同ConcreteIterator
}
複製代碼

具體容器類返回迭代器createIterator()方法,改爲new OrderTimeIterator()便可,輸出結果以下:

不懂快排的童鞋不須要了解具體細節,直接換迭代器便可,還能夠按照本身的需求自定義迭代器,妙啊。

對了foreach循環語法糖,其實也是基於迭代器實現的,接着帶出UML類圖、使用場景和優缺點:

使用場景

  • 但願對客戶端隱藏遍歷算法複雜性時;
  • 需爲容器(聚合)對象提供多種遍歷方式時;

優勢

  • 知足單一職責原則和開閉原則;
  • 更好的封裝性,簡化客戶端調用,能夠用不一樣的變量方式來遍歷一個集合;

缺點

  • 子類增長;
  • 對於簡單遍歷,略顯繁瑣,如ArrayList直接用for循環+get()遍歷便可;
  • 抽象迭代器的設計難度大,須要充分考慮到系統未來的擴展,如JDK內置迭代器Iterator就沒法實現逆向遍歷。若是須要實現逆向遍歷,只能經過其子類ListIterator等來實現,而ListIterator迭代器沒法用於操做Set類型的聚合對象。在自定義迭代器時,建立一個考慮全面的抽象迭代器並非件很容易的事情

0x三、加餐1:fail-first 快速機制

問題來了 → 當遍歷的同時增刪集合元素會怎麼樣

答:可能會致使重複遍歷或遍歷不到某個元素。

是可能,並不會全部狀況下都遍歷出錯,有時還能夠正常遍歷,這種行爲稱爲 結果不可預期行爲未決行爲,即運行的結果是對是錯,得是狀況而定。

好比原列表長度爲5,迭代的時候插入了一個元素,但迭代器length仍是以前的5,會漏掉新插入的元素; 又好比迭代時刪掉了最後一個元素,但迭代器length仍是以前的5,會引起數組越界;

如何應對這種遍歷時改變集合致使的未決行爲?

  • 遍歷時不容許增刪元素
  • 遍歷時增刪元素直接報錯

方法一比較難實現,得肯定遍歷開始與結束的時間點,開始好拿(如建立迭代器時),結束很差拿,由於遍歷不必定把全部元素都走一遍,好比找到知足條件的元素,提早結束遍歷。

在迭代器內定義一個接口finishIteration(),主動告知容器迭代器使用完畢,但這就要求調用者在使用完迭代器後要主動調用此函數,增長了開發成本之餘還容易漏掉。

Java語言中採用的方法二,如ArrayList中定義了一個成員變量modCount,記錄集合被修改的次數,調用增刪函數都會加1。

建立迭代器的時候傳入,而後每次調用迭代器的next()、hasNext()函數時都檢查集合中的modCount是否等於一開始傳入的modCount,不等說明集合存儲的元素已經發生改變,以前建立的迭代器已不能正確運行,直接拋出運行時異常,結束程序。

另外,在單線程狀況下,ArrayList使用迭代器進行迭代,經過迭代器增刪元素,不會引起異常,原理是:

內部類Itr 實現Iterator接口,定義了兩個變量cursor (下一個元素下標) 和 lastRet (上一個元素下標) 當發生元素增刪時,更新迭代器中的遊標及這兩個值,保證遍歷不出錯。

而對於多線程的狀況,除了在iterator使用處加鎖外,還能夠用 併發容器

原理是:採用的是 fail-safe(安全失敗) 機制:迭代時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。因此遍歷期間原集合發生的修改迭代器是不知道的,原迭代器也不能訪問修改後的內容。Java的併發容器放在java.util.concurrent包中,如使用 CopyOnWriteArrayList 來代替ArrayList。

0x四、加餐2:實現一個支持快照功能的迭代器

所謂的 "快照" 就是建立迭代器時至關於給容器拍了張快照(Snapshot),以後增刪容器元素,快照中的元素都不會發生改變,即迭代器遍歷的對象是快照而非容器。經過一個例子來解釋這段話:

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
ListIterator<String> it1 = list.listIterator();
while (it1.hasNext()) System.out.print(it1.next()); // 輸出: abcd

System.out.println();
list.remove("a");
ListIterator<String> it2 = list.listIterator();
while (it2.hasNext()) System.out.print(it2.next()); // 輸出:bcd

System.out.println();
list.remove("c");
ListIterator<String> it3 = list.listIterator();
while (it3.hasNext()) System.out.print(it3.next()); // 輸出:bd
複製代碼

第一種解法:迭代器類中定義一個存儲快照的成員變量,構造迭代器時複製原集合引用進行初始化,後續遍歷都基於持有的快照進行。(我上面定義的OrderTimeIterator就是這種)。

固然,缺點明顯,每次建立迭代器,都要拷貝一份數據到快照中,增長內存消耗,當有多個迭代器在遍歷元素,還會致使重複存儲多份。不過,好在Java中的拷貝屬於淺拷貝,因此只是拷貝了對象的引用而已。

第二種解法:容器中爲每一個元素保存兩個時間戳,添加時間刪除時間 (初始化爲最大長整型值),添加時將添加時間設置爲當前時間,刪除時將時間設置爲當前時間,記住只是 標記刪除,並不是真的從容器中將其刪除

而後每一個迭代器保存一個 建立時間,即快照建立時間戳,當使用迭代器遍歷容器時,只有知足:

添加時間 < 建立時間 < 刪除時間

的元素才屬於這個迭代器的快照:

  • 添加時間 > 建立時間 → 說明元素在建立迭代器後才加入,不屬於這個迭代器的快照;
  • 刪除時間 < 建立事件 → 說明元素在建立迭代器前就被刪除了,一樣不屬於這個迭代器的快照;

在不拷貝容器的狀況下,在容器自己藉助時間戳實現快照功能,妙啊!

這種方式解決了一個問題,又引入了一個問題:

ArrayList底層依賴數組這種存儲結構,本來支持快速隨機訪問,在O(1)時間複雜度內獲取下標爲i的元素。但如今刪除元素並無真正刪除,這就致使沒法支持按照下標快速隨機訪問了。

解法:

在ArrayList中存儲兩個數組,一個支持標記刪除,用來實現快照遍歷,一個不支持標記刪除(刪除數據直接從數組中刪除),用來支持隨機訪問。


以上內容就是本節的所有內容,謝謝~

相關文章
相關標籤/搜索