JAVA中容器設(shè)計(jì)的進(jìn)化史:從白盒到黑盒盗胀,再到躋身為設(shè)計(jì)模式之一的迭代器

大家好艘蹋,又見面了。

在我們的項(xiàng)目編碼中读整,不可避免的會(huì)用到一些容器類簿训,我們可以直接使用ListMap米间、Set强品、Array等類型。當(dāng)然屈糊,為了體現(xiàn)業(yè)務(wù)層面的含義的榛,我們也會(huì)根據(jù)實(shí)際需要自行封裝一些專門的Bean類,并在其中封裝集合數(shù)據(jù)來使用逻锐。

看下面的一個(gè)場景:

在一個(gè)企業(yè)級(jí)的研發(fā)項(xiàng)目事務(wù)管理系統(tǒng)里面夫晌,包含很多的項(xiàng)目雕薪,每個(gè)項(xiàng)目下面又包含很多的具體需求,而每個(gè)需求下面又會(huì)被拆分出若干的具體事項(xiàng)晓淀。

上面的示例場景中所袁,對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)邏輯可以用下圖來表示出來:

9.23.1.png

按照常規(guī)思路,我們會(huì)怎么去建模呢凶掰?為了簡化描述燥爷,我們僅以項(xiàng)目--需求--任務(wù)這個(gè)維度來說明下。

首先肯定會(huì)去創(chuàng)建Project(項(xiàng)目)懦窘、Requirement(需求)前翎、Task(任務(wù))三個(gè)類,然后每個(gè)類中會(huì)包含一個(gè)子對(duì)象的集合畅涂。比如對(duì)于Project而言港华,會(huì)包含一個(gè)Requirement的集合:

@Data
public class Project {
    private List<Requirement> requirements;
    private int status;
    private String projectName;
    // ...
}

同樣道理,我們定義Requirement的時(shí)候午衰,也會(huì)包含一個(gè)Task的集合:

@Data
public class Requirement {
    private List<Task> tasks;
    private int status;
    private String requirementName;
    private Date createTime;
    private Date closeTime;
    // ...
}

上述的例子中立宜,Project、Requirement便是兩個(gè)典型的“容器”臊岸,容器中會(huì)存儲(chǔ)著若干具體的元素對(duì)象赘理。對(duì)容器而言,遍歷容器內(nèi)的元素是無法繞過的一個(gè)基本操作扇单。

按照上面的容器對(duì)象定義實(shí)現(xiàn),在業(yè)務(wù)邏輯代碼中奠旺,需要獲取某個(gè)Project中所有已關(guān)閉的需求事項(xiàng)列表,并按照創(chuàng)建時(shí)間降序排列蜘澜,我們要如何做:先從容器中取出所有的需求集合,然后自行對(duì)此需求集合進(jìn)行過濾响疚、排序等操作鄙信。

public List<Requirement> getAllClosedRequirements(Project project) {
    return project.getRequirements().stream()
            .filter(requirement -> requirement.getStatus() ==  1)
            .sorted((o1, o2) -> (int) (o2.getCreateTime().getTime() - o1.getCreateTime().getTime()))
            .collect(Collectors.toList());
}

或者,也可能會(huì)寫成如下更為通俗的處理邏輯:

public List<Requirement> getAllClosedRequirements(Project project) {
    List<Requirement> requirements = project.getRequirements();
    List<Requirement> resultList = new ArrayList<>();
    for (Requirement requirement : requirements) {
        if (requirement.getStatus() == 1) {
            resultList.add(requirement);
        }
    }
    resultList.sort((o1, o2) -> (int) (o2.getCreateTime().getTime() - o1.getCreateTime().getTime()));
    return resultList;
}

