Java集合詳解4:一文讀懂HashMap和HashTable的區別以及常見面試題

《Java集合詳解系列》是我在完成夯實Java基礎篇的系列博客後準備開始寫的新系列。html

這些文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看java

https://github.com/h2pl/Java-Tutorialgit

喜歡的話麻煩點下Star、fork哈程序員

文章首發於個人我的博客:github

www.how2playlife.com面試

今天咱們來探索一下HashMap和HashTable機制與比較器的源碼。算法

本文參考http://cmsblogs.com/?p=176後端

HashMap

HashMap也是咱們使用很是多的Collection,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。在HashMap中,key-value老是會當作一個總體來處理,系統會根據hash算法來來計算key-value的存儲位置,咱們老是能夠經過key快速地存、取value。下面就來分析HashMap的存取。數組

定義

HashMap實現了Map接口,繼承AbstractMap。其中Map接口定義了鍵映射到值的規則,而AbstractMap類提供 Map 接口的骨幹實現,以最大限度地減小實現此接口所需的工做,其實AbstractMap類已經實現了Map,這裏標註Map LZ以爲應該是更加清晰吧!安全

public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable複製代碼

構造函數

HashMap提供了三個構造函數:複製代碼
HashMap():構造一個具備默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。複製代碼
HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。複製代碼
HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap。複製代碼

在這裏提到了兩個參數:初始容量,加載因子。

這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中桶的數量,初始容量是建立哈希表時的容量,加載因子是哈希表在其容量自動增長以前能夠達到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。

對於使用鏈表法的散列表來講,查找一個元素的平均時間是O(1+a),所以若是負載因子越大,對空間的利用更充分,然然後果是查找效率的下降;若是負載因子過小,那麼散列表的數據將過於稀疏,對空間形成嚴重浪費。系統默認負載因子爲0.75,通常狀況下咱們是無需修改的。

HashMap是一種支持快速存取的數據結構,要了解它的性能必需要了解它的數據結構。

數據結構

咱們知道在Java中最經常使用的兩種結構是數組和模擬指針(引用),幾乎全部的數據結構均可以利用這兩種來組合實現,HashMap也是如此。實際上HashMap是一個「鏈表散列」,以下是它的數據結構:

HashMap數據結構圖

下圖的table數組的每一個格子都是一個桶。負載因子就是map中的元素佔用的容量百分比。好比負載因子是0.75,初始容量(桶數量)爲16時,那麼容許裝填的元素最大個數就是16*0.75 = 12,這個最大個數也被成爲閾值,就是map中定義的threshold。超過這個閾值時,map就會自動擴容。

存儲實現:put(key,vlaue)

首先咱們先看源碼複製代碼
public V put(K key, V value) {
            //當key爲null,調用putForNullKey方法,保存null與table第一個位置中,這是HashMap容許爲null的緣由
            if (key == null)
                return putForNullKey(value);
            //計算key的hash值,此處對原來元素的hashcode進行了再次hash
            int hash = hash(key.hashCode());                  ------(1)
            //計算key hash 值在 table 數組中的位置
            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;     //返回舊值
                }
            }
            //修改次數增長1
            modCount++;
            //將key、value添加至i位置處
            addEntry(hash, key, value, i);
            return null;
        }複製代碼

經過源碼咱們能夠清晰看到HashMap保存數據的過程爲:首先判斷key是否爲null,若爲null,則直接調用putForNullKey方法。

若不爲空則先計算key的hash值,而後根據hash值搜索在table數組中的索引位置,若是table數組在該位置處有元素,則經過比較是否存在相同的key,若存在則覆蓋原來key的value,==不然將該元素保存在鏈頭(最早保存的元素放在鏈尾)==。

若table在該處沒有元素,則直接保存。這個過程看似比較簡單,其實深有內幕。有以下幾點:

一、 先看迭代處。此處迭代緣由就是爲了防止存在相同的key值,若發現兩個hash值(key)相同時,HashMap的處理方式是用新value替換舊value,這裏並無處理key,這就解釋了HashMap中沒有兩個相同的key。

二、 在看(1)、(2)處。這裏是HashMap的精華所在。首先是hash方法,該方法爲一個純粹的數學計算,就是計算h的hash值。

static int hash(int h) {
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }複製代碼

咱們知道對於HashMap的table而言,數據分佈須要均勻(最好每項都只有一個元素,這樣就能夠直接找到),不能太緊也不能太鬆,太緊會致使查詢速度慢,太鬆則浪費空間。計算hash值後,怎麼才能保證table元素分佈均與呢?咱們會想到取模,可是因爲取模的消耗較大,HashMap是這樣處理的:調用indexFor方法。

