1. 程式人生 > >Java中Map介面的解析

Java中Map介面的解析

Map詳解:

先看圖,便於巨集觀瞭解Map的地位。

Map介面中鍵和值一一對映. 可以通過鍵來獲取值。

  • 給定一個鍵和一個值,你可以將該值儲存在一個Map物件. 之後,你可以通過鍵來訪問對應的值。
  • 當訪問的值不存在的時候,方法就會丟擲一個NoSuchElementException異常.
  • 當物件的型別和Map裡元素型別不相容的時候,就會丟擲一個 ClassCastException異常。
  • 當在不允許使用Null物件的Map中使用Null物件,會丟擲一個NullPointerException 異常。
  • 當嘗試修改一個只讀的Map時,會丟擲一個UnsupportedOperationException異常。

Map基本操作:

Map 初始化

Map<String, String> map = new HashMap<String, String>();

插入元素

map.put("key1", "value1");

獲取元素

map.get("key1")

移除元素

map.remove("key1");

清空map

map.clear();

hashMap原理:

hashMap是由陣列和連結串列這兩個結構來儲存資料。

陣列:儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);定址容易,插入和刪除困難;

連結串列:儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N);定址困難,插入和刪除容易。

hashMap則結合了兩者的優點,既滿足了定址,又滿足了操作,為什麼呢?關鍵在於它的儲存結構。

它底層是一個數組,陣列元素就是一個連結串列形式,見下圖:

Entry: 儲存鍵值對。

Map類在設計時提供了一個靜態修飾介面Entry。Entry將鍵值對的對應關係封裝成了鍵值對物件,這樣我們在遍歷Map集合時,就可以從每一個鍵值對物件中獲取相應的鍵與值。之所以被修飾成靜態是為了可以用類名直接呼叫。

每次初始化HashMap都會構造一個table

陣列,而table陣列的元素為Entry節點,它裡面包含了鍵key,值value,下一個節點next,以及hash

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}

檢視hashMap的API發現,它有4個建構函式:

1、構造一個具有預設初始容量 (16) 和預設載入因子 (0.75) 的空 HashMap。

2、指定初始容量和預設載入因子 (0.75) 的空 HashMap。

3、指定初始容量和預設載入因子的空HashMap。

4、構造一個對映關係與指定Map相同的新HashMap。

注意:HashMap使用的是懶載入,構造完HashMap物件後,只要不進行put方法插入元素之前,HashMap並不會去初始化或者擴容table

Put方法:

首先判斷是否是空陣列(table == EMPTY_TABLE),如果是,開始初始化HashMap的table資料結構,然後執行擴容函式,如果未指定容量,預設是大小為16的表,然後根據載入因子計算臨界值。什麼是載入因子呢?hashMap的大小是一定的,如果不夠儲存了肯定要擴容,那麼擴容的依據是什麼呢,什麼時候確定要擴容了呢?這個時候就需要引入載入因子這個概念,我們假使依舊使用預設大小16,載入因子0.75,那麼當hashMap的size大於12(16*0.75=12)的時候,那麼就會進行擴容。

回來說put方法,如果key是null,呼叫putForNullKey方法,儲存null與key,這是HashMap允許為null的原因。然後計算hash值和用indexFor計算資料存在的位置,然後從i出開始迭代e,找到 key 儲存的位置。

上面說到如果陣列擴容,那麼每次要怎麼擴容呢?

當size大於等於某一個閾值thresholdde時候且該table並不是一個空table,因為size 已經大於等於閾值了,說明Entry數量較多,雜湊衝突嚴重,那麼若該Entry對應的桶不是一個空桶,這個Entry的加入必然會把原來的連結串列拉得更長,因此需要擴容;若對應的桶是一個空桶,那麼此時沒有必要擴容。如果擴容,table會擴容為原來的兩倍,直到達到陣列的最大長度1<<30(2的30次方),如果size大於這個值,那麼就直接修改為Integer.MAX_VALUE。擴容後的元素hash值對應的新的桶位置,然後在指定的桶位置上,建立一個新的Entry。