很司空見慣的邏輯忿晕,的確也沒有什么問題装诡。但是,其實(shí)我們僅僅只是需要遍歷容器中所有的元素践盼,然后找出符合需要的內(nèi)容鸦采,而Project類通過getRequirements()方法將整個(gè)內(nèi)部存儲(chǔ)List對(duì)象給出來讓調(diào)用方直接去操作,存在一定的弊端

  • 調(diào)用方通過project.getRequirements()方法獲取到項(xiàng)目下全部的需求列表的List存儲(chǔ)對(duì)象咕幻,然后便可以對(duì)List中的元素進(jìn)行任意的處理渔伯,比如新增元素、刪除元素甚至是清空List肄程,從可靠性角度而言锣吼,我們其實(shí)并不希望任何調(diào)用方都可以去隨意操作所有內(nèi)容选浑,不確定性太大、難以維護(hù)玄叠。

  • 某些允許調(diào)用方進(jìn)行遍歷并刪除元素的場景古徒,容器直接通過project.getRequirements()給出具體的集合對(duì)象,然后任由調(diào)用方自行遍歷并刪除读恃,一些調(diào)用方可能會(huì)處理的不夠完善隧膘,容易踩坑,存在隱患狐粱∫ㄔⅲ可以參見我之前一篇文檔《JAVA中簡單的for循環(huán)竟有這么多坑,你踩過嗎》里的詳細(xì)說明肌蜻。

進(jìn)一步思考下互墓,其實(shí)我們只是想要遍歷獲取到容器中的元素,是否有更優(yōu)雅的方式能夠?qū)崿F(xiàn)這一簡單訴求蒋搜,并且還能順帶解決上述這幾個(gè)小遺憾呢篡撵?

帶著疑問,我們一起來梳理下容器的演進(jìn)歷程豆挽,聊聊作為一個(gè)容器應(yīng)該具備怎樣的自我修養(yǎng)吧育谬。

白盒容器

如上文中所提供的例子場景。示例中直接通過get方法將容器內(nèi)管理的元素集合給暴露出去帮哈,任由調(diào)用方自行去處理使用膛檀。調(diào)用端需要知道這是一個(gè)元素集合是一個(gè)List類型還是一個(gè)Map類型,然后再根據(jù)不同類型娘侍,決定應(yīng)該如何去遍歷其中的元素咖刃,去對(duì)其中的元素進(jìn)行操作。

image.png

白盒容器是一個(gè)典型的甩手掌柜式的容器憾筏,因?yàn)樗龅氖虑榉浅:唵危航o個(gè)get方法即可嚎杨!任何調(diào)用方都可以直接獲取到容器內(nèi)部的真正元素存儲(chǔ)集合,然后自行去對(duì)集合做各種操作氧腰,而容器則完全不管枫浙。

image.png

這樣有一定的優(yōu)勢

  • 調(diào)用方限制較小,可以按照自己訴求隨意發(fā)揮古拴,實(shí)現(xiàn)自己各種訴求

  • 容器實(shí)現(xiàn)簡單箩帚,容器與業(yè)務(wù)解耦,就是個(gè)純粹的容器斤富,不夾雜任何的業(yè)務(wù)邏輯

但是呢膏潮,原本我們只是想遍歷下容器中所有的元素內(nèi)容,但是容器卻直接將整個(gè)家底都交了出來满力。這就好比小王去小李家想看看小李家的豬里面有幾只是母豬焕参,而小李直接將豬圈丟給了小王轻纪,讓小王自己進(jìn)豬圈去數(shù)一樣,這也太不把小王當(dāng)外人了不是叠纷,誰知道小王進(jìn)去是不是僅僅只是去數(shù)了下有幾只母豬呢刻帚?

由此帶來的弊端也就很明顯了:

  • 將容器內(nèi)部的結(jié)構(gòu)完全暴露給外部,業(yè)務(wù)邏輯中耦合了容器的具體實(shí)現(xiàn)細(xì)節(jié)涩嚣,后面如果容器需要改造的時(shí)候崇众,會(huì)導(dǎo)致業(yè)務(wù)調(diào)用邏輯必須跟著改動(dòng),影響較大航厚,牽一發(fā)動(dòng)全身顷歌。

舉個(gè)簡單的例子:

