為了做題用Java語法替代C++map的常用語法,記錄一下,剖析原理以后再補上。
1.import java.util.HashMap;//導入;
2.HashMap<K, V> map=new HashMap<K, V>();//定義map,K和V是類,不允許基本類型;
3.void clear();//清空
4.put(K,V);//設置K鍵的值為V
5.V get(K);//獲取K鍵的值
6.boolean isEmpty();//判空
7.int size();//獲取map的大小
8.V remove(K);//刪除K鍵的值,返回的是V,可以不接收
9.boolean containsKey(K);//判斷是否有K鍵的值
10.boolean containsValue(V);//判斷是否有值是V
11.Object clone();//淺克隆,類型需要強轉;如HashMap<String , Integer> map2=(HashMap<String, Integer>) map.clone();
1.繼承與實現
繼承AbstractMap<K,V>,實現Map<K,V>, Cloneable, Serializable
2.基本屬性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默認初始化大小 16 static final float DEFAULT_LOAD_FACTOR = 0.75f; //負載因子0.75 static final Entry<?,?>[] EMPTY_TABLE = {}; //初始化的默認數組 transient int size; //HashMap中元素的數量 int threshold; //判斷是否需要調整HashMap的容量
3.實現方式
jdk1.7是數組+鏈表,jdk1.8是數組+鏈表+紅黑樹。
二叉查找樹、平衡二叉樹、紅黑樹的概念
二叉查找樹,值唯一,在建樹的時候判斷插入的節點,如果比根節點小就插到左邊,比根節點大就插入到右邊,查找的時候通過判斷選擇正確的方向找下去,而不用遍歷一整棵樹,效率高。但如果插入值的時候是按順序插入的,一直加在左邊或者右邊形成一條鏈,查找和插入的效率就很慢,所以有了平衡二叉樹。
平衡二叉樹,左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。通過左旋右旋各種旋實現的,具體就不清楚了。這種旋轉就避免了二叉查找樹退化成鏈表導致查找效率過低的情況。但是嚴格控制高度的絕對值之差又導致在插入的時候頻繁地旋轉,浪費時間,所以有了紅黑樹。
紅黑樹,在每個節點加一個存儲為表示節點的顏色,非紅即黑。通過對任何一條從根到葉子的路徑上各個節點着色的方式的限制,紅黑樹確保沒有一條路徑會比其它路徑長出兩倍,在子樹高度差上沒有那么嚴格,旋轉的次數比較少。因此,紅黑樹是一種弱的平衡二叉樹。
4.了解一下hashCode
(一直以為hashCode是唯一的,錯得離譜啊)
Java中的hashCode方法就是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作為散列值。對象在jvm上的內存位置是唯一的,但是不同對象的hashcode可能相同,它還要包括其他內容,再根據一定的算法去算出一個值,算出來的可能一樣,這就是哈希沖突。
5.哈希沖突
HashMap存的是對象,那就有一個哈希值,如果哈希值一樣,用鏈表解決哈希沖突,先定位到數組下標,再去鏈表里查找。
1.7是鏈表,頭插,我猜測頭插的理由是:新加入的值應該比舊的值更有可能用到,定位到數組節點時,在頭部能更快找到。不論頭插還是尾插,都需要把整條鏈表遍歷一遍,確定key在不在鏈表里。1.7版本中,產生哈希沖突時,遍歷一條鏈表查找對象,時間復雜度時O(n),隨着鏈表越來越長,查找的時間越來越大。
為了提高這個沖突的查找效率,1.8在鏈表長度超過8時,把鏈表轉變成紅黑樹,大大減少查找時間。為了防止鏈表或紅黑樹巨大,需要了解擴容這個概念。
6.擴容機制與負載因子
初始容器容量是16,負載因子默認0.75,最大容量230。意思就是當前容量到達12(16*0.75=12)的時候,會觸發擴容機制。數據結構就是為了省時間省空間,擴容機制和負載因子的設定肯定也是為了效率。
(1)為什么負載因子是0.75?
如果負載因子太大,例如1時,只有當數組全部填充才會擴容,意味着會有大量的哈希沖突,紅黑樹變大變復雜,不利於添加查找。如果負載因子太小,例如0.5或者更低時,容量到達一半或者還不到一半的時候就開始擴容,看起來就有點浪費空間。負載因子的設定肯定是權衡了哈希沖突和容量大小。(個人推測,產生大量的對象放進容器,記錄哈希值和沖突情況,測試不同負載因子耗費的時間和空間,再用數據分析的方法多方面考慮,選一個最佳的負載因子作為默認值)如果想要空間換時間,減小負載因子,減少哈希沖突。
先了解一下put方法的流程:
- 先檢查大小,如果需要擴容就先擴容;
- 重新計算key的哈希值hash,定位到數組中的下標;
- 如果位置上沒有元素就直接插入,結束;
- 如果有元素就用equal檢查key是否相同,如果相同就把新value替換舊value
- key不同就往鏈表里繼續找,沒找到key就插入,找得到就替換舊value。
定位到數組中的下標,最簡單的方法就是對容量求模index=hash%n,然而源碼的計算方法是index=(n-1)&hash。
n是2的冪次方,n-1的二進制全是1,按位與和求模結果差不多,但是位運算是直接對內存數據進行操作,不需要轉成十進制,快。
那么每次擴容也要是2的冪次方才能保證n-1的二進制全是1,如果不全是1計算出來的index不均勻。擴容總不會擴4倍8倍,所以是2倍。
擴容時原本位置也是有規律去變化的,不會丟失原來的索引。
例如一個對象的hash二進制是10111(23),在容量為16時,對15按位與計算得到的索引為
10111
&1111
=0111(7)
當容量擴大到32時,對31按位與計算得到的索引為
10111
&11111
=10111(23)
23-7=16,16正好是擴容的大小。
7.線程不安全
在接近臨界點時,若此時兩個或者多個線程進行put操作,都會進行resize(擴容)和reHash(為key重新計算所在位置),而reHash在並發的情況下可能會形成鏈表環
。總結來說就是在多線程環境下,使用HashMap進行put操作會引起死循環,導致CPU利用率接近100%,所以在並發情況下不能使用HashMap。為什么在並發執行put操作會引起死循環?是因為多線程會導致HashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不為空,就會產生死循環獲取Entry。jdk1.7的情況下,並發擴容時容易形成鏈表環,此情況在1.8時就好太多太多了。
因為在1.8中當鏈表長度達到閾值(默認長度為8)時,鏈表會被改成樹形(紅黑樹)結構。如果刪剩節點變成7個並不會退回鏈表,而是保持不變,刪剩6個時就會變回鏈表,7不變是緩沖,防止頻繁變換。
在JDK1.7中,當並發執行擴容操作時會造成環形鏈和數據丟失的情況。
在JDK1.8中,在並發執行put操作時會發生數據覆蓋的情況。
8.哈希碰撞拒絕服務攻擊
用哈希碰撞發起拒絕服務攻擊(DOS,Denial-Of-Service attack),常見的場景是攻擊者可以事先構造大量相同哈希值的數據,然后以JSON數據的形式發送給服務器,服務器端在將其構建成為Java對象過程中,通常以Hashtable或HashMap等形式存儲,哈希碰撞將導致哈希表發生嚴重退化,算法復雜度可能上升一個數據級,進而耗費大量CPU資源。
9.和兄弟HashTable的異同
(1)繼承和實現
HashMap
是繼承自AbstractMap
類,而HashTable
是繼承自Dictionary
類。不過它們都同時實現了Map、Cloneable(可復制)、Serializable(可序列化)這三個接口。存儲的內容是基於key-value的鍵值對映射,不能有重復的key,而且一個key只能映射一個value。HashSet底層就是基於HashMap實現的。
(2)key-value
HashMap支持key-value、null-value、key-null、null-null這4種方式,但HashTable只支持key-value。
HashMap不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷,因為使用get()的時候,當返回null時,你無法判斷到底是不存在這個key,還是這個key就是null,還是key存在但value是null。
(3)擴容
HashMap:默認初始容量是16,嚴格要求是2的冪次方,每次擴容到原來的2倍
HashTable:默認初始容量是11,不要求是2的冪次方,每次擴容到原來的2倍+1
(4)求索引index
HashMap求索引時用&運算,index=(n-1)&hash
HashTable求索引用模運算,index = (hash & 0x7FFFFFFF) % n
(5)線程安全方面
HashMap線程不安全,在並法包Java. util. concurrent的作用下它有一個對應的線程安全類ConcurrentHashMap
HashTable是線程安全的,它的一些方法加了synchronized。
10.了解一下LinkedHashMap
從Linked這個名字可以知道肯定和鏈表有關,它的數據結構附加了雙向鏈表,彌補HashMap無序的缺點。
HashMap在存入的時候通過&計算索引,這個索引不是有序的,所以在遍歷HashMap的時候,無法獲得插入時的順序。而LinkedHashMap把插入的節點用鏈表連接起來,通過鏈表來遍歷,可以獲得插入時的順序。(在不知道這個東西的情況下,要我獲取HashMap的插入順序的話,我會開兩個ArrayList或者LinkedList來記錄順序,並且一一對應key和value)。線程不安全。
11.了解一下HashSet
Map是映射,那就是key-value。Set是集合,無序不重復,存的只是key,不是兩個對象組成的鍵值對key-value。底層數據結構是HashMap,它存的對象放在key里。線程不安全。
12.了解一下HashTree
底層數據結構是裸的紅黑樹,保證元素有序,沒有比較器Comparator的情況按照key的自然排序,可自定義比較器。線程不安全。
參考:https://yuanrengu.com/2020/ba184259.html