這篇文章主要講解了“Java的HashMap和HashTable有什么用”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Java的HashMap和HashTable有什么用”吧!
在婁星等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供網(wǎng)站建設(shè)、成都做網(wǎng)站 網(wǎng)站設(shè)計制作按需網(wǎng)站制作,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),品牌網(wǎng)站設(shè)計,成都全網(wǎng)營銷,外貿(mào)網(wǎng)站建設(shè),婁星網(wǎng)站建設(shè)費用合理。
HashMap也是我們使用非常多的Collection,它是基于哈希表的 Map 接口的實現(xiàn),以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統(tǒng)會根據(jù)hash算法來來計算key-value的存儲位置,我們總是可以通過key快速地存、取value。
HashMap實現(xiàn)了Map接口,繼承AbstractMap。其中Map接口定義了鍵映射到值的規(guī)則,而AbstractMap類提供 Map 接口的骨干實現(xiàn),以最大限度地減少實現(xiàn)此接口所需的工作,其實AbstractMap類已經(jīng)實現(xiàn)了Map,這里標注Map LZ覺得應(yīng)該是更加清晰吧!
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap提供了三個構(gòu)造函數(shù): HashMap():構(gòu)造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。 HashMap(int initialCapacity):構(gòu)造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。 HashMap(int initialCapacity, float loadFactor):構(gòu)造一個帶指定初始容量和加載因子的空 HashMap。
在這里提到了兩個參數(shù):初始容量,加載因子。
這兩個參數(shù)是影響HashMap性能的重要參數(shù),其中容量表示哈希表中桶的數(shù)量,初始容量是創(chuàng)建哈希表時的容量,加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。
對于使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負載因子太小,那么散列表的數(shù)據(jù)將過于稀疏,對空間造成嚴重浪費。系統(tǒng)默認負載因子為0.75,一般情況下我們是無需修改的。
HashMap是一種支持快速存取的數(shù)據(jù)結(jié)構(gòu),要了解它的性能必須要了解它的數(shù)據(jù)結(jié)構(gòu)。
我們知道在Java中最常用的兩種結(jié)構(gòu)是數(shù)組和模擬指針(引用),幾乎所有的數(shù)據(jù)結(jié)構(gòu)都可以利用這兩種來組合實現(xiàn),HashMap也是如此。實際上HashMap是一個“鏈表散列”,如下是它的數(shù)據(jù)結(jié)構(gòu):
HashMap數(shù)據(jù)結(jié)構(gòu)圖
下圖的table數(shù)組的每個格子都是一個桶。負載因子就是map中的元素占用的容量百分比。比如負載因子是0.75,初始容量(桶數(shù)量)為16時,那么允許裝填的元素最大個數(shù)就是16*0.75 = 12,這個最大個數(shù)也被成為閾值,就是map中定義的threshold。超過這個閾值時,map就會自動擴容。
從上圖我們可以看出HashMap底層實現(xiàn)還是數(shù)組,只是數(shù)組的每一項都是一條鏈。其中參數(shù)initialCapacity就代表了該數(shù)組的長度。下面為HashMap構(gòu)造函數(shù)的源碼:
public HashMap(int initialCapacity, float loadFactor) { //初始容量不能<0 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //初始容量不能 > 最大容量值,HashMap的最大容量值為2^30 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //負載因子不能 < 0 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 計算出大于 initialCapacity 的最小的 2 的 n 次方值。 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; //設(shè)置HashMap的容量極限,當HashMap的容量達到該極限時就會進行擴容操作 threshold = (int) (capacity * loadFactor); //初始化table數(shù)組,也就是桶數(shù)組。 table = new Entry[capacity]; init(); } 從源碼中可以看出,每次新建一個HashMap時,都會初始化一個table數(shù)組。table數(shù)組的元素為Entry節(jié)點。 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ....... } 其中Entry為HashMap的內(nèi)部類,它包含了鍵key、值value、下一個節(jié)點next,以及hash值,這是非常重要的,正是由于Entry才構(gòu)成了table數(shù)組的項為鏈表。 上面簡單分析了HashMap的數(shù)據(jù)結(jié)構(gòu),下面將探討HashMap是如何實現(xiàn)快速存取的。
首先我們先看源碼 public V put(K key, V value) { //當key為null,調(diào)用putForNullKey方法,保存null與table第一個位置中,這是HashMap允許為null的原因 if (key == null) return putForNullKey(value); //計算key的hash值,此處對原來元素的hashcode進行了再次hash int hash = hash(key.hashCode()); ------(1) //計算key hash 值在 table 數(shù)組中的位置 int i = indexFor(hash, table.length); ------(2) //從i出開始迭代 e,找到 key 保存的位置 for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; //判斷該條鏈上是否有hash值相同的(key相同) //若存在相同,則直接覆蓋value,返回舊value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; //舊值 = 新值 e.value = value; e.recordAccess(this); return oldValue; //返回舊值 } } //修改次數(shù)增加1 modCount++; //將key、value添加至i位置處 addEntry(hash, key, value, i); return null; }
通過源碼我們可以清晰看到HashMap保存數(shù)據(jù)的過程為:首先判斷key是否為null,若為null,則直接調(diào)用putForNullKey方法。
若不為空則先計算key的hash值,然后根據(jù)hash值搜索在table數(shù)組中的索引位置,如果table數(shù)組在該位置處有元素,則通過比較是否存在相同的key,若存在則覆蓋原來key的value,==否則將該元素保存在鏈頭(最先保存的元素放在鏈尾)==。
若table在該處沒有元素,則直接保存。這個過程看似比較簡單,其實深有內(nèi)幕。有如下幾點:
1、 先看迭代處。此處迭代原因就是為了防止存在相同的key值,若發(fā)現(xiàn)兩個hash值(key)相同時,HashMap的處理方式是用新value替換舊value,這里并沒有處理key,這就解釋了HashMap中沒有兩個相同的key。
2、 在看(1)、(2)處。這里是HashMap的精華所在。首先是hash方法,該方法為一個純粹的數(shù)學(xué)計算,就是計算h的hash值。
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
我們知道對于HashMap的table而言,數(shù)據(jù)分布需要均勻(最好每項都只有一個元素,這樣就可以直接找到),不能太緊也不能太松,太緊會導(dǎo)致查詢速度慢,太松則浪費空間。計算hash值后,怎么才能保證table元素分布均與呢?我們會想到取模,但是由于取模的消耗較大,HashMap是這樣處理的:調(diào)用indexFor方法。
static int indexFor(int h, int length) { return h & (length-1); }
HashMap的底層數(shù)組長度總是2的n次方,在構(gòu)造函數(shù)中存在:capacity <<= 1;這樣做總是能夠保證HashMap的底層數(shù)組長度為2的n次方。當length為2的n次方時,h&(length - 1)就相當于對length取模,而且速度比直接取模快得多,這是HashMap在速度上的一個優(yōu)化。
這是因為他們在與14進行&運算時,得到的結(jié)果最后一位永遠都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置處是不可能存儲數(shù)據(jù)的,空間減少,進一步增加碰撞幾率,這樣就會導(dǎo)致查詢速度慢。
而當length = 16時,length – 1 = 15 即1111,那么進行低位&運算時,值總是與原來hash值相同,而進行高位運算時,其值等于其低位值。所以說當length = 2^n時,不同的hash值發(fā)生碰撞的概率比較小,這樣就會使得數(shù)據(jù)在table數(shù)組中分布較均勻,查詢速度也較快。
這里我們再來復(fù)習(xí)put的流程:當我們想一個HashMap中添加一對key-value時,系統(tǒng)首先會計算key的hash值,然后根據(jù)hash值確認在table中存儲的位置。若該位置沒有元素,則直接插入。否則迭代該處元素鏈表并依此比較其key的hash值。
如果兩個hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的Entry的value覆蓋原來節(jié)點的value。如果兩個hash值相等但key值不等 ,則將該節(jié)點插入該鏈表的鏈頭。具體的實現(xiàn)過程見addEntry方法,如下:
void addEntry(int hash, K key, V value, int bucketIndex) { //獲取bucketIndex處的Entry Entry<K, V> e = table[bucketIndex]; //將新創(chuàng)建的 Entry 放入 bucketIndex 索引處,并讓新的 Entry 指向原來的 Entry table[bucketIndex] = new Entry<K, V>(hash, key, value, e); //若HashMap中元素的個數(shù)超過極限了,則容量擴大兩倍 if (size++ >= threshold) resize(2 * table.length); }
這個方法中有兩點需要注意:
后面添加的entry反而會接到前面。
一、是鏈的產(chǎn)生。
這是一個非常優(yōu)雅的設(shè)計。系統(tǒng)總是將新的Entry對象添加到bucketIndex處。如果bucketIndex處已經(jīng)有了對象,那么新添加的Entry對象將指向原有的Entry對象,形成一條Entry鏈,但是若bucketIndex處沒有Entry對象,也就是e==null,那么新添加的Entry對象指向null,也就不會產(chǎn)生Entry鏈了。
二、擴容問題。
隨著HashMap中元素的數(shù)量越來越多,發(fā)生碰撞的概率就越來越大,所產(chǎn)生的鏈表長度就會越來越長,這樣勢必會影響HashMap的速度,為了保證HashMap的效率,系統(tǒng)必須要在某個臨界點進行擴容處理。
該臨界點在當HashMap中元素的數(shù)量等于table數(shù)組長度*加載因子。但是擴容是一個非常耗時的過程,因為它需要重新計算這些數(shù)據(jù)在新table數(shù)組中的位置并進行復(fù)制處理。所以如果我們已經(jīng)預(yù)知HashMap中元素的個數(shù),那么預(yù)設(shè)元素的個數(shù)能夠有效的提高HashMap的性能。
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; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果p是紅黑樹節(jié)點,則用另外的處理方法 else if (p instanceof 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 //當鏈表節(jié)點數(shù)超過8個,則直接進行紅黑樹化。 treeifyBin(tab, hash); 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; }
JDK1.8在鏈表長度超過8時會轉(zhuǎn)換為紅黑樹。 轉(zhuǎn)換方法如下:
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //如果節(jié)點數(shù)變小小于紅黑樹的節(jié)點數(shù)閾值時,調(diào)整空間 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { //該方法直接返回一個紅黑樹結(jié)點。 TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { //從鏈表頭開始依次插入紅黑樹 p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } } // For treeifyBin TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //如果原容量大于最大空間,則讓閾值為最大值。因為不能再擴容了,最大容量就是整數(shù)最大值。 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //兩倍擴容,閾值也跟著變?yōu)閮杀? else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) //當后面沒有節(jié)點時,直接插入即可 //每個元素重新計算索引位置,此處的hash值并沒有變,只是改變索引值 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //否則,就從頭到尾依次將節(jié)點進行索引然后插入新數(shù)組,這樣插入后的鏈表順序會和原來的順序相反。 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> 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; }
相對于HashMap的存而言,取就顯得比較簡單了。通過key的hash值找到在table數(shù)組中的索引處的Entry,然后返回該key對應(yīng)的value即可。 public V get(Object key) { // 若為null,調(diào)用getForNullKey方法返回相對應(yīng)的value if (key == null) return getForNullKey(); // 根據(jù)該 key 的 hashCode 值計算它的 hash 碼 int hash = hash(key.hashCode()); // 取出 table 數(shù)組中指定索引處的值 for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; //若搜索的key與查找的key相同,則返回相對應(yīng)的value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
在這里能夠根據(jù)key快速的取到value除了和HashMap的數(shù)據(jù)結(jié)構(gòu)密不可分外,還和Entry有莫大的關(guān)系,在前面就提到過,HashMap在存儲過程中并沒有將key,value分開來存儲,而是當做一個整體key-value來處理的,這個整體就是Entry對象。
同時value也只相當于key的附屬而已。在存儲的過程中,系統(tǒng)根據(jù)key的hashcode來決定Entry在table數(shù)組中的存儲位置,在取的過程中同樣根據(jù)key的hashcode取出相對應(yīng)的Entry對象。
在java中與有兩個類都提供了一個多種用途的hashTable機制,他們都可以將可以key和value結(jié)合起來構(gòu)成鍵值對通過put(key,value)方法保存起來,然后通過get(key)方法獲取相對應(yīng)的value值。
一個是前面提到的HashMap,還有一個就是馬上要講解的HashTable。對于HashTable而言,它在很大程度上和HashMap的實現(xiàn)差不多,如果我們對HashMap比較了解的話,對HashTable的認知會提高很大的幫助。他們兩者之間只存在幾點的不同,這個后面會闡述。
HashTable在Java中的定義如下: public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable 從中可以看出HashTable繼承Dictionary類,實現(xiàn)Map接口。其中Dictionary類是任何可將鍵映射到相應(yīng)值的類(如 Hashtable)的抽象父類。每個鍵和每個值都是一個對象。在任何一個 Dictionary 對象中,每個鍵至多與一個值相關(guān)聯(lián)。Map是"key-value鍵值對"接口。 HashTable采用"拉鏈法"實現(xiàn)哈希表,它定義了幾個重要的參數(shù):table、count、threshold、loadFactor、modCount。 table:為一個Entry[]數(shù)組類型,Entry代表了“拉鏈”的節(jié)點,每一個Entry代表了一個鍵值對,哈希表的"key-value鍵值對"都是存儲在Entry數(shù)組中的。 count:HashTable的大小,注意這個大小并不是HashTable的容器大小,而是他所包含Entry鍵值對的數(shù)量。 threshold:Hashtable的閾值,用于判斷是否需要調(diào)整Hashtable的容量。threshold的值="容量*加載因子"。 loadFactor:加載因子。 modCount:用來實現(xiàn)“fail-fast”機制的(也就是快速失敗)。所謂快速失敗就是在并發(fā)集合中,其進行迭代操作時,若有其他線程對其進行結(jié)構(gòu)性的修改,這時迭代器會立馬感知到,并且立即拋出ConcurrentModificationException異常,而不是等到迭代完成之后才告訴你(你已經(jīng)出錯了)。
在HashTabel中存在5個構(gòu)造函數(shù)。通過這5個構(gòu)造函數(shù)我們構(gòu)建出一個我想要的HashTable。 public Hashtable() { this(11, 0.75f); } 默認構(gòu)造函數(shù),容量為11,加載因子為0.75。 public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } 用指定初始容量和默認的加載因子 (0.75) 構(gòu)造一個新的空哈希表。 public Hashtable(int initialCapacity, float loadFactor) { //驗證初始容量 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); //驗證加載因子 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; //初始化table,獲得大小為initialCapacity的table數(shù)組 table = new Entry[initialCapacity]; //計算閥值 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); //初始化HashSeed值 initHashSeedAsNeeded(initialCapacity); }
用指定初始容量和指定加載因子構(gòu)造一個新的空哈希表。其中initHashSeedAsNeeded方法用于初始化hashSeed參數(shù),其中hashSeed用于計算key的hash值,它與key的hashCode進行按位異或運算。這個hashSeed是一個與實例相關(guān)的隨機值,主要用于解決hash沖突。
private int hash(Object k) { return hashSeed ^ k.hashCode(); }
構(gòu)造一個與給定的 Map 具有相同映射關(guān)系的新哈希表。
public Hashtable(Map<? extends K, ? extends V> t) { //設(shè)置table容器大小,其值==t.size * 2 + 1 this(Math.max(2*t.size(), 11), 0.75f); putAll(t); }
HashTable的API對外提供了許多方法,這些方法能夠很好幫助我們操作HashTable,但是這里我只介紹兩個最根本的方法:put、get。
首先我們先看put方法:將指定 key 映射到此哈希表中的指定 value。注意這里鍵key和值value都不可為空。 public synchronized V put(K key, V value) { // 確保value不為null if (value == null) { throw new NullPointerException(); } /* * 確保key在table[]是不重復(fù)的 * 處理過程: * 1、計算key的hash值,確認在table[]中的索引位置 * 2、迭代index索引位置,如果該位置處的鏈表中存在一個一樣的key,則替換其value,返回舊值 */ Entry tab[] = table; int hash = hash(key); //計算key的hash值 int index = (hash & 0x7FFFFFFF) % tab.length; //確認該key的索引位置 //迭代,尋找該key,替換 for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; if (count >= threshold) { //如果容器中的元素數(shù)量已經(jīng)達到閥值,則進行擴容操作 rehash(); tab = table; hash = hash(key); index = (hash & 0x7FFFFFFF) % tab.length; } // 在索引位置處插入一個新的節(jié)點 Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); //容器中元素+1 count++; return null; }
put方法的整個處理流程是:計算key的hash值,根據(jù)hash值獲得key在table數(shù)組中的索引位置,然后迭代該key處的Entry鏈表(我們暫且理解為鏈表),若該鏈表中存在一個這個的key對象,那么就直接替換其value值即可,否則在將改key-value節(jié)點插入該index索引位置處。如下:
首先我們假設(shè)一個容量為5的table,存在8、10、13、16、17、21。他們在table中位置如下:
然后我們插入一個數(shù):put(16,22),key=16在table的索引位置為1,同時在1索引位置有兩個數(shù),程序?qū)υ摗版湵怼边M行迭代,發(fā)現(xiàn)存在一個key=16,這時要做的工作就是用newValue=22替換oldValue16,并將oldValue=16返回。
在put(33,33),key=33所在的索引位置為3,并且在該鏈表中也沒有存在某個key=33的節(jié)點,所以就將該節(jié)點插入該鏈表的第一個位置。
在HashTabled的put方法中有兩個地方需要注意:
1、HashTable的擴容操作,在put方法中,如果需要向table[]中添加Entry元素,會首先進行容量校驗,如果容量已經(jīng)達到了閥值,HashTable就會進行擴容處理rehash(),如下:
protected void rehash() { int oldCapacity = table.length; //元素 Entry<K,V>[] oldMap = table; //新容量=舊容量 * 2 + 1 int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) return; newCapacity = MAX_ARRAY_SIZE; } //新建一個size = newCapacity 的HashTable Entry<K,V>[] newMap = new Entry[]; modCount++; //重新計算閥值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); //重新計算hashSeed boolean rehash = initHashSeedAsNeeded(newCapacity); table = newMap; //將原來的元素拷貝到新的HashTable中 for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; if (rehash) { e.hash = hash(e.key); } int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = newMap[index]; newMap[index] = e; } } }
在這個rehash()方法中我們可以看到容量擴大兩倍+1,同時需要將原來HashTable中的元素一一復(fù)制到新的HashTable中,這個過程是比較消耗時間的,同時還需要重新計算hashSeed的,畢竟容量已經(jīng)變了。
這里對閥值啰嗦一下:比如初始值11、加載因子默認0.75,那么這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操作,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操作,依次類推。
下面是計算key的hash值,這里hashSeed發(fā)揮了作用。
private int hash(Object k) { return hashSeed ^ k.hashCode(); }
相對于put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table數(shù)組中的索引位置,然后迭代鏈表,匹配直到找到相對應(yīng)key的value,若沒有找到返回null。
public synchronized V get(Object key) { Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return e.value; } } return null; }
四、HashTable與HashMap的區(qū)別
HashTable和HashMap存在很多的相同點,但是他們還是有幾個比較重要的不同點。
第一:我們從他們的定義就可以看出他們的不同,HashTable基于Dictionary類,而HashMap是基于AbstractMap。Dictionary是什么?它是任何可將鍵映射到相應(yīng)值的類的抽象父類,而AbstractMap是基于Map接口的骨干實現(xiàn),它以最大限度地減少實現(xiàn)此接口所需的工作。
第二:HashMap可以允許存在一個為null的key和任意個為null的value,但是HashTable中的key和value都不允許為null。如下:
當HashMap遇到為null的key時,它會調(diào)用putForNullKey方法來進行處理。對于value沒有進行任何處理,只要是對象都可以。
if (key == null) return putForNullKey(value); 而當HashTable遇到null時,他會直接拋出NullPointerException異常信息。 if (value == null) { throw new NullPointerException(); }
第三:Hashtable的方法是同步的,而HashMap的方法不是。所以有人一般都建議如果是涉及到多線程同步時采用HashTable,沒有涉及就采用HashMap,但是在Collections類中存在一個靜態(tài)方法:synchronizedMap(),該方法創(chuàng)建了一個線程安全的Map對象,并把它作為一個封裝的對象來返回,所以通過Collections類的synchronizedMap方法是可以我們你同步訪問潛在的HashMap。
感謝各位的閱讀,以上就是“Java的HashMap和HashTable有什么用”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Java的HashMap和HashTable有什么用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!
網(wǎng)頁標題:Java的HashMap和HashTable有什么用
當前網(wǎng)址:http://aaarwkj.com/article40/gjgdho.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站策劃、網(wǎng)頁設(shè)計公司、網(wǎng)站設(shè)計公司、品牌網(wǎng)站制作、標簽優(yōu)化、全網(wǎng)營銷推廣
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)