static int indexFor(int h, int length) {
            return h & (length-1);
        }複製代碼

HashMap的底層數組長度老是2的n次方,在構造函數中存在:capacity <<= 1;這樣作老是可以保證hashmap的底層數組長度爲2的n次方。當length爲2的n次方時,h&(length="" -="" 1)就至關於對length取模,並且速度比直接取模快得多,這是hashmap在速度上的一個優化。至於爲何是2的n次方下面解釋。<="" p="">

==對length取模來獲得hash是經常使用的hash索引方法,這裏採用位運算的話效率更高。==

咱們回到indexFor方法,該方法僅有一條語句:h&(length - 1),這句話除了上面的取模運算外還有一個很是重要的責任:均勻分佈table數據和充分利用空間。

這裏咱們假設length爲16(2^n)和15,h爲五、六、7。

當n=15時,6和7的結果同樣,這樣表示他們在table存儲的位置是相同的,也就是產生了碰撞,六、7就會在一個位置造成鏈表,這樣就會致使查詢速度下降。誠然這裏只分析三個數字不是不少,那麼咱們就看0-15。

而當length = 16時,length – 1 = 15 即1111,那麼進行低位&運算時,值老是與原來hash值相同,而進行高位運算時,其值等於其低位值。因此說當length = 2^n時,不一樣的hash值發生碰撞的機率比較小,這樣就會使得數據在table數組中分佈較均勻,查詢速度也較快。

這裏咱們再來複習put的流程:當咱們想一個HashMap中添加一對key-value時,系統首先會計算key的hash值,而後根據hash值確認在table中存儲的位置。若該位置沒有元素,則直接插入。不然迭代該處元素鏈表並依此比較其key的hash值。

若是兩個hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的Entry的value覆蓋原來節點的value。若是兩個hash值相等但key值不等 ,則將該節點插入該鏈表的鏈頭。具體的實現過程見addEntry方法,以下:

void addEntry(int hash, K key, V value, int bucketIndex) {
            //獲取bucketIndex處的Entry
            Entry<K, V> e = table[bucketIndex];
            //將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 
            table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
            //若HashMap中元素的個數超過極限了,則容量擴大兩倍
            if (size++ >= threshold)
                resize(2 * table.length);
        }複製代碼

這個方法中有兩點須要注意:

後面添加的entry反而會接到前面。複製代碼

1、是鏈的產生。

這是一個很是優雅的設計。系統老是將新的Entry對象添加到bucketIndex處。若是bucketIndex處已經有了對象,那麼新添加的Entry對象將指向原有的Entry對象,造成一條Entry鏈,可是若bucketIndex處沒有Entry對象,也就是e==null,那麼新添加的Entry對象指向null,也就不會產生Entry鏈了。

2、擴容問題。

隨着HashMap中元素的數量愈來愈多,發生碰撞的機率就愈來愈大,所產生的鏈表長度就會愈來愈長,這樣勢必會影響HashMap的速度,爲了保證HashMap的效率,系統必需要在某個臨界點進行擴容處理。

該臨界點在當HashMap中元素的數量等於table數組長度*加載因子。可是擴容是一個很是耗時的過程,由於它須要從新計算這些數據在新table數組中的位置並進行復制處理。因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。

JDK1.8的hashmap:put方法

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是紅黑樹節點,則用另外的處理方法
                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
                            //當鏈表節點數超過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時會轉換爲紅黑樹。轉換方法以下:

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)
            //若是節點數變小小於紅黑樹的節點數閾值時,調整空間
                resize();
            else if ((e = tab[index = (n - 1) & hash]) != null) {
                TreeNode<K,V> hd = null, tl = null;
                do {
                //該方法直接返回一個紅黑樹結點。
                    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) {
                //若是原容量大於最大空間,則讓閾值爲最大值。由於不能再擴容了,最大容量就是整數最大值。
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //兩倍擴容,閾值也跟着變爲兩倍
                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)
                            //當後面沒有節點時,直接插入便可 //每一個元素從新計算索引位置,此處的hash值並無變,只是改變索引值
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                        //不然,就從頭至尾依次將節點進行索引而後插入新數組,這樣插入後的鏈表順序會和原來的順序相反。
                            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;
        }複製代碼

讀取實現:get(key)

