JAVA集合源码解析 Hashtable探索(基于JDK1.8)

JDK1.8Hashtable探索

本文的讨论分析是基于JDK1.8进行的

依旧是采用前几篇文章的大纲来进行介绍

1.简介

Hashtable 采用数组+单链表来实现的,Hashtable 实现了一个哈希表,它将键映射到值。任何非 null 对象可以用作键或值。为了成功存储和检索哈希表中的对象,用作键的对象必须实现 hashCode 方法和 equals 方法。Hashtable 的方法被synchronized修饰,因此是同步的、线程安全的。

2.探索

2.1类关系

Hashtable
Hashtable 继承了 Dictionary,能够重写里面的键值对应的一些方法,但是官方已经废弃它,推荐新的实现应该实现Map接口,而不是扩展这个类。
Hashtable 实现了 Map 接口,能够实现其中的所有可选的Map操作;
Hashtable 实现了 Cloneable 接口,能够使用 clone() 方法;
Hashtable 实现了 Serializable 接口,支持序列化操作。

2.2属性

    /**
     * 散列表数据
     */
    private transient Entry<?,?>[] table;

    /**
     * 散列表中的条目总数
     */
    private transient int count;

    /**
     * 临界值
     * (这个字段的值是(int)(capacity * loadFactor)。)
     *
     * @serial
     */
    private int threshold;

    /**
     * 加载因子
     *
     * @serial
     */
    private float loadFactor;

    /**
     * 记录结构性变化
     */
    private transient int modCount = 0;

    /**版本序列号 */
    private static final long serialVersionUID = 1421746759512286392L;

table 乍一看是个Entry[ ] 数组,其实也是个单向链表;
count 是记录了整个table的大小;
threshold 临界值或者阀值,是判断是否需要扩容的重要依据,具体计算为 threshold = capacity * loadFactor;
loadFactor 为加载因子
modCount 记录结构性变化,与fail-fast机制有关(后期专门写一篇介绍)。

2.3构造方法

构造方法
Hashtable 一共有 4 个构造方法。

    /**
     * 用指定的初始容量和指定的加载因子构造一个新的空的散列表。
     *
     * @param      initialCapacity   散列表的初始容量。
     * @param      loadFactor        散列表的加载因子。
     */
    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 = new Entry<?,?>[initialCapacity];//初始化table数组
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//初始化临界值
    }

    /**
     * 用指定的初始容量和默认加载因子(0.75)构造一个新的空哈希表。
     *
     * @param     initialCapacity   the initial capacity of the hashtable.
     */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * 使用默认的初始容量(11)和加载因子(0.75)构造一个新的空哈希表。
     */
    public Hashtable() {
        this(11, 0.75f);
    }

    /**
     * 使用与给定Map相同的映射构造一个新的散列表。
     * 散列表的初始容量足以容纳给定Map中的映射和默认加载因子(0.75)。
     *
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);//初始化hashtable
        putAll(t);//将t集合放入hashtable中
    }

1)Hashtable() 默认初始容量为 11 ,加载因子为 0.75。
2)Hashtable(int initialCapacity) 用指定的初始容量和默认加载因子(0.75)构造一个新的空哈希表。
3)Hashtable(Map<? extends K, ? extends V> t) 使用与给定Map相同的映射构造一个新的散列表。
4)以上 3 个构造方法其实最后都是调用了Hashtable(int initialCapacity, float loadFactor)构造方法。
5)Hashtable(int initialCapacity, float loadFactor) 在其中实现了 table 和 threshold 的初始化工作以及异常情况的判断。

2.4核心方法

(1)putAll( )

    /**
     * 将指定映射中的所有映射复制到此散列表。
     * 这些映射将替换此散列表对当前指定映射中的任何键的任何映射。
     *
     */
    public synchronized void putAll(Map<? extends K, ? extends V> t) {
        for (Map.Entry<? extends K, ? extends V> e : t.entrySet())//遍历t集合
            put(e.getKey(), e.getValue());//调用put方法
    }

putAll() 中遍历集合调用 put() 方法。

(2)put(K key, V value)

    /**
     * 将指定的键映射到此散列表中指定的值
     * 键和值都不能是 null
     * 通过使用与原始键相等的键调用 get 方法,可以检索该值。
     * 该方法是线程安全的,被synchronized修饰
     */
    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;//初始化数组
        int hash = key.hashCode();//计算hash值
        int index = (hash & 0x7FFFFFFF) % tab.length;//桶的位置索引
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {//如果桶中hash值相等且值相等
                V old = entry.value;//获取旧值
                entry.value = value;//覆盖旧值
                return old;
            }
        }

        addEntry(hash, key, value, index);//调用addEntry方法
        return null;
    }

