使用Java迭代器修改数据时要小心
隨著本學(xué)期的結(jié)束,我想我會分享一個關(guān)于我對Java迭代器非常非常熟悉的小故事。
現(xiàn)實(shí)世界語境
就上下文而言,我教第二年的軟件組件課程,這是嘗試進(jìn)入該專業(yè)的學(xué)生的最后障礙。 當(dāng)然,這門課程對學(xué)生來說壓力很大,我經(jīng)常必須加倍努力,為他們提供一切成功的機(jī)會。
不幸的是,本學(xué)期我們被大流行所籠罩,不得不轉(zhuǎn)換為在線教學(xué)。 結(jié)果,我們不得不對教學(xué)做出一些快速決策,從而改變了學(xué)生的學(xué)習(xí)方式。 特別是,我們將所有的紙筆考試都轉(zhuǎn)換為在線測驗(yàn)。
對于某些學(xué)生來說,這是一個很大的祝福。 畢竟,這些測驗(yàn)并沒有比考試更困難,因此我們將其設(shè)為公開考試。 換句話說,我們使課程變得更容易讓他們通過。
當(dāng)然,學(xué)生遍布世界各地,他們無法獲得所需的幫助。 此外,學(xué)生沒有像考試那樣認(rèn)真地學(xué)習(xí)。 這種組合創(chuàng)造了一些非常糟糕的測驗(yàn)分?jǐn)?shù)。
到我們進(jìn)行第四個測驗(yàn)時,學(xué)生們已經(jīng)非常沮喪。 實(shí)際上,我從幾位教師那里聽說他們的學(xué)生已經(jīng)厭倦了“技巧性問題”。 作為一名講師,聽到這有些令人沮喪,因?yàn)樗鼈兪欠浅5湫偷目荚噯栴}。 我們并沒有為他們增加困難,但這是我第一次聽到這些抱怨。
示例問題
然后,發(fā)生了一些奇怪的事情。 我們給了他們一個我真的不知道答案的問題,結(jié)果有點(diǎn)類似以下內(nèi)容:
以下代碼片段后面的Set <NaturalNumber> nums變量的值是什么?
Set<NaturalNumber> nums = new SomeSetImplementation<>(); nums.add( new NaturalNumber2( 1 )); nums.add( new NaturalNumber2( 5 )); nums.add( new NaturalNumber2( 6 )); for (NaturalNumber n : nums) { n.increment(); }當(dāng)然,學(xué)生的選擇如下:
- nums = {1,5,6,2,6,7}
- nums = {2,6,7}
- nums = {1,5,6}
- 從提供的信息中無法分辨。
現(xiàn)在,就上下文而言,此示例中有一些內(nèi)部組件。
首先,NaturalNumber是一個可變的類,表示無界的非負(fù)整數(shù)。 換句話說,NaturalNumber的范圍可以從零到無窮大。 此外,可以使用一系列基本數(shù)學(xué)運(yùn)算來修改NaturalNumber,如下所示:
- increment()加1 this
- add(NaturalNumber n) :將n添加this
此外,這個問題讓使用的Set是類似于一個數(shù)學(xué)集合。 這里的想法是Set具有兩個主要屬性:
作為參考,如果您有興趣詳細(xì)信息,請在課程網(wǎng)站上完整地記錄這兩個組件 。 所有組件均使用“按合同設(shè)計”編寫,因此每種方法都將包括一個適當(dāng)?shù)暮贤?#xff0c;其中前置條件用@requires表示,后置條件用@ensures表示。
此外,我們使用@ restores,@ updates,@ clears和@replaces等參數(shù)模式標(biāo)記每個參數(shù)。 當(dāng)然,這超出了本文的范圍。
解決問題
現(xiàn)在,我重申一下,我一開始不確定確切的答案。 顯然,第一個答案(即{1、5、6、2、6、7})是錯誤的,因?yàn)樵黾踊A(chǔ)值不會為Set添加新值-或我認(rèn)為。 使用相同的邏輯,我還假設(shè)第三組(即{1,5,6})顯然是不正確的,因?yàn)槲覀冿@然是在改變基礎(chǔ)值。
在這一點(diǎn)上,我相當(dāng)有信心第二個答案(即{2,6,7})是正確的,我的學(xué)生中有87%也是正確的。 當(dāng)然,我有答案鍵,因此我不得不挑戰(zhàn)自己以理解為什么正確答案實(shí)際上是最終答案(即“無法從提供的信息中分辨出來。”)。
現(xiàn)在,根據(jù)本文的標(biāo)題,您可能已經(jīng)遙遙領(lǐng)先于我。 沒關(guān)系! 但是,我沒有立即得出這個結(jié)論。 相反,我退后一步,決定實(shí)際繪制Set 。
當(dāng)然,當(dāng)您嘗試這樣做時會遇到幾個主要問題。 首先,正如我之前提到的, Set沒有順序。 結(jié)果,我們?nèi)绾瓮茢嗟陂g哪個元素優(yōu)先? 我們會嘗試所有可能的配置嗎?
這些是我還沒有準(zhǔn)備好應(yīng)對的問題。 幸運(yùn)的是,事實(shí)證明,按外觀順序進(jìn)行迭代可以節(jié)省很多時間。 看一看:
{ 1 , 5 , 6 } // Initial state { 2 , 5 , 6 } // After incrementing the first element { 2 , 6 , 6 } // After incrementing the second element哦哦! 我們打破了第一條規(guī)則: Set不能包含重復(fù)項(xiàng)。 因此,我們無法確定結(jié)果Set將是什么樣。 我的最終答案是D:“無法從提供的信息中分辨出來。”
不幸的是,這種解釋并不令我滿意。 就像,我知道Set不能包含重復(fù)項(xiàng),但是打破該規(guī)則的實(shí)際后果是什么? 換句話說,如果情況如此糟糕,我們?yōu)槭裁催€要授予用戶訪問基礎(chǔ)數(shù)據(jù)的權(quán)限?
我認(rèn)為,用戶僅應(yīng)在刪除數(shù)據(jù)后才能訪問數(shù)據(jù)。 總的來說,我認(rèn)為圖書館在這方面做得很好。 如果Set沒有實(shí)現(xiàn)Iterable ,那么我們將Iterable 。
Java迭代器簡介
這給我?guī)砹艘粋€甚至更奇怪的問題:Java迭代器。 為了使此代碼起作用, Set必須實(shí)現(xiàn)Iterable,這意味著為基礎(chǔ)體系結(jié)構(gòu)定義一個Iterator。
現(xiàn)在,如果您曾經(jīng)編寫自己的迭代器,那么您就需要執(zhí)行以下操作:
new Iterator<T>() { @Override public boolean hasNext() { ... } @Override public T next() { ... } @Override public void remove() { ... } }這里,基本思想是我們定義某種可以充當(dāng)惰性數(shù)據(jù)結(jié)構(gòu)的結(jié)構(gòu)。 如果您熟悉其他語言(例如Python)的生成器表達(dá)式 ,則有相同的想法:我們創(chuàng)建了一個對象,該對象可以從一系列項(xiàng)目中一次返回一個項(xiàng)目。
在實(shí)踐中, Iterator工作方式是繼續(xù)通過next()方法提供項(xiàng),直到?jīng)]有返回值為止( 這可能永遠(yuǎn)不會發(fā)生 )。 在有界序列中,我們知道何時停止,因?yàn)閔asNext()方法將返回false 。 這些方法一起可以作為循環(huán)機(jī)制的核心:
while (iter.hasNext()) { T item = next(); }通過使一個類實(shí)現(xiàn)Iterable ,我們可以利用一些Java語法糖,稱為for-each循環(huán):
for (T item: collection) { ... }Java迭代器警告
在上面定義的問題中,我們能夠遍歷Set因?yàn)樗鼘?shí)現(xiàn)了Iterable 。
當(dāng)然,僅因?yàn)槲覀兡軌虮闅v數(shù)據(jù)結(jié)構(gòu)并不意味著我們不會遇到任何問題。 畢竟, Iterator類具有一些自己的規(guī)則。 也許最重要的規(guī)則可以在remove()方法的描述中找到:
從基礎(chǔ)集合中移除此迭代器返回的最后一個元素(可選操作)。 每次調(diào)用next()只能調(diào)用一次此方法。 如果在迭代進(jìn)行過程中以其他方式(而不是通過調(diào)用此方法)修改了基礎(chǔ)集合,則未指定迭代器的行為。
Java 8文檔 (捕獲于04/23/2020)
記住我曾說過修改NaturalNumber是不好的,因?yàn)樗赡軐?dǎo)致重復(fù)。 好吧,基于此定義,修改Set可能會導(dǎo)致無法預(yù)測的行為。
當(dāng)然,這對我提出了一個問題: 修改基礎(chǔ)集合意味著什么。 對于Java集合,for-each循環(huán)不允許從集合中添加或刪除項(xiàng)目。 在這些情況下,我們可以期望看到ConcurrentModificationException ( docs )。
現(xiàn)在,該錯誤并不普遍。 畢竟, Iterator如何知道集合是否已被修改? 事實(shí)證明,該行為是自定義地烘焙到每個集合的next()方法中的。 例如,使用List集合, 當(dāng)列表的大小更改時,拋出 ConcurrentModificationException 。 換句話說,每次調(diào)用next()都會檢查數(shù)據(jù)結(jié)構(gòu)的完整性。
由于集合利用泛型類型,因此不可能考慮可能出現(xiàn)的所有不同類型的情況。 結(jié)果, next()無法檢測是否有任何數(shù)據(jù)在沒有跟蹤狀態(tài)的情況下發(fā)生了變異。 例如,檢查列表中是否有任何值更改可能需要存儲先前狀態(tài)的副本并定期檢查該先前狀態(tài)。 那不便宜!
更糟糕的是,我們還沒有真正討論修改基礎(chǔ)數(shù)據(jù)對實(shí)際迭代過程可能產(chǎn)生的影響。 例如,如果next()以某種方式依賴于基礎(chǔ)數(shù)據(jù),則對其進(jìn)行更改顯然會更改接下來要執(zhí)行的操作。
想象一下,我們有一個用于列表的Iterator ,其項(xiàng)必須實(shí)現(xiàn)Comparable 。 然后,我們以始終返回已排序順序的下一個值的方式制作此Iterator 。 如果然后要修改基礎(chǔ)值,則可以創(chuàng)建一個永遠(yuǎn)不會遍歷整個列表的循環(huán):
[ 1 , 2 , 3 ] // next() returns 1 which we scale by 5 [ 5 , 2 , 3 ] // hasNext() claims there are no other values現(xiàn)在,這并不理想。 通常,您希望for-each循環(huán)實(shí)際上遍歷整個數(shù)據(jù)結(jié)構(gòu),而這根本沒有做到這一點(diǎn)。
再談集合問題
在這一點(diǎn)上,我們有機(jī)會從兩個不同的角度來討論Set問題:
現(xiàn)在,我想借此機(jī)會談?wù)剤?zhí)行問題代碼片段時實(shí)際可能發(fā)生的情況:
Set<NaturalNumber> nums = new SomeSetImplementation<>(); nums.add( new NaturalNumber2( 1 )); nums.add( new NaturalNumber2( 5 )); nums.add( new NaturalNumber2( 6 )); for (NaturalNumber n : nums) { n.increment(); }假設(shè)Set的Iterator沒有花哨的修改檢測,則大多數(shù)人期望的結(jié)果可能是相同的Set :{2,6,7}。
另一個可能的結(jié)果是我們得到一個Set ,其中僅某些值遞增。 就像我之前說過的那樣, next()方法可能取決于基礎(chǔ)數(shù)據(jù)來決定接下來要做什么。
在這種情況下,我們可能會得到增量輸出的任何組合:
- {2,5,6}
- {1,6,6}
- {1,5,7}
- {2,6,6}
- {2,5,7}
- {1,6,7}
無論哪種情況,我們都不是完全安全的。 當(dāng)然, Set看起來一樣,但是真的一樣嗎?
讓我們想象一下,該Set是使用哈希表實(shí)現(xiàn)的。 這提供了能夠快速檢查重復(fù)項(xiàng)的優(yōu)點(diǎn),但是需要更多的維護(hù)。 例如,如果要更改Set的值,則必須重新計算哈希并檢查沖突。
當(dāng)我們直接修改NaturalNumber ,我們將跳過此維護(hù)階段。 結(jié)果,我們的哈希表仍將包含原始的三個哈希。 例如,當(dāng)有人檢查Set中是否包含兩個時,該方法將錯誤地返回false 。
當(dāng)然,這是一個實(shí)現(xiàn)細(xì)節(jié)。 很可能根本沒有發(fā)現(xiàn)任何問題。 該程序繼續(xù)平穩(wěn)運(yùn)行,沒有人注意。 但是,與所有實(shí)現(xiàn)細(xì)節(jié)一樣,我們不能依賴于它們的假定行為。 換句話說,該程序仍然是不可預(yù)測的。
除了未成年人, Set的Java實(shí)現(xiàn)實(shí)際上指出了這個確切的問題:
注意:如果將可變對象用作集合元素,則必須格外小心。 如果對象的值更改為影響相等比較的方式,而該對象是集合中的元素,則不指定集合的??行為。 此禁止的一種特殊情況是,不允許集合將自身包含為元素。
Java Set文檔 (查看04/24/2020)
看起來很難組合一個不存在可變類型問題的Set實(shí)現(xiàn)。 我不知道那是關(guān)于可變類型的...
什么是外賣?
最后,我認(rèn)為Iterator文檔的編寫方式讓用戶玩的很好。 換句話說,當(dāng)它說:
如果在迭代進(jìn)行過程中以其他方式(而不是通過調(diào)用此方法)修改了基礎(chǔ)集合,則未指定迭代器的行為。
它的真正含義是“ 以任何方式” 。 當(dāng)然,我永遠(yuǎn)無法證實(shí)這些懷疑,所以我很想看看其他人怎么說。
同時,如果您喜歡這篇文章,那么如果您借此機(jī)會學(xué)習(xí)如何可以幫助該站點(diǎn)的發(fā)展 ,我將不勝感激。 在該文章中,您將了解我的郵件列表以及Patreon。
否則,這是一些適合您的相關(guān)文章:
- 余數(shù)運(yùn)算符在Java中用于Doubles
- 復(fù)制可變數(shù)據(jù)類型時要小心
否則,感謝您的堅持。 希望我深夜的研究生學(xué)習(xí)對您有用!
翻譯自: https://www.javacodegeeks.com/2020/04/be-careful-when-modifying-data-while-using-a-java-iterator.html
總結(jié)
以上是生活随笔為你收集整理的使用Java迭代器修改数据时要小心的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑高手都要用到的快捷键你知道几个电脑各
- 下一篇: javafx隐藏_JavaFX技巧14: