作者:明明如月學長, CSDN 博客專家,大廠高級 Java 工程師,《性能優化方法論》作者、《解鎖大廠思維:剖析《阿里巴巴Java開發手冊》》、《再學經典:《EffectiveJava》獨家解析》專欄作者。
熱門文章推薦:
- (1)《為什么很多人工作 3 年 卻只有 1 年經驗?》
- (2)《從失望到精通:AI 大模型的掌握與運用技巧》
- (3)《AI 時代,程序員的出路在何方?》
- (4)《如何寫出高質量的文章:從戰略到戰術》
- (5)《我的技術學習方法論》
- (6)《我的性能方法論》
- (7)《AI 時代的學習方式: 和文檔對話》
一、背景
Guava 的 ImmutableMap
類提供了 of
方法,可以很方便地構造不可變 Map。
ImmutableMap<Object, Object> build = ImmutableMap.of("a",1,"b",2);
然而,實際工作開發中很多人會從開始認為非常方便,后面到發現很多大家都會遇到相似的“問題”。
比如 ImmutableMap
類的 of
存在很多重載的方法,但是最多只有五個鍵值對。
有無參的方法:
/*** Returns the empty map. This map behaves and performs comparably to {@link* Collections#emptyMap}, and is preferable mainly for consistency and maintainability of your* code.** <p><b>Performance note:</b> the instance returned is a singleton.*/@SuppressWarnings("unchecked")public static <K, V> ImmutableMap<K, V> of() {return (ImmutableMap<K, V>) RegularImmutableMap.EMPTY;}
有支持一個鍵值對的方法:
/*** Returns an immutable map containing a single entry. This map behaves and performs comparably to* {@link Collections#singletonMap} but will not accept a null key or value. It is preferable* mainly for consistency and maintainability of your code.*/public static <K, V> ImmutableMap<K, V> of(K k1, V v1) {return ImmutableBiMap.of(k1, v1);}
到支持五個鍵值對的方法:
/*** Returns an immutable map containing the given entries, in order.** @throws IllegalArgumentException if duplicate keys are provided*/public static <K, V> ImmutableMap<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {return RegularImmutableMap.fromEntries(entryOf(k1, v1), entryOf(k2, v2), entryOf(k3, v3), entryOf(k4, v4), entryOf(k5, v5));}
很多人會遇到的坑:
- 超過五個鍵值對怎么辦?
- key 和 value “居然”都不能為 null?
- 同一個 key 重復 put 報錯
二、場景還原
2.1 超過 5 個鍵值對問題
雖然 of
方法很好用,但是經常會遇到超過 5 個鍵值對的情況,就非常不方便。
解法1:升級版本
在 guava 31.0 版本以后,已經拓展到了 10 個鍵值對!
/*** Returns an immutable map containing the given entries, in order.** @throws IllegalArgumentException if duplicate keys are provided* @since 31.0*/public static <K, V> ImmutableMap<K, V> of(K k1,V v1,K k2,V v2,K k3,V v3,K k4,V v4,K k5,V v5,K k6,V v6,K k7,V v7,K k8,V v8,K k9,V v9,K k10,V v10) {return RegularImmutableMap.fromEntries(entryOf(k1, v1),entryOf(k2, v2),entryOf(k3, v3),entryOf(k4, v4),entryOf(k5, v5),entryOf(k6, v6),entryOf(k7, v7),entryOf(k8, v8),entryOf(k9, v9),entryOf(k10, v10));}
解法2:使用 builder 方法
com.google.common.collect.ImmutableMap#builder
方法可以通過構造器的方式不斷 put 鍵值對,最后 build
即可,也非常方便。
ImmutableMap<Object, Object> build = ImmutableMap.builder().put("a", 1).put("b", 2).put("c", 3).put("d",4).put("e",5).put("f",6).build();
也可以參考 2.2 中的解法。
2.2 鍵值都不允許為 null
復現
很多人看到名字就知道不可“修改” 但不太清楚它的鍵值都不允許為 null。
key 為空的情況:
value 為空的情況:
真正開發時不會那么簡單,有時候需要調用某個接口獲取返回值然后再構造一個不可編輯的 Map 返回給下游使用。很可能在測試的時候都沒有出現 null 值,發布上線,發現 key 或者 value 為 null,就會造成線上問題 或者 bug。
源碼
對于 of
的多參數重載:
/*** Returns an immutable map containing the given entries, in order.** @throws IllegalArgumentException if duplicate keys are provided*/public static <K, V> ImmutableMap<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3) {return RegularImmutableMap.fromEntries(entryOf(k1, v1), entryOf(k2, v2), entryOf(k3, v3));}
/*** Verifies that {@code key} and {@code value} are non-null, and returns a new immutable entry* with those values.** <p>A call to {@link Entry#setValue} on the returned entry will always throw {@link* UnsupportedOperationException}.*/static <K, V> Entry<K, V> entryOf(K key, V value) {return new ImmutableMapEntry<>(key, value);}
ImmutableMapEntry(K key, V value) {super(key, value);checkEntryNotNull(key, value);}
static void checkEntryNotNull(Object key, Object value) {if (key == null) {throw new NullPointerException("null key in entry: null=" + value);} else if (value == null) {throw new NullPointerException("null value in entry: " + key + "=null");}}
當然,如果你比較心細的話會發現 IDE 中會有警告,也可以很大程度上避免這個問題。
解法
不如換個“殊途同歸”的辦法,先用 HashMap 去實現同一個 key 的值覆蓋的功能,然后通過 Collections.unmodifiableMap
來實現不可編輯功能。
Map<String, Object> map = new HashMap<>();map.put("a", 1);map.put("b", 2);map.put("c", 3);map.put("d", 4);map.put("e", 5);map.put("f", null);Map<String, Object> unmodifiableMap = Collections.unmodifiableMap(map);System.out.println(unmodifiableMap);
2.3 key 重復報錯
復現
如果一不小心 key 重復,也會報 java.lang.IllegalArgumentException
異常。
ImmutableMap<Object, Object> build = ImmutableMap.builder().put("a", 1).put("b", 2).put("c", 3).put("d",4).put("f",5).put("f",6).build();System.out.println(build);
源碼
/*** Returns an immutable map containing the given entries, in order.** @throws IllegalArgumentException if duplicate keys are provided*/public static <K, V> ImmutableMap<K, V> of(K k1, V v1, K k2, V v2) {return RegularImmutableMap.fromEntries(entryOf(k1, v1), entryOf(k2, v2));}
最底層會對 entry 進行校驗:
/*** Checks if the given key already appears in the hash chain starting at {@code keyBucketHead}. If* it does not, then null is returned. If it does, then if {@code throwIfDuplicateKeys} is true an* {@code IllegalArgumentException} is thrown, and otherwise the existing {@link Entry} is* returned.** @throws IllegalArgumentException if another entry in the bucket has the same key and {@code* throwIfDuplicateKeys} is true* @throws BucketOverflowException if this bucket has too many entries, which may indicate a hash* flooding attack*/@CanIgnoreReturnValuestatic <K, V> @Nullable ImmutableMapEntry<K, V> checkNoConflictInKeyBucket(Object key,Object newValue,@CheckForNull ImmutableMapEntry<K, V> keyBucketHead,boolean throwIfDuplicateKeys)throws BucketOverflowException {int bucketSize = 0;for (; keyBucketHead != null; keyBucketHead = keyBucketHead.getNextInKeyBucket()) {if (keyBucketHead.getKey().equals(key)) {if (throwIfDuplicateKeys) {checkNoConflict(/* safe= */ false, "key", keyBucketHead, key + "=" + newValue);} else {return keyBucketHead;}}if (++bucketSize > MAX_HASH_BUCKET_LENGTH) {throw new BucketOverflowException();}}return null;}
最終報錯:
static IllegalArgumentException conflictException(String conflictDescription, Object entry1, Object entry2) {return new IllegalArgumentException("Multiple entries with same " + conflictDescription + ": " + entry1 + " and " + entry2);}
解法
ImmutableMap
的 builder
除了提供 buid
之外, 在 31.0 版本之后還通過了 buildKeepingLast
和 buildOrThrow
。
可以通過 buildKeepingLast
設置當 key 重復時取后面的值。
/*** Returns a newly-created immutable map. The iteration order of the returned map is the order* in which entries were inserted into the builder, unless {@link #orderEntriesByValue} was* called, in which case entries are sorted by value.** <p>Prefer the equivalent method {@link #buildOrThrow()} to make it explicit that the method* will throw an exception if there are duplicate keys. The {@code build()} method will soon be* deprecated.** @throws IllegalArgumentException if duplicate keys were added*/public ImmutableMap<K, V> build() {return buildOrThrow();}/*** Returns a newly-created immutable map, or throws an exception if any key was added more than* once. The iteration order of the returned map is the order in which entries were inserted* into the builder, unless {@link #orderEntriesByValue} was called, in which case entries are* sorted by value.** @throws IllegalArgumentException if duplicate keys were added* @since 31.0*/public ImmutableMap<K, V> buildOrThrow() {return build(true);}/*** Returns a newly-created immutable map, using the last value for any key that was added more* than once. The iteration order of the returned map is the order in which entries were* inserted into the builder, unless {@link #orderEntriesByValue} was called, in which case* entries are sorted by value. If a key was added more than once, it appears in iteration order* based on the first time it was added, again unless {@link #orderEntriesByValue} was called.** <p>In the current implementation, all values associated with a given key are stored in the* {@code Builder} object, even though only one of them will be used in the built map. If there* can be many repeated keys, it may be more space-efficient to use a {@link* java.util.LinkedHashMap LinkedHashMap} and {@link ImmutableMap#copyOf(Map)} rather than* {@code ImmutableMap.Builder}.** @since 31.1*/public ImmutableMap<K, V> buildKeepingLast() {return build(false);}
低版本的話可以考慮先用 HashMap
構造數據,然后使用 com.google.common.collect.ImmutableMap#copyOf(java.util.Map<? extends K,? extends V>)
轉換即可。
Map<String, Object> map = new HashMap<>();map.put("a", 1);map.put("b", 2);map.put("c", 3);map.put("d", 4);map.put("f", 5);map.put("f", 6);ImmutableMap<Object, Object> build = ImmutableMap.copyOf(map);System.out.println(build);
三、為什么?
3.1 為什么默認是 5 個鍵值對?
其實 31.0 版本,已經支持 10 個鍵值對了。
此處,斗膽猜測,of
方法僅是為了提供更簡單的構造 ImmutableMap
的方法,而“通常” 5 個就足夠了。
然而,實踐中很多人發現 5 個并不夠,因此高版本中支持 10個鍵值對。
Guava 也有相關 Issues 的討論 ImmutableMap::of should accept more entries #2071
:
https://github.com/google/guava/issues/2071
3.2 為什么不允許鍵值為 null ?
Github 上也有相關討論:
Question: Why RegularImmutableMap.fromEntryArray enforces “not null” policy on values? #5844
wiki 上有相關解釋:
https://github.com/google/guava/wiki/UsingAndAvoidingNullExplained
使用 ChatGPT 對上述 wiki 進行關鍵信息提取:
在谷歌的 Guava 庫的設計哲學中,不允許在 ImmutableMap
(或其他類似的集合)中使用 null 值有幾個關鍵原因:
防止錯誤:Guava 團隊發現在 Google 的代碼庫中,大約 95% 的集合不應包含任何 null 值。允許 null 值會增加出錯的風險,比如可能導致空指針異常。讓這些集合在遇到 null 時快速失敗(fail-fast)而不是默默接受 null,對開發者來說更有幫助。
消除歧義:null 值的含義通常不明確。例如,在使用 Map.get(key) 時,如果返回 null,可能是因為映射中該鍵對應的值為 null,或者該鍵在映射中不存在。這種歧義會導致理解和使用上的困難。
提倡更清晰的實踐:在 Set 或 Map 中使用 null 值通常不是一個好的做法。更清晰的方法是在查找操作中顯式處理 null,例如,如果你想在 Map 中使用 null 作為值,最好將那個條目留空,并保持一個單獨的非空鍵集合。這樣做可以避免混淆那些映射中鍵存在但值為 null,和那些映射中根本沒有該鍵的情況。
選擇適當的替代方案:如果你確實需要使用 null 值,并且遇到了不友好處理 null 的集合實現時,Guava 建議使用不同的實現。例如,如果 ImmutableList
不滿足需求,可以使用 Collections.unmodifiableList(Lists.newArrayList())
作為替代。
總體而言,Guava 庫通過避免在其集合中使用 null,旨在提供更清晰、更健壯、且更易于維護的代碼實踐。
3.3 為什么重復 key 會報錯?
我認為,主要是為了符合“不可變”的語義,既然是不可變,那么相同的 key 不應該重復放入到 map 中。其次,也可以避免意外的數據覆蓋或丟失。
四、總結
雖然這個問題并不難,但很多人并不知道會有那么多“坑”,很多人都需要重復思考如何解決這些限制。
因此,本文總結在這里,希望對大家有幫助。