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 碰撞,
正常情況下, 如果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;
}