當(dāng)前Project中采用List來存儲(chǔ)項(xiàng)目下所有的需求數(shù)據(jù),而所有的調(diào)用端都是按照List的格式來處理需求數(shù)據(jù)幔睬。如果現(xiàn)在需要將Project中改為使用Map來存儲(chǔ)需求數(shù)據(jù)眯漩,則原先所有通過project.getRequirements()獲取需求數(shù)據(jù)的地方,都需要配套修改麻顶。

  • 對(duì)容器內(nèi)數(shù)據(jù)的管控力太弱赦抖。容器將數(shù)據(jù)全盤給出,任由調(diào)用方隨意的去添加辅肾、刪除元素队萤、甚至是清空元素集合,而容器卻無法對(duì)其進(jìn)行約束矫钓。

還是上面的例子:

業(yè)務(wù)調(diào)用方使用project.getRequirements()拿到List對(duì)象后要尔,便可以對(duì)List進(jìn)行add、remove新娜、clear等各種操作盈电。而很多時(shí)候,我們是需要保證對(duì)元素的內(nèi)容的變更或者增減都在統(tǒng)一的地方去實(shí)行杯活,這樣可以保證數(shù)據(jù)的準(zhǔn)確、也可以做一些統(tǒng)一處理熬词,比如統(tǒng)一記錄創(chuàng)建需求的日志之類的旁钧。而寫操作入口變得不確定,使得整個(gè)數(shù)據(jù)的維護(hù)就存在很大的漏洞互拾。

黑盒容器

既然甩手掌柜式的白盒容器有著種種弊端歪今,那么我們將其變?yōu)橐粋€(gè)黑盒容器,不允許將內(nèi)部的元素集合和盤托出颜矿,這樣的話寄猩,不就解決上述所有的問題了嗎?這個(gè)思路是正確的骑疆,但是對(duì)于一個(gè)黑盒容器來說田篇,又該如何讓調(diào)用端能實(shí)現(xiàn)對(duì)內(nèi)部托管的元素的逐個(gè)遍歷獲取呢替废?

回答這個(gè)問題前,我們先來想一個(gè)問題:我們對(duì)List或者Array是怎么遍歷的泊柬?可以通過記錄下標(biāo)的方式椎镣,按照下標(biāo)所示的位置去逐個(gè)獲取下標(biāo)對(duì)應(yīng)位置的元素,然后將下標(biāo)往后移動(dòng)兽赁,再去讀取下一個(gè)位置的元素状答,一直到最后一個(gè)。對(duì)應(yīng)代碼我們?cè)偈煜げ贿^了:

public void dealWithRequirements(Project project) {
    List<Requirement> requirements = project.getRequirements();
    for (int i = 0; i < requirements.size(); i++) {
        // ...
    }
}

上述處理邏輯中刀崖,有兩個(gè)關(guān)鍵的數(shù)據(jù)對(duì)遍歷的動(dòng)作起著決定作用惊科。一個(gè)是下標(biāo)索引i,用來標(biāo)記當(dāng)前遍歷到的元素位置亮钦;另一個(gè)則是集合的總長度馆截,決定著遍歷操作是繼續(xù)還是終止。

image.png

回到當(dāng)前討論的黑盒容器中或悲,如果調(diào)用方拿不到集合自己去遍歷孙咪,就需要我們?cè)诤诤腥萜髦写嬲{(diào)用方將上述循環(huán)邏輯給自行實(shí)現(xiàn)。那么容器自身就需要知曉并記錄當(dāng)前遍歷到哪個(gè)元素下標(biāo)位置(也可以將其稱為游標(biāo)位置)巡语。而同樣由于黑盒的原因翎蹈,容器內(nèi)元素集合的總元素個(gè)數(shù)、當(dāng)前遍歷到的下標(biāo)位置等信息男公,都在黑盒內(nèi)部荤堪,調(diào)用方無法知曉,那就需要容器給個(gè)接口枢赔,告訴調(diào)用方是否已經(jīng)遍歷完了(是否還有元素沒遍歷的)

等等澄阳,越說這玩意就越覺得眼熟有木有?這不就是一個(gè)迭代器Iterator)嗎踏拜?

