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。
這里需要說明的是,hashmap是可以存放key和value均為null的,存放在table[0]的位置,此時使用put方法在添加元素的時候,如果在table[0]中已經存入key為null的元素則給null賦上新的value值并返回后面的值,否則則初始化null的元素,存入put里面存放的值。
public static void main(String[] args) { HashMap hashMap = new HashMap(); hashMap.put(null, null); System.out.println(hashMap.get(null)); Integer a = (Integer) hashMap.put(null, 1); System.out.println(a); System.out.println(hashMap.get(null)); }
輸出為:
null
null
1
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的存儲結構,將其中的Entry元素增加一個pre指針和一個next指針,這樣,根據插入元素的順序將各個元素依次連接起來,這樣LinkedHashMap就保證了元素的順序。
繼承自HashMap,實現了Map接口,LinkedHashMap重寫了父類HashMap的get方法,實際在調用父類getEntry()方法取得查找的元素后,再判斷當排序模式accessOrder為true時(即按訪問順序排序),先將當前節點從鏈表中移除,然后再將當前節點插入到鏈表尾部。
實現LRU緩存:
LinkedHashMap和HashMap+LinkedList的操作都是類似的,LRU緩存是我最近看到一個很巧妙的東西,所以推薦大家看一下這篇文章。
對比下Hashmap、Hashtable和ConcurrentHashmap:
第一、Hashmap是線程不安全的,Hashtable和ConcurrentHashMap是線程安全的,在Hashtable中使用了關鍵字synchronized修飾,加上了同步鎖;ConcurrentHashMap在JDK1.7中采用了鎖分離的技術,每一個Segment都獨立上鎖,保證了并發的安全性;每一個Segment元素存儲的是HashEntry數組+ 鏈表,Segment的大小是一開始就確定的,后期不能再進行擴容,但是單個Segment里面的數組是可以擴容的。
但是在JDK1.8上則摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,如下圖所示,并發控制使用Synchronized和CAS來操作,每一個Node節點都是用volatile修飾的,整個看起來就像是優化過且線程安全的HashMap。
第二、Hashmap是可以存放key和value均為null的,存放在table[0]的位置,此時使用put方法在添加元素的時候,如果在table[0]中已經存入key為null的元素則給null賦上新的value值并返回后面的值,否則則初始化null的元素,存入put里面存放的值。Hashtable和ConcurrentHashMap是不可以存放null的key或者value的,原因和并發狀態下的操作有關,當在并發狀態下執行無法分辨是key沒找到的null還是有key值為null,這在多線程里面是模糊不清的,所以不允許put、get為null的元素,如果強行操作就會報空指針異常。
總結
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關注服務器之家的更多內容!
原文鏈接:https://blog.csdn.net/jiguquan3839/article/details/84546835