相對於HashMap的存而言,取就顯得比較簡單了。經過key的hash值找到在table數組中的索引處的Entry,而後返回該key對應的value便可。複製代碼
public V get(Object key) {
            // 若爲null,調用getForNullKey方法返回相對應的value
            if (key == null)
                return getForNullKey();
            // 根據該 key 的 hashCode 值計算它的 hash 碼  
            int hash = hash(key.hashCode());
            // 取出 table 數組中指定索引處的值
            for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
                Object k;
                //若搜索的key與查找的key相同,則返回相對應的value
                if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                    return e.value;
            }
            return null;
        }複製代碼

在這裏可以根據key快速的取到value除了和HashMap的數據結構密不可分外,還和Entry有莫大的關係,在前面就提到過,HashMap在存儲過程當中並無將key,value分開來存儲,而是當作一個總體key-value來處理的,這個總體就是Entry對象。

同時value也只至關於key的附屬而已。在存儲的過程當中,系統根據key的hashcode來決定Entry在table數組中的存儲位置,在取的過程當中一樣根據key的hashcode取出相對應的Entry對象。

在java中與有兩個類都提供了一個多種用途的hashTable機制,他們均可以將能夠key和value結合起來構成鍵值對經過put(key,value)方法保存起來,而後經過get(key)方法獲取相對應的value值。

HashTable

一個是前面提到的HashMap,還有一個就是立刻要講解的HashTable。對於HashTable而言,它在很大程度上和HashMap的實現差很少,若是咱們對HashMap比較瞭解的話,對HashTable的認知會提升很大的幫助。他們二者之間只存在幾點的不一樣,這個後面會闡述。

定義

HashTable在Java中的定義以下:複製代碼
public class Hashtable<K,V>
        extends Dictionary<K,V>
        implements Map<K,V>, Cloneable, java.io.Serializable
          從中能夠看出HashTable繼承Dictionary類,實現Map接口。其中Dictionary類是任何可將鍵映射到相應值的類(如 Hashtable)的抽象父類。每一個鍵和每一個值都是一個對象。在任何一個 Dictionary 對象中,每一個鍵至多與一個值相關聯。Map是"key-value鍵值對"接口。

複製代碼
HashTable採用"拉鍊法"實現哈希表,它定義了幾個重要的參數:table、count、threshold、loadFactor、modCount。

複製代碼
table:爲一個Entry[]數組類型,Entry表明了「拉鍊」的節點,每個Entry表明了一個鍵值對,哈希表的"key-value鍵值對"都是存儲在Entry數組中的。

複製代碼
count:HashTable的大小,注意這個大小並非HashTable的容器大小,而是他所包含Entry鍵值對的數量。

複製代碼
threshold:Hashtable的閾值,用於判斷是否須要調整Hashtable的容量。threshold的值="容量*加載因子"。

複製代碼
loadFactor:加載因子。

複製代碼
modCount:用來實現「fail-fast」機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操做時,如有其餘線程對其進行結構性的修改,這時迭代器會立馬感知到,而且當即拋出ConcurrentModificationException異常,而不是等到迭代完成以後才告訴你(你已經出錯了)。複製代碼

構造方法

在HashTabel中存在5個構造函數。經過這5個構造函數咱們構建出一個我想要的HashTable。複製代碼
public Hashtable() {
            this(11, 0.75f);
        }
          默認構造函數,容量爲11,加載因子爲0.75。複製代碼
public Hashtable(int initialCapacity) {
            this(initialCapacity, 0.75f);
        }
          用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。複製代碼
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數組
            table = new Entry[initialCapacity];
            //計算閥值
            threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
            //初始化HashSeed值
            initHashSeedAsNeeded(initialCapacity);
        }
複製代碼


用指定初始容量和指定加載因子構造一個新的空哈希表。其中initHashSeedAsNeeded方法用於初始化hashSeed參數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。這個hashSeed是一個與實例相關的隨機值,主要用於解決hash衝突。

private int hash(Object k) {
            return hashSeed ^ k.hashCode();
        }複製代碼

構造一個與給定的 Map 具備相同映射關係的新哈希表。

public Hashtable(Map<? extends K, ? extends V> t) {
            //設置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[]是不重複的
             * 處理過程:
             * 一、計算key的hash值,確認在table[]中的索引位置
             * 二、迭代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) {  //若是容器中的元素數量已經達到閥值,則進行擴容操做
                rehash();
                tab = table;
                hash = hash(key);
                index = (hash & 0x7FFFFFFF) % tab.length;
            }複製代碼
// 在索引位置處插入一個新的節點
            Entry<K,V> e = tab[index];
            tab[index] = new Entry<>(hash, key, value, e);
            //容器中元素+1
            count++;
            return null;
        }複製代碼