不錯(cuò)碎赢,對(duì)一個(gè)黑盒容器而言,迭代器可以完美實(shí)現(xiàn)對(duì)其內(nèi)部元素的遍歷訴求速梗,且不會(huì)暴露容器內(nèi)部的數(shù)據(jù)結(jié)構(gòu)肮塞。迭代器的兩個(gè)關(guān)鍵方法:

  • hasNext()

告訴調(diào)用方是否還有元素可以繼續(xù)遍歷,如果沒有了姻锁,則遍歷結(jié)束枕赵,否則繼續(xù)遍歷。

  • next()

獲取一個(gè)新的元素內(nèi)容位隶。

這樣拷窜,對(duì)于調(diào)用方而言,無需關(guān)注到底容器內(nèi)部是怎么存儲(chǔ)集合數(shù)據(jù)的,也無需知道到底有多少個(gè)集合元素篮昧,只需要使用這兩個(gè)方法赋荆,便可以輕松完成遍歷。

我們按照迭代器的思路恋谭,對(duì)Project類進(jìn)行黑盒化改造糠睡,如下:

public class Project {
    private List<Requirement> requirements;
    // ...

    private int cursor;

    public boolean hasNext() {
        return cursor < requirements.size();
    }

    public Requirement next() {
        return requirements.get(cursor++);
    }
}

接著,業(yè)務(wù)方可以按照下面的方式去遍歷:

public void dealWithIterator(Project project) {
    while (project.hasNext()) {
        Requirement requirement = project.next();
        // ...
    }
}

這樣的話疚颊,在Project內(nèi)部List類型的requirements對(duì)象沒有暴露給調(diào)用方的情況下狈孔,依舊可以完成對(duì)Project中所有的Requirement元素的遍歷處理,也自然就不用擔(dān)心調(diào)用方會(huì)對(duì)集合進(jìn)行元素新增或者刪除操作了材义。此外均抽,后續(xù)如果有需要,可以方便地將Project當(dāng)前內(nèi)部使用的List類型變更為需要的其它類型其掂,比如Array或者Set等油挥,而不用擔(dān)心需要同步修改所有外部的調(diào)用方處理邏輯。

從黑盒到迭代器

黑盒容器的出現(xiàn)款熬,有效的增強(qiáng)了容器內(nèi)部數(shù)據(jù)結(jié)構(gòu)的隱藏深寥,但是容器也需要自己去實(shí)現(xiàn)對(duì)應(yīng)的元素遍歷邏輯提供給調(diào)用方使用。

還是以上面的Project類的實(shí)現(xiàn)為例贤牛,除了當(dāng)前支持的正序遍歷邏輯惋鹅,若現(xiàn)在還需要提供一個(gè)倒序遍歷的邏輯,那么應(yīng)該怎么辦呢殉簸?

似乎也沒那么難回答闰集,再增加個(gè)遍歷邏輯就好了嘛。很快般卑,代碼就改好了:

public class Project {
    private List<Requirement> requirements;
    // ...

    private int cursor;
    private int reverseCursor = Integer.MIN;

    public boolean hasNext() {
        return cursor < requirements.size();
    }
    public Requirement next() {
        return requirements.get(cursor++);
    }

    public boolean reverseHasNext() {
        if (reverseCursor == Integer.MIN) {
            reserseCursor = requirements.size() - 1;
        }
        return reverseCursor >= 0;
    }
    public requirement reverseNext() {
        return requirements.get(reverseCursor--);
    }
}

如果需要正序遍歷武鲁,就hasNext()next()兩個(gè)方法結(jié)合使用,而通過reverseHasNext()reverseNext()組合使用便可以實(shí)現(xiàn)逆序遍歷蝠检。

image.png

回頭再來看下Project類沐鼠,作為一個(gè)容器,它似乎又變得不那么純粹了叹谁。試想一下迟杂,如果后面再有新的訴求,除了需要正序遍歷本慕、逆序遍歷之外,還需要僅遍歷偶數(shù)位置的元素侧漓,我們是不是還得再在容器中增加兩個(gè)新的方法锅尘?

