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

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

??這種技術(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時,如下所示:

??這個程序更短,更清晰,更安全,與原始速度相當。這里沒有不安全的強制轉(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ù)制了前面例子的行為:

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

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

??這個程序運行正常甚至顯得優(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:

??初始化相變映射的代碼有點復(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的版本更新,只需要將等離子體添加到相位列表中,將電離(氣體,等離子體)和去電離(等離子體,氣體)添加到相變列表中:

??這個程序負責所有其他的一切,幾乎沒有出錯的機會。在內(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天