1. 程式人生 > >java HashMap 原理

java HashMap 原理

基於jdk 1.6 的HashMap

都知道HashMap 內部結構是陣列+連結串列,但是一般正常插入很少會出現連結串列,因為hash 不同,這裡模擬一下hash 相同的情況

參考: http://www.importnew.com/28263.html
參考: https://blog.csdn.net/v123411739/article/details/78996181
HashMap的wirteObject私有化 參考: http://www.a-site.cn/article/140346.html

hash 併發死迴圈 參考: https://blog.csdn.net/bigtree_3721/article/details/77123701

問題:

1. HashMap 為什麼將 wirteObject 和 readObject 方法,私有化

{@see ObjectOutputStream} 的 writeObject 方法可看出,如果 其他類有自己的writeObject 方法,會呼叫的自己的writeObject 方法,wirteObject0 -> writeOrdinaryObject -> writeSerialData() 最後執行自己的writeObject 方法

2. 怎麼才能讓entry 中形成連結串列結構?

只有兩個key值不相等但hash 值相等,這樣才能形成hash 碰撞,

才會在 Entry 中形成連結串列結構,因為是否新增新的鍵值對是根據key 的hash 值來判斷的
正常情況下, 如果table 表中的key值相等,則覆蓋原value 值,如果不相等,則新增新的鍵值對
如果key 值不相等,但是hash 值相等(table 中 索引是根據hash 值來計算的,如果hash 值 ),所以在新增key-value 時,會先查出table 表中原索引位置的key-value 值,然後用 Entry.next 紀錄下這個值,這個時候連結串列就形成了

3. 形成連結串列時,連結串列中元素的順序是怎樣的

jdk1.6,jdk1.7: 這個時候,新的鍵值會覆蓋在entry 中的value 值上,而原來的值則會紀錄在 entry.next 中,就形成了連結串列結構 (jdk 1.6 新值.next = 舊值.next = 最開始的值)
jdk1.8: 與jdk 1.6 相反: 最開始的值.next = 新值.next = 最新值

4. 常見的基於hash 碰撞的攻擊,

黑客短時間向某一個網站後臺插入大量hash 值相同的資料,造成連結串列過長,取資料的時候,我們都知道連結串列增刪快,查詢慢,短時間插入大量資料,就會造成,網站後臺執行緩慢,甚至宕機

基於這個問題:減少hash 相同值,嚴格使用泛型,確定Map的資料型別,為了增加查詢效率,jdk 1.8 HashMap 底層實現由 陣列+連結串列,轉化為 陣列+連結串列+紅黑色 結構,當連結串列值大於8時,連結串列會被拆分為紅黑色結構

模擬hash碰撞

這裡選擇3個key值, String 1, char '1',int 49,這三個key 值得hash 都是 49, null 得hash 總是0,因此只能有一個null鍵,可以有多個null值, 當get()方法返回null值時,可能是 HashMap中沒有該鍵,也可能使該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。

/**
	 * 計算map 的 key 的hash 
	 * @param key
	 * @return
	 */
	 static final int hash(Object key) {
	        int h;
	        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 可以看出如果map 有多個null, 這裡返回的hash 則總是0
	 }
	 

HashMap中的新增key-value 的方法,基於jdk1.6

void createEntry(int hash, K key, V value, int bucketIndex) {
	        Entry<K,V> e = table[bucketIndex];
	        table[bucketIndex] = new Entry<>(hash, key, value, e);
	        size++;
	    }

測試hash 碰撞的例子,在debug 檢視時,可以發現map 中有8個 資料,但hashMap 的table 屬性中只有5個值,然後在點開具體的table項,可以發現被next關聯的連結串列結構

package test.map;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;


/**
 * 基於jdk 1.6 的HashMap
 * 
 * 參考: http://www.importnew.com/28263.html
 * 參考: https://blog.csdn.net/v123411739/article/details/78996181
 * HashMap的wirteObject私有化 參考: http://www.a-site.cn/article/140346.html
 * 
 * 1. HashMap 為什麼將 wirteObject 和 readObject 方法,私有化
 * {@see ObjectOutputStream} 的 writeObject 方法可看出,如果 其他類有自己的writeObject 方法,會呼叫的自己的writeObject 方法,wirteObject0 -> writeOrdinaryObject -> writeSerialData() 最後執行自己的writeObject 方法
 * 
 * 2. 怎麼才能讓entry 中形成連結串列結構?
 * 只有兩個key值不相等但hash 值相等,這樣才能形成hash 碰撞,才會在 Entry 中形成連結串列結構,因為是否新增新的鍵值對是根據key 的hash 值來判斷的
 * 正常情況下, 如果table 表中的key值相等,則覆蓋原value 值,如果不相等,則新增新的鍵值對
 * 如果key 值不相等,但是hash 值相等(table 中 索引是根據hash 值來計算的,如果hash 值 ),所以在新增key-value 時,會先查出table 表中原索引位置的key-value 值,然後用 Entry.next 紀錄下這個值,這個時候連結串列就形成了
 * 
 * 3. 形成連結串列時,連結串列中元素的順序是怎樣的
 * jdk1.6,jdk1.7: 這個時候,新的鍵值會覆蓋在entry 中的value 值上,而原來的值則會紀錄在 entry.next 中,就形成了連結串列結構 (jdk 1.6 新值.next = 舊值.next = 最開始的值)
 * jdk1.8: 與jdk 1.6 相反: 最開始的值.next = 新值.next = 最新值
 * 
 * @author 12198
 *
 */ 