我們說白盒容器是一個(gè)純粹的容器、但是存在一些明顯弊端,而黑盒容器解決了白盒容器的一些數(shù)據(jù)隱藏與管控方便的問題藤违,卻又讓自己變得冗脹浪腐、變得不再純粹了。應(yīng)該如何選擇呢顿乒?

話說议街,小孩子才要做選擇,成年人總是貪婪地全要璧榄!如何才能既保持一個(gè)容器本身的純粹特漩、又可以實(shí)現(xiàn)內(nèi)部數(shù)據(jù)的隱藏與管控呢?—— 將遍歷的邏輯外包出去唄骨杂!這里的外包員工就要登場了涂身,它便是我們姍姍來遲的主角:迭代器

image.png

繼續(xù)前面的場景搓蚪,我們可以將正序遍歷蛤售、逆序遍歷封裝為2個(gè)不同的迭代器,都實(shí)現(xiàn)相同的Iterator接口妒潭。

  • 正序遍歷
public class RequirementIterator implements Iterator<Requirement> {
    private List<Requirement> requirements;
    private int cursor;
    public RequirementIterator(List<T> requirements) {
        this.requirements = requirements;
        this.cursor = 0;
    }

    @Override
    public boolean hasNext() {
        return this.cursor < this.requirements.size();
    }

    @Override
    public Requirement next() {
        return this.requirements.get(cursor++);
    }
}
逆序遍歷
public class ReverseRequirementIterator implements Iterator<Requirement> {
    private List<Requirement> requirements;
    private int cursor;
    public ReverseRequirementIterator(List<T> requirements) {
        this.requirements = requirements;
        this.cursor = requirements.size() - 1;
    }

    @Override
    public boolean hasNext() {
        return this.cursor > 0;
    }

    @Override
    public Requirement next() {
        return this.requirements.get(cursor--);
    }
}

在容器里悴能,提供不同的迭代器獲取操作,將迭代器提供給調(diào)用方即可雳灾。

public class Project {
    private List<Requirement> requirements;

    public RequirementIterator iterator() {
        return new RequirementIterator(this.requirements);
    }

    public ReverseRequirementIterator reverseIterator() {
        return new ReverseRequirementIterator(this.requirements);
    }
}

這樣漠酿,我們便完成了將具體的遍歷邏輯從容器中剝離“外包”給第三方來實(shí)現(xiàn)了。

調(diào)用方使用時(shí)候佑女,直接向容器獲取對(duì)應(yīng)的迭代器记靡,然后直接用迭代器提供的固定的hasNext()以及next()方法進(jìn)行遍歷即可。選擇使用哪種迭代器团驱,便可以按照此迭代器提供的遍歷邏輯進(jìn)行遍歷摸吠,業(yè)務(wù)無需關(guān)注與區(qū)分。

比如需要按照逆序遍歷元素并進(jìn)行處理的時(shí)候嚎花,我們就可以這樣來調(diào)用:

public void dealWithIterator(Project project) {
    ReverseIterator reverseIterator = project.reverseIterator();
    while (reverseIterator.hasNext()) {
        Requirement requirement = reverseIterator.next();
        // ...
    }
}

按照上面的實(shí)現(xiàn)策略:

對(duì)調(diào)用方而言寸痢,只需要保證Iterator接口不變即可,根本不關(guān)注Project容器內(nèi)部的結(jié)構(gòu)或者具體遍歷邏輯實(shí)現(xiàn)細(xì)節(jié)紊选;

對(duì)容器而言啼止,內(nèi)部的實(shí)際存儲(chǔ)邏輯完全private私有,有效的控制了外部對(duì)其內(nèi)容的隨意增刪兵罢、也降低了與外部耦合献烦,后續(xù)想修改或者變更的時(shí)候只需要配合修改下迭代器實(shí)現(xiàn)即可。

對(duì)迭代器而言卖词,承載了容器中剝離的遍歷邏輯巩那,保持了容器的純粹性,自身也只需要實(shí)現(xiàn)特定的能力接口,使自己成為了容器的合格搭檔即横。

