Effective Java(3rd)-Item37 使用EnumMap而不是序號索引

??偶爾,你可能會看到代碼使用序號方法(item37 )對數(shù)組或列表進行索引。例如,考慮這個簡單的類代表了一個植物:

image.png

??現(xiàn)在假設(shè)你有一個花園擁有一個植物數(shù)組,你想要列出這些按生命周期分類的植物(一年生,多年生,或兩年一次)。為了達到這個效果,你構(gòu)造了三個set,一個用于每個生命周期,在花園中迭代,將每個植物放置在合適的set上。有寫程序員會通過將集合放入按生命周期序號索引的數(shù)據(jù)來做到這一點:


image.png

??這種技術(shù)有效,但是充滿了問題。因為數(shù)組與泛型不兼容(item28)。程序需要一個未經(jīng)檢查的強制轉(zhuǎn)換,將不會干凈地編譯。因為數(shù)組不知道它的索引代表了什么,你不得不手動標記輸出。但是,這種技術(shù)最嚴重的問題是,當你訪問由枚舉的序號索引的數(shù)組時,使用正確的int值是你的責任;ints不提供枚舉的類型安全。如果你使用了錯誤值,程序?qū)察o地做了錯誤的事情,或者如果你足夠幸運,將拋出ArrayIndexOutOfBoundsException。

??這里有一個更好的方式來實現(xiàn)相同的作用。數(shù)組實際上是從枚舉到值的 映射,所以最好使用Map。更具體地說,這里有一個使用枚舉key而設(shè)計的非??斓膍ap,叫作java.util.EnumMap。當程序被重寫未使用EnumMap時,如下所示:


image.png

??這個程序更短,更清晰,更安全,與原始速度相當。這里沒有不安全的強制轉(zhuǎn)換;不需要手動標記輸入因為map的keys是枚舉,它們知道如何轉(zhuǎn)換它們,并可打印字符串;在計算數(shù)組索引時不可能出現(xiàn)錯誤。EnumMap的速度與序號索引相當?shù)脑蚴荅numMap在內(nèi)部使用了這樣的數(shù)組,但它對程序員隱藏了實現(xiàn)細節(jié),聯(lián)合了Map的特性和類型安全以及數(shù)組的速度。注意到EnumMap構(gòu)造器接受key類型的Class對象:這是一個 有界的類型標記,提供了運行時泛型類型信息(item33

??使用stream( item45 )來管理map,先前的程序會更短。這是基于stream的最簡單代碼,極大地復(fù)制了前面例子的行為:

image.png

??這串代碼的問題在于,它選擇了它自己map的實現(xiàn),實際上它不會是EnumMap,所以它與顯式的EnumMap不匹配版本的空間和時間性能。為了矯正這個問題,使用Collectors.groupingBy的三個參數(shù)的形式,允許調(diào)用方確定map的實現(xiàn),使用mapFacotory參數(shù):


image.png

??這種優(yōu)化在像這樣的玩具程序中是不值得做的,但是對于一個大量使用map的程序來說可能是至關(guān)重要的。
??基于stream的版本的行為與EnumMap版本略有不同,EnumMap版本總是為每個植物生命周期生成一個嵌套的map,基于stream的版本值只生成了一個嵌套版本,如果花園包含了一個或更多具有該生命周期的植物。所以,比如,如果花園包含了一年生植物和多年生植物但是沒有兩年期植物,plantsByLifeCycle的大小在EnumMap版本中將是3,在基于stream的兩個版本中都是2.
??你可能會看到一個數(shù)組用序數(shù)索引(兩次!)來表示來自兩個枚舉值的映射。比如,這個程序使用這樣的數(shù)組來映射兩相向相變(流體向固體是通過冷凍,流體向氣體是煮,諸如此類):

??
image.png

??這個程序運行正常甚至顯得優(yōu)雅,但是外表是騙人的。想之前的簡單花園例子看到的哪有,編譯器沒有辦法值得序號和數(shù)組指數(shù)的關(guān)系。如果你在轉(zhuǎn)化表中犯錯了或忘記修改相或相的枚舉,你的程序?qū)⒃诰幾g時失敗。失敗可能是ArrayIndexOutOfBoundsException,NullPointerException,或(更糟的是)沉默的錯誤行為。以及表的大小在階段數(shù)上是二次方的,即使非空項的數(shù)目較小。
??同樣,使用EnumMap你能做得更好。因為每個狀態(tài)改變都基于一系列的狀態(tài)枚舉,最好將關(guān)系表示為從一個枚舉(”從“階段)到從第二個枚舉(”到“階段)到結(jié)果(相變)的映射。與相變相關(guān)聯(lián)的兩個階段最好通過將它們與相變枚舉相關(guān)聯(lián)來捕獲,然后使用初始化嵌套的EnumMap:


