Java集合中的Map介面

来源:https://www.cnblogs.com/yulinfeng/archive/2018/02/26/8476573.html
-Advertisement-
Play Games

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

  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的公眾號 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 緩存 前言: 大家都聽過緩存,緩存是幹啥的呢?我們可以和json和pickle來說,兩個程式之間實現信息交互,可以通過在A程式中把數據改成json ,然後傳給B程式,通過文件這個介質。文件這個效率很低。 比方說一個是QQ,一個是微信,我們想要實現二者的交互,我們之前學了rabbitMQ,可以實現消息 ...
  • 作為程式員,要時刻保持一顆好奇心和想要學習的姿態。 練習怎樣利用搜狗微信爬取某指定微信公眾號的歷史文章。爬取微信公眾號本身難度非常大,感謝搜狗提供了一個可以爬取數據的平臺。 代碼部分參考於: https://github.com/Chyroc/WechatSogou/tree/master/wec ...
  • 遇到一個奇葩的需求。一般情況下我們列印單據,用FastReport設置列印格式,也就是就設一個模版頁而己,就是一種單據格式。如果列印的單據數據多了就自動列印多頁了,他們的格式是一樣的。也就是讀同一個模版頁。 現的需求是,如果列印N頁內容。每一頁的格式除了表體外是一樣的(也可能部份不同)。而表體取自不 ...
  • ==>(個人微信公眾號:IT知更鳥)歡迎關註<^>@<^> 1.問題描述: 金字塔5層,從上到下,星號數1、3、5、7、9,空格數4、3、2、1 。 2.問題分析 : 1)確定程式框架: 用for迴圈。 2)尋找空格和星號的規律: 空格數 5-行數;依次遞減1。 星號數 行數*2-1;依次遞增2。 ...
  • 本文主要內容 可散列類型 泛映射類型 字典 (1)字典推導式 (2)處理不存在的鍵 集合 映射的再討論 python高級——目錄 文中代碼均放在github上:https://github.com/ampeeg/cnblogs/tree/master/python高級 可散列類型 泛映射類型 字典 ...
  • Spring MVC是如何逐步簡化Servlet的編程的 Servlet和JSP是開發java Web應用程式的兩種基本技術,Spring MVC是Spring框架中用於Web應用程式開發的一個模塊,能夠清晰的瞭解到從Servlet到Spring MVC開發技術之間逐步簡化的過程對於深刻理解Spri ...
  • C++ 能夠使用流提取運算符 和流插入運算符 和插入運算符 using namespace std; class Person{ public: Person(const char str) : name(str){} int GetAge(){ return this age; } / 聲明為類的 ...
  • 1.協程(微線程)協程是一種用戶態的輕量級線程。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此: 協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...