1) put(K key, V value) 方法中会先初始化 table 数组,然后计算 key 对应的 hashcode() 以及 key 在桶中的位置索引 index。
2)如果放入的键值对都不为空,判断是否桶中 hash 和 key 相等的位置是否有值,有的话覆盖原值。
3)调用addEntry( ) 方法。

private void addEntry(int hash, K key, V value, int index) {
        modCount++;//结构性加1

        Entry<?,?> tab[] = table;
        if (count >= threshold) {//如果散列表中条目数大于临界值
            // 如果超出临界值,则扩容
            rehash();

            tab = table;//初始化新值
            hash = key.hashCode();//计算key的hash值
            index = (hash & 0x7FFFFFFF) % tab.length;//key的位置索引
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];//初始化新的链表
        tab[index] = new Entry<>(hash, key, value, e);//保存新链表到tab中
        count++;//链表大小加1
    }

1)如果 table 大小 count 大于临界值 threshold ,则进行扩容操作。
2)初始化新 tab 数组,key 的hash值,key 的位置索引。
3)保存新值到tab链表中,链表大小加1。

(3)rehash()扩容方法

    /**
     * 增加散列表的容量并在内部重新组织,以便更有效地容纳和访问条目。
     * 当散列表中的键数超过散列表的容量和加载因子时,将自动调用此方法。
     */
    @SuppressWarnings("unchecked")
    protected void rehash() {
        int oldCapacity = table.length;//保存旧的table容量
        Entry<?,?>[] oldMap = table;//保存旧的数组

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;//扩大容量为原来的2倍+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++;//结构性加1
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//计算新的临界值
        table = newMap;//保存新的table

        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;//产生新的hash位置
                e.next = (Entry<K,V>)newMap[index];//同一表中,新结点链接到表头
                newMap[index] = e;//保存新值
            }
        }
    }

1 ) 首先保存旧的table容量和数组
2 ) 扩大容量为原来的2倍+1,判断是否需要重新赋值容量值
3 ) 计算新的临界值,保存table数组,把旧数组遍历保存到扩容后的数组中

(4)get(Object key)

    /**
     * 返回指定键映射到的值,
     * 或者如果此映射不包含密钥的映射,则返回null。
     */
    @SuppressWarnings("unchecked")
    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;//赋值新数组链表
        int hash = key.hashCode();//计算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)) {//如果hash值相同且key相等
                return (V)e.value;//返回
            }
        }
        return null;
    }

1)先计算key的hashcode和key对应的索引位置。
2)遍历数组链表查找hash值和key都匹配的值。

(5)remove(Object key)

    /**
     * 从该散列表中删除键(及其相应的值)。
     * 如果密钥不在散列表中,此方法不执行任何操作。
     */
    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;//获取当前的table
        int hash = key.hashCode();//计算要移除key的hashcode
        int index = (hash & 0x7FFFFFFF) % tab.length;//计算位置
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];//保存该值到e中
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {//遍历链表
            if ((e.hash == hash) && e.key.equals(key)) {//如果hash值且key相等
                modCount++;//结构性加1
                if (prev != null) {//如果不为空
                    prev.next = e.next;//下一个表头替换该位置
                } else {//如果为空
                    tab[index] = e.next;//下一个表头赋值该位置
                }
                count--;//大小减1
                V oldValue = e.value;//获取旧值
                e.value = null;//置空当前位置值
                return oldValue;
            }
        }
        return null;
    }

1)先获取hashcode和key的位置值 。
2)先保存垓值到e中 。
3)如果hash值和key都相等,表头不为空时,下一个表头替换该位置,表头为空时,下一个表头赋值该位置。
4)返回移除的值。

3.总结

  1. Hashtable 继承的是 Dictionary,HashMap 继承的是 AbstractMap,Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似。AbstractMap实现了大部分的Map接口。
  2. Hashtable 的 put() 方法中是允许键和值为 null ,HashMap 则不允许为空。
  3. Hashtable 比 HashMap 多了 enumerator 迭代器。
  4. Hashtable 的大部分 public 方法都被 synchronized 修饰,是线程安全的,HashMap 不是,如果一个线程安全的实现是不需要的,建议使用 HashMap 代替 Hashtable。如果线程安全高度并发的实现是需要的,那么推荐使用java.util.concurrent.ConcurrentHashMap 或者 Collections.synchronizeMap 代替 Hashtable。

猜你喜欢

转载自blog.csdn.net/ouzhuangzhuang/article/details/80234065