更安全的遍歷并刪除操作

將容器變?yōu)楹诤性肷⒔栌伞暗谌健钡鱽韺iT提供容器內(nèi)元素的遍歷策略,除了代碼層面更為清晰獨(dú)立东囚,還有一個(gè)很重要的原因跺嗽,就是可以在迭代器里面進(jìn)行一些增強(qiáng)處理操作,這樣可以保證容器的遍歷動(dòng)作不會(huì)因?yàn)槿萜鲀?nèi)元素出現(xiàn)變更而導(dǎo)致異常页藻,使得代碼更加的穩(wěn)健桨嫁。

以最常見的ArrayList為例,在我之前的文檔《JAVA中簡單的for循環(huán)竟有這么多坑惕橙,你踩過嗎》里瞧甩,有專門講過這方面的一個(gè)處理。比如在遍歷并且刪除元素的場景弥鹦,如果由使用方自行去遍歷且在遍歷過程中執(zhí)行刪除操作肚逸,可能會(huì)出現(xiàn)異常報(bào)錯(cuò)或者是結(jié)果與預(yù)期不符的情況。而使用ArrayList提供的迭代器去執(zhí)行此操作彬坏,就不會(huì)有任何問題朦促。為什么呢?因?yàn)?code>ArrayList的迭代器里面已經(jīng)對(duì)此操作邏輯做了充足的支持栓始,可以保證調(diào)用方無感知的情況下安全的執(zhí)行务冕。

看下ArrayList的Iterator中提供的next方法是怎么做的。首先是remove操作中增加了一些額外處理幻赚,在remove掉list本身的元素之后禀忆,也順便的更新了下輔助維護(hù)參數(shù)

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

而在執(zhí)行next()操作的時(shí)候,也會(huì)先通過checkForComodification()執(zhí)行校驗(yàn)落恼,確保數(shù)據(jù)是符合預(yù)期的情況下才會(huì)進(jìn)一步的執(zhí)行后續(xù)邏輯:

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

而上述的邏輯箩退,對(duì)于調(diào)用方而言是感知不到的 —— 實(shí)際上也無需去感知、但是卻可以保證他們獲取到想要的效果佳谦。

設(shè)計(jì)模式中的一席之地 —— 迭代器模式

編碼工作一向都是個(gè)逐步改進(jìn)優(yōu)化的過程戴涝。開始的時(shí)候,我們主要面向我們當(dāng)前的訴求進(jìn)行編碼實(shí)現(xiàn)钻蔑;到后面遇到一些類似場景或者關(guān)聯(lián)場景訴求的時(shí)候啥刻,就會(huì)需要我們?nèi)?duì)原先的代碼做變更、做擴(kuò)展咪笑、或者是修改并使其可復(fù)用可帽。針對(duì)不同應(yīng)用場景,一些良好的實(shí)現(xiàn)策略窗怒,經(jīng)過長期的實(shí)踐驗(yàn)證后脫穎而出映跟,并成為了大家普遍認(rèn)同的優(yōu)秀實(shí)踐钝满。也便是軟件開發(fā)設(shè)計(jì)中所謂的“設(shè)計(jì)模式”。

在23種設(shè)計(jì)模式中申窘,迭代器模式作為其中的行為型設(shè)計(jì)模式之一,也算是一種比較常見且比較古老的模式了孔轴。其對(duì)應(yīng)的實(shí)現(xiàn)UML類圖如下所示:

image.png

相比于上一章節(jié)中我們針對(duì)具體的Project定制實(shí)現(xiàn)的迭代器剃法,這里衍生出來的迭代器設(shè)計(jì)模式,更加注重的是后續(xù)的可復(fù)用路鹰、可擴(kuò)展 —— 這也是設(shè)計(jì)模式存在的意義之一贷洲,設(shè)計(jì)模式永遠(yuǎn)不是面向與解決某一個(gè)具體問題,而是面向某一類場景晋柱,關(guān)注讓這一類場景都按照統(tǒng)一的策略實(shí)施优构,以支持相同的能力、更好的復(fù)用性雁竞、更靈活的擴(kuò)展性钦椭。

