在 Java 中實現具有多個鍵的映射
一、簡介
我們經常在程序中使用映射,作為將鍵與值相關聯的一種方式。通常在我們的 Java 程序中,尤其是在引入泛型之後,我們會讓所有的鍵都是相同的類型,所有的值都是相同的類型。例如,ID 到數據存儲中的值的映射。
在某些情況下,我們可能希望使用鍵並不總是相同類型的映射。例如,如果我們將 ID 類型從Long
更改為String,
那麼我們的數據存儲將需要支持兩種鍵類型 - Long
用於舊條目, String
用於新條目。
不幸的是,Java Map
接口不允許多種鍵類型,因此我們需要找到另一種解決方案。我們將在本文中探討實現這一目標的幾種方法。
2. 使用泛型超類型
實現這一點的最簡單方法是使用一個映射,其中鍵類型是最接近我們所有鍵的超類型。在某些情況下,這可能很容易——例如,如果我們的鍵是Long
和Double
,那麼最接近的超類型是Number
:
Map<Number, User> users = new HashMap<>();
users.get(longId);
users.get(doubleId);
但是,在其他情況下,最接近的超類型是Object
。這樣做的缺點是它完全從我們的地圖中刪除了類型安全:
Map<Object, User> users = new HashMap<>();
users.get(longId); /// Works.
users.get(stringId); // Works.
users.get(Instant.now()); // Also works.
在這種情況下,編譯器不會阻止我們傳入錯誤的類型,從而有效地從我們的映射中刪除所有類型安全。在某些情況下,這可能沒問題。例如,如果另一個類封裝映射以強制執行類型安全本身,這可能會很好。
但是,它仍然會為地圖的使用方式帶來風險。
3. 多張地圖
如果類型安全很重要,並且我們將把我們的映射封裝在另一個類中,另一個簡單的選擇是擁有多個映射。在這種情況下,我們將為每個支持的鍵提供不同的映射:
Map<Long, User> usersByLong = new HashMap<>();
Map<String, User> usersByString = new HashMap<>();
這樣做可以確保編譯器為我們保持類型安全。如果我們在這裡嘗試使用Instant
,那麼編譯器不會讓我們這樣做,所以我們在這裡是安全的。
不幸的是,這增加了複雜性,因為我們需要知道要使用哪些地圖。這意味著我們要么有不同的方法來處理不同的地圖,要么我們到處都在進行類型檢查。
這也不能很好地擴展。如果我們需要添加新的密鑰類型,我們將需要添加一個新的映射和新的檢查。對於兩個或三個密鑰類型,這是可以管理的,但很快就會變得太多。
4. 密鑰包裝類型
如果我們需要類型安全,並且我們不想要許多映射的可維護性負擔,那麼我們需要找到一種方法來擁有一個可以在鍵中具有不同值的單一映射。這意味著我們需要找到某種方法來擁有一個實際上是不同類型的單一類型。我們可以通過兩種不同的方式來實現這一點——使用單個包裝器或使用接口和子類。
4.1。單包裝類
我們有一個選擇是編寫一個可以包裝我們任何可能的密鑰類型的類。這將有一個用於實際鍵值的字段、正確的equals
和hashCode
方法,然後為每種可能的類型提供一個構造函數:
class MultiKeyWrapper {
private final Object key;
MultiKeyWrapper(Long key) {
this.key = key;
}
MultiKeyWrapper(String key) {
this.key = key;
}
@Override
public bool equals(Object other) { ... }
@Override
public int hashCode() { ... }
}
這保證是類型安全的,因為它只能用Long
或String
構造。我們可以將它用作地圖中的單一類型,因為它本身就是一個單一的類:
Map<MultiKeyWrapper, User> users = new HashMap<>();
users.get(new MultiKeyWrapper(longId)); // Works
users.get(new MultiKeyWrapper(stringId)); // Works
users.get(new MultiKeyWrapper(Instant.now())); // Compilation error
我們只需要將Long
或String
包裝在新的MultiKeyWrapper
中,以便每次訪問地圖。
這相對簡單,但會使擴展更加困難。每當我們想要支持任何其他類型時,我們都需要更改MultiKeyWrapper
類以支持它。
4.2.接口和子類
另一種選擇是編寫一個接口來表示我們的密鑰包裝器,然後為我們想要支持的每種類型編寫此接口的實現:
interface MultiKeyWrapper {}
record LongMultiKeyWrapper(Long value) implements MultiKeyWrapper {}
record StringMultiKeyWrapper(String value) implements MultiKeyWrapper {}
正如我們所見,這些實現可以使用 Java 14 中引入的 Record 功能,這將使實現變得更加容易。
和以前一樣,我們可以使用MultiKeyWrapper
作為地圖的單鍵類型。然後,我們為要使用的密鑰類型使用適當的實現:
Map<MultiKeyWrapper, User> users = new HashMap<>();
users.get(new LongMultiKeyWrapper(longId)); // Works
users.get(new StringMultiKeyWrapper(stringId)); // Works
在這種情況下,我們沒有可用於其他任何事情的類型,所以我們甚至不能一開始就編寫無效代碼。
使用此解決方案,我們不是通過更改現有類而是通過編寫新類來支持其他鍵類型。這更容易支持,但這也意味著我們對支持哪些鍵類型的控制較少。
但是,這可以通過正確使用可見性修飾符來管理。類只能實現我們的接口,如果它們可以訪問它,所以如果我們將它設為包私有,那麼只有同一個包中的類才能實現它。
5. 結論
在這裡,我們已經看到了一些表示鍵到值的映射的方法,但是鍵並不總是相同的類型。這些策略的示例可以在 GitHub 上找到。