Get方法:

Get比較好理解,判斷key是不是null,如果是,返回getForNullKey的函式返回值,如果不是,則在table中去找。

Remove方法:

判斷,如果hashMap的size是0,返回null;找到需要移除的元素的前一個節點,然後把前驅節點的next指向刪除節點的next節點,此時當前節點沒有任何引用指向,它在程式結束之後就會被gc回收。

final Entry<K,V> removeEntryForKey(Object key) {
    		if (size == 0) {
    		    return null;
    		}
   		 int hash = (key == null) ? 0 : hash(key);
   		 int i = indexFor(hash, table.length);
    		Entry<K,V> prev = table[i];
    		Entry<K,V> e = prev;
		 while (e != null) {
        		Entry<K,V> next = e.next;
       		 Object k;
       		 if (e.hash == hash &&
        		    ((k = e.key) == key || (key != null && key.equals(k)))) {
        		    modCount++;
         		   size--;
         		   if (prev == e)
          		      table[i] = next;
         		   else
          		      prev.next = next;
          		  e.recordRemoval(this);
         		   return e;
        		}
        		prev = e;
        		e = next;
   		 }
		 return e;
	}

Map的遍歷:

map這裡可以用增強for和迭代器兩種方式遍歷:

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class MapDemo {
    public static void main(String[] args) {
        HashMap<String, String> sets = new HashMap<>();
        sets.put("username", "value1");
        sets.put("password", "value2");
        sets.put("key3", "value3");
        sets.put("key4", "value4");
        sets.put(null,null);
        // 增強for迴圈 =========== keySet ===================
        for (String s : sets.keySet()) {
            System.out.println(s + ".." + sets.get(s));
        }
        //================== entrySet ======================
        for (Map.Entry<String, String> m : sets.entrySet()) {
            System.out.println(m.getKey() + ".." + m.getValue());
        }
        // 迭代器 ================ keySet ===================
        Iterator it = sets.keySet().iterator();
        while (it.hasNext()) {
            String key = (String) it.next();
            System.out.println(key + ".." + sets.get(key));
        }
        //================== entrySet ======================
        Iterator<Map.Entry<String, String>> iterator = sets.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> m = iterator.next();
            System.out.println(m.getKey() + ".." + m.getValue());
        }
    }
}

TreeMap

這裡簡要介紹下:TreeMap 是一個有序的key-value集合,繼承於AbstractMap,它是通過紅黑樹實現的。TreeMap 實現了NavigableMap介面,實現了Cloneable介面,實現了java.io.Serializable介面。

TreeMap基於紅黑樹(Red-Black tree)實現。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

紅黑樹(Red Black Tree) 是一種自平衡二叉查詢樹,是在電腦科學中用到的一種資料結構,典型的用途是實現關聯陣列。它有五個特點如下:

性質1:節點是紅色或黑色。

性質2:根節點是黑色。

性質3:每個葉節點(NIL節點,空節點)是黑色的。

性質4:每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)。

性質5:從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

詳細瞭解請點選

LinkedHashMap:

HashMap是無序的,只要不涉及執行緒安全問題,Map基本都可以使用HashMap。如果我們期待一個有序的Map,這個時候,LinkedHashMap就派上用場了,它雖然增加了時間和空間上的開銷,但是通過維護一個運行於所有條目的雙向連結串列,LinkedHashMap保證了元素迭代的順序。該迭代順序可以是插入順序或者是訪問順序。

繼承自HashMap,實現了Map介面,LinkedHashMap重寫了父類HashMap的get方法,實際在呼叫父類getEntry()方法取得查詢的元素後,再判斷當排序模式accessOrder為true時(即按訪問順序排序),先將當前節點從連結串列中移除,然後再將當前節點插入到連結串列尾部。

實現LRU快取:

LinkedHashMap和HashMap+LinkedList的操作都是類似的,LRU快取是我最近看到一個很巧妙的東西,所以推薦大家看一下這篇文章