源碼中無處不在的迭代器

迭代器作為容器元素遍歷的得力幫手,幾乎成了JDK中各種容器類的標(biāo)配碑诉,像大家比較熟悉的ArrayList彪腔、HashMap中的EntrySet等都提供了配套的Iterator實(shí)現(xiàn)類,基于Iterator類进栽,可以實(shí)現(xiàn)對(duì)元素的逐個(gè)遍歷德挣。

下面可以看幾個(gè)JDK源碼或者其他優(yōu)秀框架源碼中的迭代器應(yīng)用實(shí)踐。

JDK中的迭代器

JDK中定義了一個(gè)Iterator接口快毛,一些常見的集合類都有提供實(shí)現(xiàn)Iterator的具體迭代器實(shí)現(xiàn)類格嗅,來提供迭代遍歷的能力。

先看下Iterator接口類的定義:

public interface Iterator<E> {
    boolean hasNext();
    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }

其中hasNext()與remove()是最長被使用的唠帝,也是具體迭代器實(shí)現(xiàn)類必須要自行實(shí)現(xiàn)的方法屯掖。如果一些場景需要支持迭代過程中刪除元素,則可以選擇實(shí)現(xiàn)remove()方法没隘,而對(duì)于Java8之后的場景懂扼,也可通過實(shí)現(xiàn)forEachRemaining()方法,來支持傳入一個(gè)函數(shù)式接口的方式來對(duì)每個(gè)元素進(jìn)行處理右蒲,可以簡化我們的編碼阀湿。

按照前面章節(jié)我們的描述,一個(gè)容器雷伊根據(jù)不同的遍歷訴求瑰妄,提供多種不同的迭代器陷嘴。這一點(diǎn)在JDK源碼的各集合類中也普遍被使用。還是以ArrayList為例间坐,作為編碼中最常使用的一種集合類灾挨,ArrayList也提供了多個(gè)不同的Iterator實(shí)現(xiàn)類邑退,可以實(shí)現(xiàn)對(duì)List中元素的遍歷操作的差異化訴求。

比如源碼中我們可以看到其提供了兩個(gè)獲取迭代器的方法:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // ...

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

    public ListIterator<E> listIterator() {
        return new ListItr(0);
    }
}

其中ListIterator接口是繼承自Iterator接口的子接口劳澄,相比于Iterator接口地技,提供了更為豐富的能力、不僅支持讀取秒拔、也支持寫操作莫矗,還支持向前向后遍歷:

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}

實(shí)際使用中,調(diào)用方可以根據(jù)自身的訴求砂缩,決定具體應(yīng)該使用ArrayList提供的哪一種迭代器作谚,可以大大降低調(diào)用方的使用成本。

迭代器在數(shù)據(jù)庫操作中的身影

在項(xiàng)目中庵芭,經(jīng)常會(huì)遇到一些場景妹懒,需要我們將數(shù)據(jù)庫表中全量數(shù)據(jù)讀取到內(nèi)存中并進(jìn)行一些處理。比如需要將DB數(shù)據(jù)重新構(gòu)建ES索引的時(shí)候双吆,我們需要逐條處理DB記錄眨唬,然后將其寫入到ES中進(jìn)行索引存儲(chǔ)以方便后續(xù)搜索。如果表中數(shù)據(jù)量特別大伊诵,比如有1000萬條記錄的時(shí)候单绑,逐條去數(shù)據(jù)庫查詢的方式速度太慢、全量加載到內(nèi)存中又容易撐爆內(nèi)存曹宴,這個(gè)時(shí)候就會(huì)涉及到批量獲取的場景搂橙。

