jdk1.8.0_144 Map是Java三種集合中的一種位於java.util包中,Map作為一個介面存在定義了這種數據結構的一些基礎操作,它的最終實現類有很多:HashMap、TreeMap、SortedMap等等,這些最終的子類大多有一個共同的抽象父類AbstractMap。在Abstract ...
jdk1.8.0_144
Map是Java三種集合中的一種位於java.util包中,Map作為一個介面存在定義了這種數據結構的一些基礎操作,它的最終實現類有很多:HashMap、TreeMap、SortedMap等等,這些最終的子類大多有一個共同的抽象父類AbstractMap。在AbstractMap中實現了大多數Map實現公共的方法。本文介紹Map介面定義了哪些方法,同時JDK8又新增了哪些。
Map翻譯為“映射”,它如同字典一樣,給定一個key值,就能直接定位value值,它的存儲結構為“key : value"形式,核心數據結構在Map內部定義了一個介面——Entry,這個數據結構包含了一個key和它對應的value。首先來窺探Map.Entry介面定義了哪些方法。
interface Map.Entry<K, V>
K getKey()
獲取key值。
V getValue()
獲取value值。
V setValue(V value)
存儲value值。
boolean equals(Object o)
int hashCode()
這兩個方法我在《萬類之父——Object》中提到過,這是Object類中的方法,這兩個方法通常是同時出現,也就是說要重寫equals方法時為了保證不出現問題往往需要重寫intCode方法。而重寫equals則需要滿足5個規則(自反性、對稱性、傳遞性、一致性、非空性)。當然具體是如何重寫的,此處作為介面並不做解釋而是交由它的子類完成。
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey()
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue()
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp)
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp)
這四個方法放到一起是因為這都是JDK8針對Map更為簡單的排序新增加的泛型方法,這裡的泛型方法看似比較複雜,我們針對第一個方法先來簡單回顧一下泛型方法。
一個泛型方法的基本格式就是泛型參數列表需要定義在返回值前。這個方法的返回值返回的是Comparator<Map.Entry<K, V>>,也就是說它的泛型參數列表是“<K extends Comparable<? super K>, V>”,有兩個泛型參數K和V。參數K需要實現Comparable介面。
既然這是JDK8為Map排序新增的方法,那它是如何使用的呢? 不妨回憶下JDK8以前對Map是如何排序的:
1 /** 2 * Sort a Map by Keys.——JDK7 3 * @param map To be sorted Map. 4 * @return Sorted Map. 5 */ 6 public Map<String, Integer> sortedByKeys(Map<String, Integer> map) { 7 List<Map.Entry<String, Integer>> list = new LinkedList<>(map.entrySet()); 8 Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() { 9 @Override 10 public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { 11 return o1.getKey().compareTo(o2.getKey()); 12 } 13 }); 14 Map<String, Integer> linkedMap = new LinkedHashMap<>(); 15 Iterator<Map.Entry<Strin g, Integer>> iterator = list.iterator(); 16 while (iterator.hasNext()) { 17 Map.Entry<String, Integer> entry = iterator.next(); 18 linkedMap.put(entry.getKey(), entry.getValue()); 19 } 20 21 return linkedMap; 22 }View Code
從JDK7版本對Map排序的代碼可以看到,首先需要定義泛型參數為Map.Entry類型的List,利用Collections.sort對集合List進行排序,再定義一個LinkedHashMap,遍歷集合List中的元素放到LinkedHashMap中,也就是說並沒有一個類似Collections.sort(Map, Comparator)的方法對Map集合類型進行直接排序。JDK8對此作了改進,通過Stream類對Map進行排序。
1 /** 2 * Sort a Map by Keys.——JDK8 3 * @param map To be sorted Map. 4 * @return Sorted Map. 5 */ 6 public Map<String, Integer> sortedByKeys(Map<String, Integer> map) { 7 Map<String, Integer> result = new LinkedHashMap<>(); 8 map.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEachOrdered(x -> result.put(x.getKey(), x.getValue())); 9 return result; 10 }
可見代碼量大大減少,簡而言之,這四個方法是JDK8利用Stream類和Lambda表達式彌補Map所缺少的排序方法。
comparingByKey() //利用key值進行排序,但要求key值類型需要實現Comparable介面。
comparingByValue() //利用value值進行排序,但要求key值類型需要實現Comparable介面。
comparingByKey(Comparator) //利用key值進行排序,但key值並沒有實現Comparable介面,需要傳入一個Comparator比較器。
comparingByValue(Comparator) //利用value值進行排序,但value值並沒有實現Comparable介面,需要傳入一個Comparator比較器。
再多說一句,Comparator採用的是策略模式,即不修改原有對象,而是引入一個新的對象對原有對象進行改變,此處即如果key(或value)並沒有實現Comparable介面,此時可在不修改原有代碼的情況下傳入一個Comparator比較器進行排序,對原有代碼進行修改是一件糟糕的事情。
參考鏈接:《JDK8的新特性——Lambda表達式》《似懂非懂的Comparable與Comparator》
Map.Entry介面中定義的方法到此結束,下麵是Map介面中鎖定義的方法。
int size()
返回Map中key-value鍵值對的數量,最大值是Integer.MAX_VALUE(2^31-1)。
boolean isEmpty()
Map是否為空,可以猜測如果size() = 0,Map就為空。
boolean containsKey(Object key)
Map是否包含key鍵值。
boolean containsValue(Object value)
Map是否包含value值。
V get(Object key)
通過key值獲取對應的value值。如果Map中不包含key值則返回null,也有可能該key值對應的value值本身就是null,此時要加以區別的話可以先使用containsKey方法判斷是否包含key值。
V put(K key, V value)
向Map中存入key-value鍵值對,並返回插入的value值。
Map從JDK5過後就改為了泛型類,get方法的參數不是泛型K,而是一個Object對象呢?包括上面的containsKey(Object)和containsValue(Object)參數也是Object而不是泛型。在這個地方似乎是使用泛型更加合適。思考以下場景:
- 最開始我寫了一段代碼,定義HashMap<String, String>,定義HashMap<String, String>,此時我put("a", "a"),同時我通過get("a")獲取值。
- 寫著寫著,我發現我應該定義為HashMap<Integer, String>,此時IDE 會自動的在put("a", "a")方法報錯,因為Map的泛型參數類型key修改為了Integer,我能很好的發現它並改正。但是,我的get("a")並不會有任何提示,因為它的參數是Object能接收任意類型的值,假如我get方法同樣使用了泛型此時IDE就會提醒我這個地方參數類型不對,應該是Integer類型。那麼為什麼會出現get方法是使用Object類型,而不是泛型呢?難道JDK的作者沒有想到這一點嗎?明明能在編譯時就能發現的問題,為什麼要在運行時再去判斷?
這個問題在StackOverflow上也有討論,鏈接:https://stackoverflow. com/questions/1926285/why-does-hashmapcontainskey-take-an-parameter-of-type-object,http://smallwig.blogspot.com/2007/12/why-does-setcontains-take-object-not-e.html 我大致翻譯了一下這可能有以下幾個方面的原因:
1.這是為了保證相容性 泛型是在JDK1.5才出現的,而HashMap則是在JDK1.2才出現,在泛型出現的時候伴隨著不少相容性問題,為了保證其相容性不得不做了一些處理,例如泛型類型的擦除等等。假設在JDK1.5之前存在以下代碼:
1 HashMap hashMap = new HashMap(); 2 ArrayList arrayList = new ArrayList(); 3 hashMap.put(arrayList, "this is list"); 4 System.out.println(hashMap.get(arrayList)); 5 LinkedList linkedList = new LinkedList(); 6 System.out.println(hashMap.get(linkedList));
這段代碼在不使用泛型的時候能運行的很好,如果此時get方法中的參數變成了泛型,而不是Object,那麼此時hashMap.get(linkedList)這句話將會在編譯時出錯,因為它不是ArrayList類型。
2.無法確定Key的類型。這裡有一個例子:
1 public class HashMapTest { 2 public static void main(String[] args) { 3 HashMap<SubFoo, String> hashMap = new HashMap<>(); 4 //SubFoo是Foo類的子類 5 test(hashMap); //編譯時出錯 6 } 7 8 public static void test(HashMap<Foo, String> hashMap) { //參數為HashMap,key值是Foo類,但是不能接收它的子類 9 System.out.println(hashMap.get(new Foo())); 10 } 11 }
上面這種情況把test方法中的參數類型修改為HashMap<? extends Foo, String>即可。但是這是在get方法的參數類型是Object情況下才正確,如果get方法的參數類型是泛型,那它對於“? extends Foo”是一無所知的,換句話說,編譯器不知道它應該接收Foo類型還是SubFoo類型,甚至是SubSubFoo類型。對於第二個假設,不少網友指出,get方法的參數類型可以是“<T extends E>”,這就能避免第二個問題了。
在國外網友的討論中,我還是比較傾向於第一種相容性問題,畢竟泛型相對來說較晚出現,對於作者John也說過,他們嘗試把它泛型化,但泛型化過後產生了一系列的問題,這不得不使得他們放棄將其泛型化。其實在源碼的get方法註釋中能看到put以前也是Object類型,在泛型出現過後,put方法能成功的改造成泛型,而get由於要考慮相容性問題不得不放棄將它泛型化。
V remove(Object key)
刪除Map中的key-value鍵值對。
void putAll(Map<? extends K, ? extends V> m)
這個方法的參數是一個Map,將傳入的Map全部放入此Map中,當然對參數Map有要求,“? extends K”意味著傳入的Map其key值需要是此Map的key或者是子類,value同理。
void clear()
移除Map中所有的key-value鍵值對。
Set<K> keyset()
返回key的set集合,註意set是無序且不可存儲重覆的值,當然Map中也不可能存在重覆的key值,也沒有有序無序一說。其實這個方法的運用還是有點意思的,這會涉及到Java對象引用相關的一些知識。
1 Map<String, Integer> map = new HashMap<String, Integer>(); 2 map.put("a", 1); 3 map.put("b", 2); 4 System.out.println(map.keySet()); //output: [a, b] 5 Set<String> sets = map.keySet(); 6 sets.remove("a"); 7 System.out.println(map.keySet()); //output: [b] 8 sets.add("c"); //output: throws UnsupportedOperationException 9 System.out.println(map.keySet());
第4行的輸出的是Map中key的set集合,即“[a,b]” 。
接著創建一個set對象指向map.keySet()方法返回set的集合,並且通過這個set對象刪除其中的“a”元素。此時再來通過map.keySet()方法列印key的集合,會發現此時列印“[b]”。這是因為我們在虛擬機棧上定義的sets對象其指針指向的是map.keySet()返回的對象,也就是說這兩者指向的是同一個地址,那麼只要任一一個對其改變都會影響這個對象本身,這也是Map介面對這個方法的定義,同時Map介面對該方法還做了另外一個限制,不能通過keySet()返回的Set對象對其進行add操作,此時將會拋出UnsupportedOperationException異常,原因很簡單如果給Set對象add了一個元素,相對應的Map的key有了,那麼它對應的value值呢?
Collection<V> values()
返回value值的Collection集合。這個集合就直接上升到了集合的頂級父介面——Collection。為什麼不是Set對象了呢?原因也很簡單,key值不能重覆返回Set對象很合理,但是value值肯定可以重覆,返回Set對象顯然不合適,如果僅僅返回List對象,那也不合適,索性返回頂級父介面——Collection。
Set<Map.Entry<K, V>> entrySet()
返回Map.Entry的Set集合。
boolean equals(Object o)
int hashCode()
equals在Object類中只是用“==”簡單的實現,對於比較兩個Map是否值相等顯然需要重寫equals方法,重寫equals方法通常需要重寫hashCode方法。重寫equals方法需要遵守5個原則:自反性、對稱性、傳遞性、一致性、非空性。在滿足了這個幾個原則後還需要滿足:兩個對象equals比較相等,它們的hashCode散列值也一定相等;但hashCode散列值相等,兩個對象equals比較不一定相等。
default V getOrDefault(Object key, V defaultValue)
這個方法是JDK8才出現的,並且使用了JDK8的一個新特性,在介面中實現一個方法,叫做default方法,和抽象類類似,default方法是一個具體的方法。這個方法主要是彌補在編碼過程中遇到的這樣場景:如果一個Map不存在某個key值,則存入一個value值。以前是會寫一個判斷使用contanisKey方法,現在則只需要一句話就可以搞定map.put("a", map.getOrDefault("a", 2)); 它的實現也很簡單,就是判斷key值在Map中是否存在,不存在則存入getOrDefault中的defaultValue參數,存在則再存入一次以前的value參數。 (((v = get(key)) != null) || containsKey(key)) ? v : defaultValue;
default void forEach(BiConsumer<? super K, ? super V> action)
這個方法也是JDK8新增的,為了更方便的遍歷,這個方法幾乎新增在JDK8的集合中,使用這個新的API能方便的遍歷集合中的元素,這個方法的使用需要結合Lambda表達式:map.forEach((k, v) -> System.out.println("key=" + k + ", value=" + v))
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
替換Map中的value值,Lambda表達式作為參數,例如:
1 map.replaceAll((k, v) -> 10); //將Map中的所有值替換為10 2 map.replaceAll((k, v) -> { //如果Map中的key值等於a,其value則替換為10 3 if (k.equals("a")) { 4 return 10; 5 } 6 return v; 7 });
default V putIfAbsent(K key, V value)
在ConcurrentHashMap中也有一個putIfAbsent方法,那個方法指的key值不存在就插入,存在則不插入。JDK8中在Map中直接也新增了這個方法,這個方法ConcurrentHashMap#putIfAbsent含義相同,這個方法等同於:
1 if (!map.containsKey(key, value)) { 2 map.put(key, value); 3 } else { 4 map.get(key); 5 }
在之前提到了一個方法和這個類似——getOrDefault。註意不要搞混了,調用putIfAbsent會直接插入,而getOrDefault不會直接插入到Map中。
default boolean remove(Object key, Object value)
原來的remove方法是直接傳遞一個key從Map中移除對應的key-value鍵值對。新增的方法需要同時滿足key和value同時在Map有對應鍵值對時才刪除
default boolean replace(K key, V oldValue, V newValue)
和replaceAll類似,當參數中的key-oldValue鍵值對在Map存在時,則使用newValue替換oldValue。
default V replace(K key, V value)
這個方法是上面方法的重載,不會判斷key值對應的value值,而是直接使用value替換key值原來對應的值。
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
如果Map中不存在key值,則調用Lambda表達式中的函數主體計算value值,再放入Map中,下次再獲取的時候直接從Map中獲取。這其實在Map實現本地緩存中隨處可見,這個方法類似於下列代碼:
1 if (map.get(key) == null) { 2 value = func(key); //計算value值 3 map.put(key, value); 4 } 5 return map.get(key);
default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
這個方法給定一個key值,通過Lambda表達式可計算自定義key和value產生的新value值,如果新value值為null,則刪除Map中對應的key值,如果不為空則用新的替換舊的值。
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
這個方法是上面兩個方法的結合,有同時使用到上面兩個的地方可使用這個方法代替,其中Lambda表達式的函數主體使用三木運算符。
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
“合併”,意味著舊值和新值都會參與計算並複製。給定key和value值參數,如果key值在Map中存在,則將舊value和給定的value一起計算出新value值作為key的值,如果新value為null,那麼則從Map中刪除key。如果key不存在,則將給定的value值直接作為key的值。
Map映射集合類型作為Java中最重要以及最常用的數據結構之一,Map介面是它們的基類,在這個介面中定義了許多基礎方法,而具體的實習則由它的子類完成。JDK8在Map介面中新值了許多default方法,這也為我們在實際編碼中提供了很大的便利,如果是使用JDK8作為開發環境不妨多多學習使用新的API。
這是一個能給程式員加buff的公眾號