Java中的Map【六】Hashtable類

    所使用的jdk版本爲1.8.0_172版本,先看一下 Hashtable<K,V> 在JDK中Map的UML類圖中的位置:

2.1 Hashtable<K,V> 類概述

      上圖中的繼承實現關係不夠詳盡,先看一下 Hashtable 定義:

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {

       Hashtable繼承實現了抽象類Dictionary<K,V>類,並且實現了Map<K,V>,Cloneable、Serializable三個接口。Dictionary<K,V>抽象類JDK已經註明是一個過時的類,新的實現應該實現Map<K,V>接口,而不是繼承該類。

      Hashtable存儲映射用的是內部私有靜態類 Entry<K,V>,基本思想就是 Entry<K,V>數組 加 Entry<K,V>鏈表 存儲映射數據。看一下 Entry<K,V> 的簡單定義:

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;

        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }

       Hashtable<K,V>中的鍵K和值V不能是null,使用時用作鍵K的對象必須實現 hashCode 方法和 equals 方法。常用的Integer和String等都實現了這兩個方法,如果是我們自己定義的的類作爲Key,一定要記得實現。Hashtable是線程安全的,因爲它的方法比如put、get等方法都用synchronized關鍵字修飾了,不過因爲這種實現方式鎖比較重,因此官方推薦如果不需要線程安全的Map實現,則推薦使用 HashMap;如果需要支持高併發的Map實現,推薦使用 ConcurrentHashMap。

       Hashtable中有兩個參數initialCapacity(初始容量)和 loadFactor(加載因子,float類型),兩者影響Hashtable何時擴容Entry數組的長度以及Rehash的過程。Entry數組的長度達到閥值threshold(threshold = initialCapacity * loadFactor),put方法中就會觸發擴容Rehash的過程,新的Entry數組長度 newCapacity = (oldCapacity * 2) + 1。

       新數組的長度最大爲 Integer.MAX_VALUE - 8。爲什麼不是整型最大值,原因如下:

Some VMs reserve some header words in an array,Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit)。

       initialCapacity的默認值爲 11,loadFactor默認值爲 0.75f。

2.1.1 構造方法

      Hashtable提供了四種構造方法:

/**
* 用指定初始容量和指定加載因子構造一個新的空哈希表。
*/
public Hashtable(int initialCapacity, float loadFactor)

/**
* 用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。
*/
public Hashtable(int initialCapacity)

/**
* 用默認的初始容量 (11) 和加載因子 (0.75) 構造一個新的空哈希表
*/
public Hashtable()

/**
* 構造一個與給定的 Map 具有相同映射關係的新哈希表。
*/
public Hashtable(Map<? extends K, ? extends V> t)

2.1.2 put 方法

public synchronized V put(K key, V value);

將指定的K-V映射關係放入Hashtable中,注意方法使用了synchronized關鍵字修飾。

入參:鍵和值都不可以爲 null,否則拋出NullPointerException。

返回值:此哈希表中指定key以前的值;如果不存在該key,則返回 null

代碼簡單分析:

public synchronized V put(K key, V value) {
        // 校驗映射的值value不能爲null
        if (value == null) {
            throw new NullPointerException();
        }

        // 校驗key是否已在Hashtable中
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        //計算key在Hashtable中位置,和整型的最大值做位與運算,保證得到一個正整數
        //對entry[]長度取模運算,得到key在數組中的位置
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            //如果數組中的位置已被佔用,遍歷該位置的entry鏈表,查看該key是否已存在
            //如果已存在,更新值,返回舊值
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        
        //該key不存在,新建entry對象加入Hashtable,返回null
        addEntry(hash, key, value, index);
        return null;
    }

再進入 addEntry(hash, key, value, index) 方法中看一下:

private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
        //檢驗數組長度是否達到容量閥值
        if (count >= threshold) {
            // 如果超過閾值,rehash過程
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        //創建新的entry,頭插法添加鏈表節點,tab[index]位置處放的是新加入的key對應的Entry對象.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

最後看一下 rehash() 過程:

    /**
     * 增加此哈希表的容量並在內部重新組織該哈希表
     * 以便更有效地容納和訪問其哈希表
     * 當哈希表中的鍵數超過該哈希表的容量和負載因子的乘積時,將自動調用此方法。
     */
    @SuppressWarnings("unchecked")
    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        //新的容量變爲舊容量的2倍 + 1
        //Entry[]最大容量的限制,整型最大值減8
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        //更新下一次觸發rehash的閥值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
        
        //重新映射hashtable中已有的數據
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

2.1.3 get 方法

public synchronized V get(Object key)

返回指定鍵key所映射到的值,如果此映射不包含此key的映射,則返回 null. 注意get()方法也是被synchronized修飾。

更確切地講,如果此映射包含滿足 (key.equals(k)) 的從鍵 k 到值 v 的映射,則此方法返回 v;否則,返回 null

入參:key不能爲null,否則拋出 NullPointerException

代碼很簡單:

public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        //找到tab[index],遍歷entry鏈表對比查找
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

2.1.4 containsKey方法

public synchronized boolean containsKey(Object key)

測試指定對象是否爲此哈希表中的鍵key

源代碼很簡單,和上面 get() 方法幾乎一樣:

public synchronized boolean containsKey(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return true;
            }
        }
        return false;
    }

2.1.4 containsValue 和 contains 方法

public boolean containsValue(Object value)

public synchronized boolean contains(Object value)

這兩個方法基本功能一致,都是測試此映射表中是否存在與指定值value關聯的鍵;其中containsValue方法內部直接調用的contains方法實現,所以方法定義沒有加synchronized。

public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;
        //兩層循環,第一層遍歷entry數組,第二層遍歷entry鏈表
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }

2.1.5 remove 方法

public synchronized V remove(Object key) 

從哈希表中移除該鍵及其相應的值。如果該鍵不在哈希表中,則此方法不執行任何操作。

入參:key不能爲null,否則拋出 NullPointerException

返回值:此哈希表中與該鍵key存在映射關係的值;如果該鍵沒有映射關係,則返回 null

public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            //刪除key操作就是考慮鏈表節點的刪除,prev節點記錄key的前置節點
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

 

    其餘的方法,如size()、keySet()、entrySet()等,大家可以自己看一下,這裏不再介紹了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章