在批量獲取的場景中,往往會(huì)涉及到一個(gè)概念笛坦,叫做游標(biāo)区转。而我們本文中提到的迭代器設(shè)計(jì)模式,很多場景中也有人稱之為游標(biāo)模式版扩。借助游標(biāo)废离,我們也可以將DB當(dāng)做一個(gè)黑盒,然后對(duì)其元素進(jìn)行遍歷獲取礁芦。JAVA中的數(shù)據(jù)庫操作框架很多蜻韭,SpringData JPA作為SpringData家族中用于關(guān)系型數(shù)據(jù)庫處理的一個(gè)封裝框架,可以極大簡化開發(fā)編碼過程中對(duì)于簡單數(shù)據(jù)庫操作的編碼柿扣。

先看下實(shí)際使用SpringData JPA進(jìn)行表數(shù)據(jù)加載到ES的處理邏輯:

private <F> void fullLoadToEs() {
    try {
        long totalLoadedCount = 0L;
        Pageable pageable = PageRequest.of(0, 1000);
        do {
            Slice<F> entitySilce = repository.findAll(pageable);
            List<F> contents = entitySilce.getContent();
            // do something here...
            if (!entitySilce.hasNext()) {
                break;
            }
            pageable = entitySilce.nextPageable();
        } while (true);
    } catch (Exception e) {
        log.error("error occurred when load data into es", e);
    }
}

其實(shí)和前面介紹的迭代器使用邏輯很相似肖方,通過hasNext()判斷是否還有剩余的數(shù)據(jù)待獲取,如果有則nextPageable()可以獲取到下一個(gè)分頁查詢條件未状,然后拿著新的分頁條件俯画,去加載下一個(gè)的數(shù)據(jù)。

可以看下Slice類的源碼UML類圖:

image.png

會(huì)發(fā)現(xiàn)其實(shí)現(xiàn)了個(gè)Iterable接口司草,此接口定義源碼如下:

public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

可以發(fā)現(xiàn)艰垂,其最終也是要求實(shí)現(xiàn)類對(duì)外提供具體的迭代器實(shí)現(xiàn)類泡仗,也即最終也是基于迭代器的模式,來實(shí)現(xiàn)對(duì)DB中數(shù)據(jù)的遍歷獲取猜憎。

總結(jié)回顧

好啦娩怎,關(guān)于容器設(shè)計(jì)的相關(guān)探討與思路分享,這里就給大家介紹到這里了胰柑。適當(dāng)?shù)膱鼍爸惺褂玫骺梢宰屛覀兊拇a在滿足業(yè)務(wù)功能訴求的同時(shí)更具可維護(hù)性峦树,是我們實(shí)現(xiàn)容器類的時(shí)候的一個(gè)好幫手。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末旦事,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子急灭,更是在濱河造成了極大的恐慌姐浮,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葬馋,死亡現(xiàn)場離奇詭異卖鲤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)畴嘶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門蛋逾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人窗悯,你說我怎么就攤上這事区匣。” “怎么了蒋院?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵亏钩,是天一觀的道長。 經(jīng)常有香客問我欺旧,道長姑丑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任辞友,我火速辦了婚禮栅哀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘称龙。我一直安慰自己留拾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布茵瀑。 她就那樣靜靜地躺著间驮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪马昨。 梳的紋絲不亂的頭發(fā)上竞帽,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天扛施,我揣著相機(jī)與錄音,去河邊找鬼屹篓。 笑死疙渣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的堆巧。 我是一名探鬼主播妄荔,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谍肤!你這毒婦竟也來了啦租?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤荒揣,失蹤者是張志新(化名)和其女友劉穎篷角,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體系任,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恳蹲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俩滥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘉蕾。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖霜旧,靈堂內(nèi)的尸體忽然破棺而出错忱,到底是詐尸還是另有隱情,我是刑警寧澤挂据,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布航背,位于F島的核電站,受9級(jí)特大地震影響棱貌,放射性物質(zhì)發(fā)生泄漏玖媚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一婚脱、第九天 我趴在偏房一處隱蔽的房頂上張望今魔。 院中可真熱鬧,春花似錦障贸、人聲如沸错森。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涩维。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓦阐,已是汗流浹背蜗侈。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留睡蟋,地道東北人踏幻。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像戳杀,于是被迫代替她去往敵國和親该面。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容