put方法的整個處理流程是:計算key的hash值,根據hash值得到key在table數組中的索引位置,而後迭代該key處的Entry鏈表(咱們暫且理解爲鏈表),若該鏈表中存在一個這個的key對象,那麼就直接替換其value值便可,不然在將改key-value節點插入該index索引位置處

在HashTabled的put方法中有兩個地方須要注意:

一、HashTable的擴容操做,在put方法中,若是須要向table[]中添加Entry元素,會首先進行容量校驗,若是容量已經達到了閥值,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中的元素一一複製到新的HashTable中,這個過程是比較消耗時間的,同時還須要從新計算hashSeed的,畢竟容量已經變了。

這裏對閥值囉嗦一下:好比初始值十一、加載因子默認0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操做,容量 = 8 2 + 1 =17,而閥值threshold=170.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操做,依次類推。

下面是計算key的hash值,這裏hashSeed發揮了做用。

private int hash(Object k) {
            return hashSeed ^ k.hashCode();
        }複製代碼

相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table數組中的索引位置,而後迭代鏈表,匹配直到找到相對應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的異同點

HashTable和HashMap存在不少的相同點,可是他們仍是有幾個比較重要的不一樣點。

第一:咱們從他們的定義就能夠看出他們的不一樣,HashTable基於Dictionary類,而HashMap是基於AbstractMap。Dictionary是什麼?它是任何可將鍵映射到相應值的類的抽象父類,而AbstractMap是基於Map接口的骨幹實現,它以最大限度地減小實現此接口所需的工做。

第二:HashMap能夠容許存在一個爲null的key和任意個爲null的value,可是HashTable中的key和value都不容許爲null。以下:

當HashMap遇到爲null的key時,它會調用putForNullKey方法來進行處理。對於value沒有進行任何處理,只要是對象均可以。

if (key == null)

return putForNullKey(value);

而當HashTable遇到null時,他會直接拋出NullPointerException異常信息。

if (value == null) {
        throw new NullPointerException();
    }複製代碼

第三:Hashtable的方法是同步的,而HashMap的方法不是。因此有人通常都建議若是是涉及到多線程同步時採用HashTable,沒有涉及就採用HashMap,可是在Collections類中存在一個靜態方法:synchronizedMap(),該方法建立了一個線程安全的Map對象,並把它做爲一個封裝的對象來返回,因此經過Collections類的synchronizedMap方法是能夠咱們你同步訪問潛在的HashMap。這樣君該如何選擇呢???

面試題:HashMap和HashTable的區別

HashMap線程不安全,HashTable是線程安全的。HashMap內部實現沒有任何線程同步相關的代碼,因此相對而言性能要好一點。若是在多線程中使用HashMap須要本身管理線程同步。HashTable大部分對外接口都使用synchronized包裹,因此是線程安全的,可是性能會相對差一些。

兩者的基類不同。HashMap派生於AbstractMap,HashTable派生於Dictionary。它們都實現Map, Cloneable, Serializable這些接口。AbstractMap中提供的基礎方法更多,而且實現了多個通用的方法,而在Dictionary中只有少許的接口,而且都是abstract類型。

key和value的取值範圍不一樣。HashMap的key和value均可覺得null,可是HashTablekey和value都不能爲null。對於HashMap若是get返回null,並不能代表HashMap不存在這個key,若是須要判斷HashMap中是否包含某個key,就須要使用containsKey這個方法來判斷。

算法不同。HashMap的initialCapacity爲16,而HashTable的initialCapacity爲11。HashMap中初始容量必須是2的冪,若是初始化傳入的initialCapacity不是2的冪,將會自動調整爲大於出入的initialCapacity最小的2的冪。HashMap使用本身的計算hash的方法(會依賴key的hashCode方法),HashTable則使用key的hashCode方法獲得。

參考文章

http://cmsblogs.com/?p=176

http://mini.eastday.com/mobile/180310183019559.html#

https://blog.csdn.net/lihua5419/article/details/87691965

https://www.cnblogs.com/aeolian/p/8468632.html

微信公衆號

Java技術江湖

若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站,做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師經常使用學習資源,關注公衆號後,後臺回覆關鍵字 「Java」 便可免費無套路獲取。

個人公衆號

我的公衆號:黃小斜

黃小斜是跨考軟件工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長爲阿里工程師。

做者專一於 JAVA 後端技術棧,熱衷於分享程序員乾貨、學習經驗、求職心得和程序人生,目前黃小斜的CSDN博客有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。

黃小斜是一個斜槓青年,堅持學習和寫做,相信終身學習的力量,但願和更多的程序員交朋友,一塊兒進步和成長!關注公衆號【黃小斜】後回覆【原創電子書】便可領取我原創的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》

程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取。


本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索