image.png

??初始化相變映射的代碼有點復(fù)雜。映射的類型是Map<Phase,Map<Phase,Transition>>,這意味著”從(源)階段映射到從(目標)階段映射到過渡階段?!按擞成涫褂脙蓚€收集器的級聯(lián)序列初始化的。第一個收集器通過源相分類,第二個從目標相映射創(chuàng)建了一個EnumMap。在第二個收集器()(x,y)->y)種的合并函數(shù)并未使用;之所以需要它,只是因為我們需要指定一個映射工廠才能獲得EnumMap,收集器提供了可伸縮的工廠。本書的上一版使用顯式迭代來初始化相變映射。代碼更冗長,但可以說更容易理解。
??現(xiàn)在,假設(shè)你想要在這個系統(tǒng)中加上新的相:等離子體,或店里話氣體。只有兩個與此階段相關(guān)的過渡:電離化,把氣體運輸?shù)降入x子體中;以及去化,將等離子體轉(zhuǎn)化未氣體。要更新基于數(shù)組的程序,你必須向階段添加一個新的常量,向Phase.Transition添加兩個新的常數(shù),并用一個新的16元素版本替換原來的9元素數(shù)組。如果將太多或太少的元素添加到數(shù)組中,或者將元素放置得亂七八糟,你就不走運了:程序?qū)幾g,但在運行時失敗?;贓numMap的版本更新,只需要將等離子體添加到相位列表中,將電離(氣體,等離子體)和去電離(等離子體,氣體)添加到相變列表中:

image.png

??這個程序負責所有其他的一切,幾乎沒有出錯的機會。在內(nèi)部映射是用數(shù)組來實現(xiàn)的,因此為了增加清晰度,安全性和易于維護,你只需在空間或時間上花費很少的費用。
??為了簡潔起見,上面的示例用null來表示沒有狀態(tài)更改(其中往返是相同的)。這不是很好的實踐,很可能在運行時導致NullPointweException.為這個問題設(shè)計一個干凈而優(yōu)雅的解決方案是令人驚訝的棘手的,由此產(chǎn)生的程序足夠長以至于它們將本item中的主要材料中減少了。
??總之,幾乎沒有使用序數(shù)索引數(shù)組的使用場景:使用EnumMap代替。如果關(guān)系代表了多層面,使用EnumMap<...,EnumMap<...>>。使用Enum.ordinal是應(yīng)用程序員很少應(yīng)該遵守的一般原則的特例( item35)

本文寫于2019.7.8,歷時2天

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • [{"reportDate": "2018-01-23 23:28:49","fluctuateCause": n...
    加勒比海帶_4bbc閱讀 881評論 1 2
  • 目錄: Android:Android 0.*Android 1.*Android 2.*Android 3.*A...
    敲代碼的令狐蔥閱讀 4,511評論 0 2
  • 若我們的共鳴體不曾被發(fā)現(xiàn) 是不是也就不會有 那么多空想雜念 徘徊在人群邊緣 在夜里書寫秘密的想念 若我們的感情脆弱...
    JJJaeL閱讀 277評論 0 1
  • 月亮還沒圓,在板房的上空如半邊括號。 我們在教室里,提前過中秋,提前做著月圓時做的事。 因為過中秋的時候有人回不了...
    如月如月閱讀 523評論 9 8
  • 海醇是個正在讀大二的學生,爽朗大方,敢愛敢恨,就是脾氣有點不好,家住農(nóng)村生活也還算過得去??粗磉叺呐笥岩粋€個的一...
    小漢堡姐姐閱讀 315評論 22 3

友情鏈接更多精彩內(nèi)容