Java集合中的Map接口


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而不是泛型。在這個地方似乎是使用泛型更加合適。思考以下場景:

  1. 最開始我寫了一段代碼,定義HashMap<String, String>,定義HashMap<String, String>,此時我put("a", "a"),同時我通過get("a")獲取值。
  2. 寫着寫着,我發現我應該定義為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-objecthttp://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的公眾號 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM