大家好艘蹋,又見面了。
在我們的項(xiàng)目編碼中读整,不可避免的會(huì)用到一些容器類簿训,我們可以直接使用List
、Map
米间、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)邏輯可以用下圖來表示出來:
按照常規(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)行操作。
白盒容器是一個(gè)典型的甩手掌柜式的容器憾筏,因?yàn)樗龅氖虑榉浅:唵危航o個(gè)get
方法即可嚎杨!任何調(diào)用方都可以直接獲取到容器內(nèi)部的真正元素存儲(chǔ)集合,然后自行去對(duì)集合做各種操作氧腰,而容器則完全不管枫浙。
這樣有一定的優(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ù)還是終止。
回到當(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)逆序遍歷蝠检。
回頭再來看下Project類沐鼠,作為一個(gè)容器,它似乎又變得不那么純粹了叹谁。試想一下迟杂,如果后面再有新的訴求,除了需要正序遍歷本慕、逆序遍歷之外,還需要僅遍歷偶數(shù)位置的元素侧漓,我們是不是還得再在容器中增加兩個(gè)新的方法锅尘?
我們說白盒容器是一個(gè)純粹的容器、但是存在一些明顯弊端,而黑盒容器解決了白盒容器的一些數(shù)據(jù)隱藏與管控方便的問題藤违,卻又讓自己變得冗脹浪腐、變得不再純粹了。應(yīng)該如何選擇呢顿乒?
話說议街,小孩子才要做選擇,成年人總是貪婪地全要璧榄!如何才能既保持一個(gè)容器本身的純粹特漩、又可以實(shí)現(xiàn)內(nèi)部數(shù)據(jù)的隱藏與管控呢?—— 將遍歷的邏輯外包出去唄骨杂!這里的外包員工就要登場了涂身,它便是我們姍姍來遲的主角:迭代器。
繼續(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
類圖如下所示:
相比于上一章節(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
類圖:
會(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è)好幫手。