本節書摘來華章計算機《資料結構與抽象:java語言描述(原書第4版)》一書中的第2章 ,第2.1.7節,[美]弗蘭克m.卡拉諾(frank m. carrano) 蒂莫西m.亨利(timothy m. henry) 著 羅得島大學 新英格蘭理工學院 辛運帏 饒一梅 譯 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
我們将從包中删除項的3個方法推遲到現在,因為3個方法中的一個有些困難,它涉及的查找機制與contains中用到的相似。我們從更容易定義的另兩個方法入手。
方法clear。方法clear從包中删除所有的項,一次删除一個。下面這個clear的定義調用方法remove,直到包為空。

循環中每次要删除哪個項是不重要的。是以,我們調用删除一個不确定項的remove方法。另外,不儲存方法傳回的這個項。
注意,因為remove方法将調用checkinitialization,是以clear不需要顯式地調用它。
注:我們可以根據尚未定義的方法remove來定義方法clear。但是,在定義remove之前,我們不能完全測試clear。 自測題10 修改方法clear的定義,讓它不調用isempty。 提示:while語句應該有一個空循環體。
自測題11 用下列語句
替換上一段的clear定義中的循環,有什麼缺點?
删除不确定項。不帶參數的方法remove從包中删除一個不确定項,隻要包不為空。回憶程式清單1-1所示的接口中給出的方法的規格說明,該方法傳回被它删除的項:
如果方法執行前包是空的,則傳回null。
從包中删除一個項,涉及從數組中删除它。雖然我們能通路數組bag中的任何項,但最後一項是最容易删除的。為此,
通路最後一項,它能被傳回。
将項的數組元素設定為null。
numberofentries減1。
numberofentries減1,就會忽略最後一項,意味着它已被高效地删除了,即使我們沒有将它在數組中的位置設定為null。但是不要跳過這一步。
将前面的步驟轉換為java程式,得到如下的方法定義:
安全說明:将數組元素bag[numberofentries???-???1]設定為null,标記被删除對象可進行垃圾回收,并防止惡意代碼來通路它。 安全說明:在正确計數後更新計數器。在前面的代碼中,删除數組最後一項後才将numberofentries減1,即使計算了3次numberofentries-1。雖然下面的改進可以避免這個重複,但時間上微不足道的節省不值得冒太早減小計數器帶來的不安全
風險:
不可否認,在這種情形下,數組和計數器不同步的情況還是有可能的。不管怎樣,如果邏輯更複雜,則數組處理過程中可能會發生異常。這個中斷将導緻已更新的計數器不準确。
自測題12 為什麼方法remove将從數組bag中删除的項替換為null?
自測題13 前一個remove方法删除數組bag中的最後一項。删除其他項為什麼會更難完成?
删除給定的項。從包中删除項的第3個方法涉及删除給定項(稱為anentry)的方法。如果項在包中出現多次,則僅删除它的一次出現。沒有指定删除哪次出現。我們隻删除查找時遇到的anentry的首次出現。正如我們在1.2節中所讨論的,方法将根據在包中是否找到這個項而傳回真或假。
假定包不為空,則查找數組bag與方法contains所做的一樣。如果anentry等于bag[index],則記下index的值。圖2-4說明成功查找後的數組。
現在需要删除bag[index]中的項。如果隻寫
則删除bag[index]中指向的項的引用,但數組中會留下空隙。即包的内容不再占據連續的數組位置,如圖2-5a所示。我們可以移動随後的項來去掉這個空隙,如圖2-5b所示,并将指向最後項的重複引用替換為null,如圖2-5c所示。但不一定用這個費時的方法。
記住,我們不需要維護包中項的具體次序。是以删除項後,不是移動數組項,而是用數組中最後面的項替換被删除的項,如圖2-6所示。找到bag[index]中的anentry後,如圖2-6a所示,将bag[numberofentries-1]中的項複制到bag[index]中(見圖2-6b)。然後将bag[numberofentries-1]中的項替換為null,如圖2-6c所示,最後numberofentries減1。
删除給定項的僞代碼。現在将前面的讨論用僞代碼寫出來,對給定的項anentry,從含有它的包中删除:
這個僞代碼假定包中含有anentry。
在僞代碼中添加一些細節,以适應anentry不在包中的情形,僞代碼如下:
避免重複工作。很容易将這段僞代碼翻譯為java方法remove。但是,如果我們這樣做了,就會發現新方法與删除不确定項中寫過的remove方法有很多類似的地方。實際上,如果anentry出現在bag[numberofentries-1]處,則這兩個remove方法将有完全相同的效果。為避免這樣的重複勞動,兩個remove方法可以調用一個私有方法來完成删除操作。可以如下說明一個方法:
因為這是一個私有方法,是以類中的其他方法可以給它傳一個下标作為參數,仍能讓這個下标(實作細節)對類的客戶隐藏。
在實作這個私有方法之前,讓我們看看是否可以用它來修改“删除不确定項”中的remove方法。因為該方法删除并傳回數組bag的最後一項,即bag[numberofentries-1],是以它的定義中可以調用removeentry(numberofentries-1)。繼續我們的工作,就如同removeentry已經定義且測試過一樣,可以如下定義remove:
這個定義看上去不錯,我們來實作第二個remove方法。
第二個remove方法。第一個remove方法不查找要删除的項,因為它删除數組中的最後一項。但第二個remove方法必須執行查找。現在先不考慮在數組中查找一個項的細節,我們将這個任務委派給另一個私有方法來完成,它的規格說明如下所示。
假定這個私有方法已經定義且測試過,我們在第二個remove方法中調用它,如下所示。
注意,removeentry傳回它删除的項或者null。這正是第一個remove方法所需要的,但第二個remove方法必須傳回一個布爾值。是以,在第二個方法中,我們必須将想删除的項與removeentry的傳回值進行比較,以便得到希望的布爾值。
自測題14 remove的前一個定義中的return語句能寫成下面這樣嗎? 自測題15 arraybag中的數組bag含有包abag中的項。如果數組含有字元串"a","a","b","a","c",為什麼abag.remove("b")将數組的内容改變為"a","a","c","a",null,而不是"a","a","a","c",null或"a","a",null,"a","c"?
私有方法removeentry的定義。現在回過頭來看在“删除給定項的僞代碼”中為從包中删除指定項而寫的僞代碼。私有方法removeentry假定項的查找已經完成,是以可以忽略僞代碼的第一步。不管怎樣,僞代碼的其他部分給出了删除一個項的基本邏輯。可以修改僞代碼,如下所示。
前一段給出的第二個remove方法的定義将getindexof傳回的整數傳給removeentry。因為getindexof可能傳回-1,是以removeentry必須對這樣一個參數值進行查找。是以,如果包不為空(即如果numberofentries大于0)且givenindex大于或等于0,則removeentry删除位于givenindex的數組項,将它用最後一項來替換,并讓numberofentries減1。然後該方法傳回删除的項。但是,如果包是空的,則該方法傳回null。
該方法的代碼如下所示。
找到要删除的項。現在需要考慮如何在包中找到要删除的項,這樣才可以将它的下标傳給removeentry。方法contains執行與remove的定義中查找anentry相同的機制。不幸的是,contains傳回真或假,它不傳回項在數組中的下标。是以在定義這個方法時不能簡單調用那個方法。
設計決策:方法contains應該傳回找到項的下标嗎?
我們應該修改contains的定義,讓它傳回一個下标而不是一個布爾值嗎?不應該。作為一個公有方法,contains不應該給客戶提供這樣的實作細節。客戶不應該期望得到放在數組中的包的項,因為它們沒有具體的次序。不應改變contains的規格說明,而是應該遵循最初的規劃,定義一個私有方法來查找一個項并傳回它的下标。
getindexof的定義。getindexof的定義與contains的定義很像,我們記得方法contains中它的循環是這樣的:
這個循環結構适合于方法getindexof,但當找到項時必須儲存index的值。該方法将傳回這個下标而不是一個布爾值。
為修改前面這個循環以便将其用在getindexof中,我們定義一個整數變量where來記錄當anentry等于bag[index]時index的值。是以getindexof是這樣的:
方法getindexof傳回where的值。注意,我們将where初始化為-1,這是沒找到anentry時傳回的值。
自測題16 在方法getindexof的return語句之前,能添加什麼assert語句來表示方法能夠傳回的可能值?
自測題17 修改方法getindexof的定義,讓它不使用布爾值。
旁白:正向思考
與方法contains不一樣,方法getindexof将布爾變量found僅用來控制循環,而不是作為傳回值。是以我們可以修改邏輯,避免取反操作符!的使用。
我們使用變量stilllooking來替代found,并将它初始化為真。然後可以将布爾表達式!found替換為stilllooking,如你在下面的方法getindexof的定義中所見:
如果在數組中找到anentry,則将stilllooking設定為假來結束循環。有些程式員傾向于正向思考,如這個版本中所述,而另一些人覺得!found就已經非常清楚了。
方法contains定義的修改。完成remove和它們調用的私有方法的定義後,我們知道方法contains可以調用私有方法getindexof,得到比方法contains中更簡單的定義。我們記得,如果anentry在包中,則表達式getindexof(anentry)傳回0~numberofentries-1的一個整數,否則傳回-1。即如果anentry在包中,則getindexof(anentry)大于-1。是以可以定義contains如下:
因為已經修改了contains的定義,是以應該再次測試它。為此,我們還要測試私有方法getindexof。
注:方法contains和remove都必須執行類似的對項的查找。将查找功能單獨放在方法contains和remove都能調用的一個私有方法中,使得我們的代碼更易調試及維護。這個政策與在兩個remove方法都調用的私有方法removeentry中定義删除操作時用到的一樣。 設計決策:什麼方法應該調用checkinitialization?
類arraybag的關鍵點是數組bag的配置設定。你已經看到,像add這樣依賴于這個數組的方法,是從調用checkinitialization開始的,以確定構造方法已經完全初始化了arraybag對象,包括數組的配置設定。而我們可以堅持在直接涉及數組bag的每個方法中調用checkinitialization,不過我們選擇更加靈活的做法。例如,私有方法getindexof和removeentry直接通路bag,但它們不調用checkinitialization。為什麼?删除給定項的remove方法調用getindexof和removeentry。如果這兩個私有方法都調用checkinitialization,則它被公有方法調用兩次。是以,對于這個具體實作來說,我們在公有方法中調用checkinitialization,并為這兩個私有方法添加一個前置條件來說明checkinitialization必須要先調用。因為它們是私有方法,是以這樣的前置條件隻給我們這些實作者和維護者使用。一旦做了這個決策,其他的remove方法和contains方法都必須調用checkinitialization,因為它們都隻調用這兩個私有方法中的一個。
注意,私有方法getindexof和removeentry都執行一個已定義的任務。它們不再為第二個任務(檢查初始化)負責。
程式設計技巧:即使可能已經有了方法的正确定義,但如果你想到了一個更好的實作,也不要猶豫地去修改它。肯定要再次測試方法!
測試。我們的arraybag類基本上完成了。可以使用前面測試remove和clear的測試方法了——我們假定它們是正确的。從一個沒滿的包開始,線上程式arraybagdemo3删除包中的項直到它為空時為止。它還包括了從滿包開始的類似的測試。最後,應該将前面的測試整合起來再次運作它們。在本書線上網站的源代碼中,測試程式是arraybagdemo,完整的類是arraybag。