public class TestHashMap extends HashMap{
	
	
	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
		String path = "D://hashMap.data";
		HashMap map = (HashMap) getMap();
		int hash = hash("3");
//		System.out.println(hash);// hash 值是51
		int indexFor = indexFor(hash, 8);
//		System.out.println(indexFor);//算出索引
//		map.put("3", "6");
		System.out.println(map.size());
//		System.out.println(hash("1"));//算出是49
		hashBreak(map);
	}
	
	
	
	/**
	 * 模擬hash 碰撞
	 * 只有兩個key值不相等但hash 值相等,這樣才能形成hash 碰撞,才會在 Entry 中形成連結串列結構,
	 * 
	 * '1' = 49
	 * 這兩者的hash值dous  49; hash('1') = hash(49);
	 * 
	 */
	@SuppressWarnings("unchecked")
	public static void hashBreak(@SuppressWarnings("rawtypes") HashMap hashMap) {
		hashMap.put('1', "234");
		hashMap.put(49, "234");
		System.out.println(hashMap.size());
		System.out.println(hashMap.toString());
	}




	private static Map getMap() {
		HashMap<String,String> map = new HashMap<String, String>();
		map.put("1", "2");
		map.put(null, "8");
		map.put("2", "8");
		map.put("3", "8");
		map.put("4", "8");
		map.put("5", "8");
//		String put = map.put("5", "9");
//		System.out.println(put);
//		System.out.println(map.get("5"));
		return map;
	}
	
	
	
	
	//問題一,測試HashMap 的序列化 和 反序列化 =======================================
	public static  void writeObject(Map map,String path) throws FileNotFoundException, IOException{
		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(path));
		out.writeObject(map);
		out.close();
	}
	
	public static Map readObject(String path) throws FileNotFoundException, IOException, ClassNotFoundException{
		ObjectInputStream input = new ObjectInputStream(new FileInputStream(path));
		Map map = (Map) input.readObject();
		return map;
	}
	
	
	//測試 hashMap 的hash
	/**
	 * 計算map 的 key 的hash 
	 * @param key
	 * @return
	 */
	 static final int hash(Object key) {
	        int h;
	        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 可以看出如果map 有多個null, 這裡返回的hash 則總是0
	 }
	 
	 
	 
	

}

基於jdk1.8 的HashMap

新增了紅黑樹結構(TreeNode extends LinkedHashMap.Entry<K,V> extends HashMap.Node<K,V>) 實際上還是繼承的Entry 結構的Node 節點, 當某個連結串列長度大於8 的時候(見 HashMap.TREEIFY_THRESHOLD 的註釋)會擴容更改為TreeNode 結構,底層轉換為 陣列 + 連結串列 + 紅黑樹結構

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//如果是第一次put,則擴充套件map的容量
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);//如果原始節點沒有值,則直接 增加一個Node 節點 即可
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode) //如果 p 屬於 TreeNode而不是 Node 則直接增加 TreeNode 節點
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //檢視TREEIFY_THRESHOLD 屬性的註釋得知, 當連結串列的長度最少要為8,才會執行下面的操作
                            treeifyBin(tab, hash);//擴容,將普通Node 節點 擴充套件為紅黑樹節點 ,呼叫replacementTreeNode 方法,
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

treeNode 節點的繼承關係,TreeNode extends LinkedHashMap.Entry<K,V> extends HashMap.Node<K,V>

before, after 屬性來源於 LinkedHashMap.Entry (雙向連結串列,可以從兩邊開始查詢)

  /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

查詢相同hash 值,在long範圍內,傳入long 的最大值 long l = 9223372036854775806L;在 Long 類型範圍內查詢 hash=49 的數值,可以查出40-50個以上,具體有多少,沒等程式跑完

private static void findHash(long num) {
//		Random random = new Random();
		long i = 0;
		while( i < num) {
//			int nextInt = random.nextInt(num);
			long nextInt = i;
			long hash;
			if((hash = (new Long(nextInt)).hashCode()) == 49) {
				System.out.println("value="+ nextInt + ",hash = " + hash);
			}
//			System.out.println("value="+ nextInt + ",hash = " + hash);
			i++;
			
		}
	}

get(key)方法分析

public V get(Object key) {
	        Node<K,V> e;
	        return (e = getNode(hash(key), key)) == null ? null : e.value;
	    }

	
	 final Node<K,V> getNode(int hash, Object key) {
	        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	        if ((tab = table) != null && (n = tab.length) > 0 &&
	            (first = tab[(n - 1) & hash]) != null) {//根據hash 算出對應node 陣列張的索引,並判斷是否是要尋找的值
	            if (first.hash == hash && // always check first node
	                ((k = first.key) == key || (key != null && key.equals(k))))
	                return first;
	            if ((e = first.next) != null) {//繼續尋找這個索引位置的連結串列中的下一個元素,如果不為null.進行下一步判斷
	                if (first instanceof TreeNode)// 判斷下一個節點node 是否屬於TreeNode節點, 如果是轉化為TreeNode ,並得到這個節點,返回
	                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 在 紅黑樹結構中,查詢根節點,如果沒找到,則以當前節點為根節點搜尋,
	                do {//如果是普通的 node 節點,則採用 do while 迴圈查詢next 節點,直至找到被尋找的節點
	                    if (e.hash == hash &&
	                        ((k = e.key) == key || (key != null && key.equals(k))))
	                        return e;
	                } while ((e = e.next) != null);
	            }
	        }
	        return null;
	    }