摘要:當(dāng)哈希表中的鍵值對(duì)的數(shù)量超過當(dāng)前容量與負(fù)載因子的乘積時(shí),哈希表將會(huì)進(jìn)行操作,使哈希表的桶數(shù)增加一倍左右。只有散列值相同且相同才是所要找的節(jié)點(diǎn)。
HashMap容器 1. 簡(jiǎn)介
HashMap基于散列表實(shí)現(xiàn)了Map接口,提供了Map的所有可選操作,HashMap與Hashtable大致相同,區(qū)別在于HashMap不支持同步而且HashMap中存儲(chǔ)的鍵值都可以為null。HashMap中不保證散列表的順序。
當(dāng)散列函數(shù)將元素正確地分散到各個(gè)桶之中的時(shí)候,HashMap中存取操作的時(shí)間復(fù)雜度都是O(1)。當(dāng)HashMap實(shí)例的容量(capacity)為M,存儲(chǔ)的鍵值對(duì)的數(shù)量(size)為N時(shí),遍歷HashMap的時(shí)間復(fù)雜度為O(M+N)。
影響一個(gè)HashMap實(shí)例的性能的兩個(gè)參數(shù)分別是:initial capacity(初始容量)和 load factor(負(fù)載因子)。容量表示哈希表中桶的個(gè)數(shù),初始容量就是HashMap實(shí)例在構(gòu)造化時(shí)初始化的容量;負(fù)載因子則控制哈希表在多滿的時(shí)候需要自動(dòng)擴(kuò)容。當(dāng)哈希表中的鍵值對(duì)(entry)的數(shù)量超過當(dāng)前容量與負(fù)載因子的乘積時(shí),哈希表將會(huì)進(jìn)行rehash操作,使哈希表的桶數(shù)增加一倍左右。
一般默認(rèn)的負(fù)載因子取0.75,可以較好地平衡時(shí)間與空間花費(fèi)。過高的負(fù)載因子雖然節(jié)省空間,但是增加了訪問哈希表的時(shí)間消耗。在設(shè)置初始容量時(shí)需要考慮散列表中期望存放的鍵值對(duì)數(shù)量以及負(fù)載因子,減少rehash的次數(shù)。
2. 方法 2.1 構(gòu)造方法HashMap的構(gòu)造方法共有四種重載形式,可以在構(gòu)造時(shí)指定HashMap的初始容量、負(fù)載因子或者使用已有的HashMap來初始化新的HashMap。
HashMap2.2 添加元素map1 = new HashMap<>(); // 創(chuàng)建空的HashMap,默認(rèn)容量16,默認(rèn)負(fù)載因子0.75 System.out.println(map1); // {} int cap = 50; HashMap map2 = new HashMap<>(cap); // 創(chuàng)建空的HashMap,初始化容量為cap,默認(rèn)負(fù)載因子0.75 System.out.println(map2); // {} float lf = 0.5f; HashMap map3 = new HashMap<>(cap, lf); // 創(chuàng)建空的HashMap,初始化容量為cap,默認(rèn)負(fù)載因子lf System.out.println(map3); // {} HashMap map4 = new HashMap<>(map3); // 使用另一個(gè)HashMap來創(chuàng)建一個(gè)新的HashMap System.out.println(map4); // {} System.out.println("map4 == map3? "+(map4 == map3)); // 不是同一個(gè)HashMap對(duì)象 System.out.println("map4.equals(map3)? "+(map4.equals(map3))); // HashMap中的內(nèi)容相等
HashMap提供put(K key, V value) 、putAll(Map extends K, ? extends V> m)
以及putIfAbsent(K key, V value)方法向HashMap添加單個(gè)鍵值對(duì)或添加指定HashMap中的所有鍵值對(duì)。
map1.put("a", 100); // 添加{k:v}記錄 System.out.println("map1:"+map1); // map1:{a=100} map2.putAll(map1); // 復(fù)制另一個(gè)HashMap的所有鍵值對(duì) System.out.println("map2:"+map2); // map2:{a=100} int ret1 = map1.putIfAbsent("a", 200); // 嘗試添加記錄{k:v},若k已存在且指向非null,返回當(dāng)前HashMap的k所指對(duì)象 System.out.println("map1:"+map1+" return:"+ret1); // map1:{a=100} return:100 Object ret2 = map1.putIfAbsent("b", 200); // 若k不存在或指向null,添加{k:v}記錄,返回null System.out.println("map1:"+map1+" return:"+ret2); // map1:{a=100, b=200} return:null2.3 刪除元素
HashMap提供重載的remove()方法刪除HashMap中的鍵值對(duì)。
int ret3 = map1.remove("a"); // 根據(jù)指定的鍵刪除一個(gè)鍵值對(duì) System.out.println("map1:"+map1+" return:"+ret3); // map1:{b=200} return:100 boolean ret4 = map1.remove("b", 300); // 刪除指定的鍵值對(duì),若k不指向v,則不刪除,返回是否成功刪除 System.out.println("map1:"+map1+" return:"+ret4); // map1:{b=200} return:false boolean ret5 = map1.remove("b", 200); System.out.println("map1:"+map1+" return:"+ret5); // map1:{} return:true2.4 訪問元素
HashMap提供get(Object key)和getOrDefault(Object key, V defaultValue)獲取指定鍵對(duì)應(yīng)的值,若HashMap中無該鍵的記錄,前者返回null后者返回默認(rèn)值。
int val1 = map2.get("a"); // 獲取指定鍵的值,若無記錄返回null System.out.println("map2:"+map2+" a:"+val1); // map2:{a=100} a:100 int val2 = map2.getOrDefault("b", 200); // 獲取指定鍵的值,若無記錄則返回指定值 System.out.println("map2:"+map2+" b:"+val2); // map2:{a=100} b:2002.5 元素變更
HashMap提供重載的replace()方法,用于更新HashMap中指定的鍵值對(duì)。
int ret6 = map2.replace("a", 300); // 更新HashMap中存在的某個(gè)鍵對(duì)應(yīng)的值,返回更新前的值 System.out.println("map2:"+map2+" return:"+ret6); // map2:{a=300} return:100 Object ret7 = map2.replace("c", 300); // 更新HashMap中某個(gè)鍵的值,若無該鍵的記錄,直接返回null System.out.println("map2:"+map2+" return:"+ret7); // map2:{a=300} return:null boolean ret8 = map2.replace("a", 300, 400); // 更新HashMap中存在的指定鍵值對(duì),返回操作是否成功 System.out.println("map2:"+map2+" return:"+ret8); // map2:{a=400} return:true boolean ret9 = map2.replace("a", 100, 200); // 指定的鍵值對(duì)不對(duì)應(yīng) System.out.println("map2:"+map2+" return:"+ret9); // map2:{a=400} return:false boolean ret10 = map2.replace("b", 100, 200); // 無指定的鍵 System.out.println("map2:"+map2+" return:"+ret10); // map2:{a=400} return:false // void replaceAll(BiFunction super K,? super V,? extends V> function)2.6 HashMap操作
clear()方法用于刪除所有元素;
isEmpty()方法檢查HashMap是否為空;
size()方法返回HashMap中鍵值對(duì)的數(shù)量;
containsKey(Object key)方法檢查HashMap中是否有指定的鍵的記錄;
containsKey(Object value)方法檢查HashMap中是否有指定的值的記錄;
boolean ret11 = map1.containsKey("a"); // 檢查HashMap中是否有指定的鍵的記錄 System.out.println("map1:"+map1+" return:"+ret11); // map1:{a=100, b=200, c=100} return:true boolean ret12 = map1.containsKey("d"); System.out.println("map1:"+map1+" return:"+ret12); // map1:{a=100, b=200, c=100} return:false boolean ret13 = map1.containsValue(100); // 檢查HashMap中是否有指定的值的記錄 System.out.println("map1:"+map1+" return:"+ret13); // map1:{a=100, b=200, c=100} return:true boolean ret14 = map1.containsValue(500); System.out.println("map1:"+map1+" return:"+ret14); // map1:{a=100, b=200, c=100} return:false
entrySet()方法以Set的形式返回HashMap中的每個(gè)鍵值對(duì)(Entry);
Set ret15 = map1.entrySet(); // 以Set的形式返回HashMap中的每個(gè)鍵值對(duì)(Entry) System.out.println("map1:"+map1+" Set:"+ret15); // map1:{a=100, b=200, c=100} // Set :[a=100, b=200, c=100]
keySet()方法以Set的形式返回HashMap中的每個(gè)鍵;
Set ret17 = map1.keySet(); // 以Set的形式返回HashMap中的每個(gè)鍵 System.out.println("map1:"+map1+" Set:"+ret17); // map1:{a=100, b=200, c=100} // Set :[a, b, c]
values()方法以Collection的形式返回HashMap中的值。
Collection ret19 = map1.values(); // 以Collection的形式返回HashMap中的值 System.out.println("map1:"+map1+" Collection3. 源碼分析:"+ret19); // map1:{a=100, b=200, c=100} // Collection :[100, 200, 100]
版本:jdk 1.8
3.1 存儲(chǔ)結(jié)構(gòu)HashMap中使用一個(gè)數(shù)組存儲(chǔ)鍵值對(duì)(Node
/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node[] table;
每個(gè)Node
static class Node3.2 put()方法implements Map.Entry { final int hash; final K key; V value; Node next; ... }
put(K key, V value)方法向HashMap中添加一個(gè)鍵值對(duì),
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * ... */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
其會(huì)對(duì)輸入的key調(diào)用hash()方法,再調(diào)用putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法實(shí)現(xiàn),
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; /*判斷是否需要擴(kuò)容,若需要?jiǎng)t擴(kuò)容,最后得到HashMap的當(dāng)前容量*/ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /*計(jì)算新添加條目在散列表的位置,若hash值沒發(fā)生沖突,直接添加新條目*/ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); /*hash值沖突*/ else { Node e; K k; /*若新添加的條目的key與已有的key相同,則覆蓋*/ if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; /*若該桶中已經(jīng)是樹結(jié)構(gòu),添加條目到該樹中*/ else if (p instanceof TreeNode) e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); /*新條目與舊條目hash值相同,key不同的情況(采用拉鏈法)*/ else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 新條目插在鏈表末端 /*若鏈表長(zhǎng)度大于等于8時(shí),轉(zhuǎn)換為樹結(jié)構(gòu)*/ if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } /*若新添加的條目的key與鏈表中已有的key相同,則覆蓋*/ 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; } } /*記錄操作次數(shù)*/ ++modCount; /*添加條目后再檢查是否需要resize*/ if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
在插入新條目的時(shí)候,首先根據(jù)key計(jì)算出散列值,然后根據(jù)散列值確定條目存放的桶的下標(biāo),
i = (n - 1) & hash // i為桶的索引,n為桶的數(shù)量
如果該位置為空,則直接存放新條目;如果不為空,則在該位置使用鏈表記錄這些相同散列值的條目,當(dāng)一個(gè)桶記錄的條目大于8時(shí),改用紅黑樹記錄該桶中的元素。
3.3 get()方法get(Object key)方法根據(jù)指定的key返回一個(gè)value,
public V get(Object key) { Nodee; return (e = getNode(hash(key), key)) == null ? null : e.value; }
調(diào)用了getNode(int hash, Object key)方法,獲取指定key的節(jié)點(diǎn)(Entry),
final NodegetNode(int hash, Object key) { Node [] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { /*在紅黑樹結(jié)構(gòu)中搜索節(jié)點(diǎn)*/ if (first instanceof TreeNode) return ((TreeNode )first).getTreeNode(hash, key); /*在鏈表中搜索節(jié)點(diǎn)*/ do { /*分別比較散列值和key是否相同*/ if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
在尋找指定key的節(jié)點(diǎn)時(shí),會(huì)先比較散列值是否相同,再比較key的值是否相同。只有key散列值相同且key相同才是所要找的節(jié)點(diǎn)。
3.4 remove()方法remove()方法,
@Override public boolean remove(Object key, Object value) { return removeNode(hash(key), key, value, true, true) != null; }
調(diào)用removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法,
final Node3.5 計(jì)算桶下標(biāo)removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node [] tab; Node p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v; /*找到待刪除的節(jié)點(diǎn),并用node指向它*/ if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode )p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } /**/ if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { /*在紅黑樹中刪除該節(jié)點(diǎn)*/ if (node instanceof TreeNode) ((TreeNode )node).removeTreeNode(this, tab, movable); /*該桶只有一個(gè)節(jié)點(diǎn)*/ else if (node == p) tab[index] = node.next; /*在鏈表中刪除該節(jié)點(diǎn)*/ else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
在諸如插入或者刪除的操作中都涉及到桶下標(biāo)的計(jì)算
i = (n - 1) & hash // n-1做hash的掩碼
其中的n表示桶的個(gè)數(shù)(2的整數(shù)次冪),hash由hash(Object key)方法計(jì)算,
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
舉個(gè)例子:
以在一個(gè)容量為16(1000B)的HashMap中,計(jì)算key為字符串"hello"時(shí),應(yīng)該存放的桶下標(biāo)。
第一步,計(jì)算字符串"hello"的hashCode,
"hello".hashCode() = 0000 0101 1110 1001 0001 1000 1101 0010
第二步,在hash(Object key)中,將hashCode的高位(16位)與低位(16位)異或,
h = 0000 0101 1110 1001 0001 1000 1101 0010 // hashCode h >>> 16 = 0000 0000 0000 0000 0000 0101 1110 1001 // hashCode無符號(hào)右移16位 XOR = 0000 0101 1110 1001 0001 1101 0011 1011 // 用于計(jì)算桶下標(biāo)的散列值
第三步,將異或的結(jié)果作為計(jì)算桶下標(biāo)的hash值,
hash = 0000 0101 1110 1001 0001 1101 0011 1011 // 散列值 n - 1 = 0000 0000 0000 0000 0000 0000 0000 1111 (15) // 桶數(shù)-1 index = 0000 0000 0000 0000 0000 0000 0000 1011 (11) // 桶下標(biāo)
其中要注意的是,在第三步計(jì)算桶下標(biāo)的時(shí)候,沒有直接使用hash%n這樣取余的方式,因?yàn)槿∮嗟姆绞綇?fù)雜度較位運(yùn)算要高。由于hash算法均勻分布的原則,作為掩碼的二進(jìn)制位全為1,可以使得求得的桶下標(biāo)也是均勻的,因此規(guī)定table的容量應(yīng)該是2的整數(shù)次冪。
而在第二步計(jì)算hash的時(shí)候?qū)ashCode的高位與低位求異或,則是因?yàn)槭褂昧搜诖a的方式求下標(biāo),導(dǎo)致大部分情況下只利用了hash值中低位的信息,容易產(chǎn)生hash沖突,因此將hashCode的高位信息通過這種形式引入到hash值中。
3.6 擴(kuò)容當(dāng)HashMap中的節(jié)點(diǎn)數(shù)量達(dá)到臨界值時(shí),會(huì)調(diào)用resize()方法對(duì)HashMap進(jìn)行擴(kuò)容,該方法也用于初始化HashMap中的table數(shù)組,
final Node[] resize() { Node [] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; /*擴(kuò)容*/ if (oldCap > 0) { /*已經(jīng)達(dá)到最大容量,直接返回*/ if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } /*新容量為舊容量的2倍(<<1)*/ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } /*使用構(gòu)造函數(shù)中建議的容量*/ else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; /*默認(rèn)初始化*/ else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } /*更新擴(kuò)容閾值*/ if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node [] newTab = (Node [])new Node[newCap]; // 構(gòu)造新的table table = newTab; /*將舊table中的節(jié)點(diǎn)保存到新table*/ if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; /*桶中只有一個(gè)節(jié)點(diǎn),直接放入新table*/ if (e.next == null) newTab[e.hash & (newCap - 1)] = e; /*桶中是紅黑樹結(jié)構(gòu),拆分放入新table*/ else if (e instanceof TreeNode) ((TreeNode )e).split(this, newTab, j, oldCap); /*桶中是鏈表結(jié)構(gòu),放入新table時(shí)保留鏈表中的順序*/ else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
在進(jìn)行擴(kuò)容時(shí),首先計(jì)算新table的容量,然后將遍歷舊table中的節(jié)點(diǎn)放到新table中,十分費(fèi)時(shí)。其中涉及對(duì)節(jié)點(diǎn)在新table中的桶下標(biāo)的計(jì)算,HashMap采取一個(gè)機(jī)制,降低重新計(jì)算桶下標(biāo)的復(fù)雜度,
oldTable[index] --> newTable[index+oldCap]
舉個(gè)例子,
假設(shè)原數(shù)組容量oldCap為 16,擴(kuò)容之后newCap為 32:
hash = 110110 (54) // 散列值 oldIndex = 000110 (6) // 舊下標(biāo) oldCap = 010000 (16) // 舊table容量 newCap = 100000 (32) // 新table容量
按照求異或的算法,算出新下標(biāo)為
hash = 110110 (54) // 散列值 newCap-1 = 011111 (32) // 新table容量-1 newIndex = 010110 (22) // 新下標(biāo)
根據(jù)公式算得新的下標(biāo)為
oldIndex + oldCap = 6 + 16 = 22 // 新下標(biāo)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/76073.html
摘要:原文地址游客前言金三銀四,很多同學(xué)心里大概都準(zhǔn)備著年后找工作或者跳槽。最近有很多同學(xué)都在交流群里求大廠面試題。 最近整理了一波面試題,包括安卓JAVA方面的,目前大廠還是以安卓源碼,算法,以及數(shù)據(jù)結(jié)構(gòu)為主,有一些中小型公司也會(huì)問到混合開發(fā)的知識(shí),至于我為什么傾向于混合開發(fā),我的一句話就是走上編程之路,將來你要學(xué)不僅僅是這些,豐富自己方能與世接軌,做好全棧的裝備。 原文地址:游客kutd...
摘要:源碼分析屬性內(nèi)部使用虛擬對(duì)象,用來作為放到中構(gòu)造方法非,主要是給使用的構(gòu)造方法都是調(diào)用對(duì)應(yīng)的構(gòu)造方法。遍歷元素直接調(diào)用的的迭代器。什么是機(jī)制是集合中的一種錯(cuò)誤機(jī)制。當(dāng)使用迭代器迭代時(shí),如果發(fā)現(xiàn)集合有修改,則快速失敗做出響應(yīng),拋出異常。 簡(jiǎn)介 集合,這個(gè)概念有點(diǎn)模糊。 廣義上來講,java中的集合是指java.util包下面的容器類,包括和Collection及Map相關(guān)的所有類。 中...
摘要:到此發(fā)現(xiàn),實(shí)際上可以拆分成跟指的是,則是指實(shí)現(xiàn)了接口,這樣看來,的實(shí)現(xiàn)其實(shí)就比較簡(jiǎn)單了,下面開始分析源碼。 概述 在分析HashSet源碼前,先看看HashSet的繼承關(guān)系 showImg(https://segmentfault.com/img/bVWo4W?w=605&h=425); HashSet繼承關(guān)系從上圖可以看出,HashSet繼承自AbstractSet,實(shí)現(xiàn)了Set接口...
摘要:基礎(chǔ)問題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線抽象關(guān)鍵字修飾符知識(shí)點(diǎn)總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類與三大特征時(shí)間和時(shí)間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類對(duì)象鎖和類鎖的區(qū)別,,優(yōu)缺點(diǎn)及比較提高篇八詳解內(nèi)部類單例模式和 Java基礎(chǔ)問題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 1362·2021-09-24 10:26
閱讀 3678·2021-09-06 15:02
閱讀 632·2019-08-30 14:18
閱讀 588·2019-08-30 12:44
閱讀 3129·2019-08-30 10:48
閱讀 1953·2019-08-29 13:09
閱讀 2006·2019-08-29 11:30
閱讀 2292·2019-08-26 13:36