Hashtable源码研究

    上几篇笔记研究了HashMap和LinkedHashMap,此笔记研究Hashtable。
一:概述
    先来看下Hashtable的类信息

//跟HashMap相比,继承的类不一样,实现具
//的接口倒是一样的,说明他们具有类似的功能
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    /**
     * The hash table data.
     * hash数组,跟HashMap一样
     */
    private transient Entry<?,?>[] table;

    /**
     * The total number of entries in the hash table.
     * 元素个数
     */
    private transient int count;

    /**
     * The table is rehashed when its size exceeds this threshold.  (The
     * value of this field is (int)(capacity * loadFactor).)
     * 扩容阈值
     *
     * @serial
     */
    private int threshold;

    /**
     * The load factor for the hashtable.
     * 加载因子
     *
     * @serial
     */
    private float loadFactor;

    /**
     * The number of times this Hashtable has been structurally modified
     * Structural modifications are those that change the number of entries in
     * the Hashtable or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the Hashtable fail-fast.  (See ConcurrentModificationException).
     * 修改次数
     */
    private transient int modCount = 0;

    /**
     * Constructs a new, empty hashtable with the specified initial
     * capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hashtable.
     * @param      loadFactor        the load factor of the hashtable.
     * @exception  IllegalArgumentException  if the initial capacity is less
     *             than zero, or if the load factor is nonpositive.
     * 两个参数的构造函数
     */
    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);

        //如果初始容量是0的话,那么置成1
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;

        //创建指定长度的Entry数组
        table = new Entry<?,?>[initialCapacity];

        //计算阈值
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

    /**
     * Constructs a new, empty hashtable with the specified initial capacity
     * and default load factor (0.75).
     *
     * @param     initialCapacity   the initial capacity of the hashtable.
     * @exception IllegalArgumentException if the initial capacity is less
     *              than zero.
     *
     * 带有容量的构造函数
     */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     * 默认构造函数,容量是11,加载因子是0.75;HashMap的容量是16,加载因子也是0.75
     */
    public Hashtable() {
        this(11, 0.75f);
    }

    /**
     * Constructs a new hashtable with the same mappings as the given
     * Map.  The hashtable is created with an initial capacity sufficient to
     * hold the mappings in the given Map and a default load factor (0.75).
     *
     * @param t the map whose mappings are to be placed in this map.
     * @throws NullPointerException if the specified map is null.
     * @since   1.2
     *
     * 根据一个Map创建一个Hashtable,容量是原来容量的两倍和11的最大值
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }
}

    Hashtable的类信息还是比较简单的,下面看下他存入元素的方法put(K key, V value):

    /**
     * Maps the specified <code>key</code> to the specified
     * <code>value</code> in this hashtable. Neither the key nor the
     * value can be <code>null</code>. <p>
     *
     * The value can be retrieved by calling the <code>get</code> method
     * with a key that is equal to the original key.
     *
     * @param      key     the hashtable key
     * @param      value   the value
     * @return     the previous value of the specified key in this hashtable,
     *             or <code>null</code> if it did not have one
     * @exception  NullPointerException  if the key or value is
     *               <code>null</code>
     * @see     Object#equals(Object)
     * @see     #get(Object)
     */
    public synchronized V put(K key, V value) {
        // Make sure the value is not null

        //如果value为空就死给你看
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        //拿到Hashtable的数组
        Entry<?,?> tab[] = table;

        //计算key的hash值
        int hash = key.hashCode();

        //桶的索引是用key的hash值和0x7FFFFFFF做与运算,最后对数组长度取余
        int index = (hash & 0x7FFFFFFF) % tab.length;

        //拿到指定桶上的Entry对象
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];

        //Hashtable上只有链表,没有红黑树,所以遍历链表
        for(; entry != null ; entry = entry.next) {

            //如果遍历到的Entry的hash和目标hash一样,而且两个的key
            //也相等,那么只需要更新value即可,同时将老的value返回回去
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        //若干Hashtable里面没有key与目标key一样的Entry,那么新插入一个
        addEntry(hash, key, value, index);

        //如果是插入全新的Entry的话,就返回空
        return null;
    }

    put方法就是先查找,找到了就更新;没找到就插入一个全新的Entry,首先可以看到,put这个方法加了synchronized,说明这个方法是线程安全的,实际上Hashtable的大部分方法都是线程安全的,这是跟HashMap的一个重要区别;然后在put里面对value进行了空判断,也就是说,HashTable是不允许存入空的键值对的,这也是和HashMap的一个在重要区别;下面分析插入的方法addEntry:

private void addEntry(int hash, K key, V value, int index) {
    //modCount自增,注意到没,更新操作是不自增的
    modCount++;

    Entry<?,?> tab[] = table;

    //如果当前元素数量超出阈值
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        //重新hash,也就是扩容
        rehash();

        tab = table;

        //计算key的hash值
        hash = key.hashCode();

        //计算key的索引
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    //将桶上原来的Entry赋值给e
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];

    //根据原来的Entry、hash、key和value
    //创建一个新的Entry放到指定的桶上
    tab[index] = new Entry<>(hash, key, value, e);

    //数量自增
    count++;
}

     addEntry方法的核心是rehash(),这个方法看起来是重新计算hash,事实上他确实也计算了,这个方法的核心作用还是扩容:

    /**
     * Increases the capacity of and internally reorganizes this
     * hashtable, in order to accommodate and access its entries more
     * efficiently.  This method is called automatically when the
     * number of keys in the hashtable exceeds this hashtable's capacity
     * and load factor.
     */
    @SuppressWarnings("unchecked")
    protected void rehash() {
        //老数组的长度
        int oldCapacity = table.length;

        //将老数组赋值给oldMap
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        //新数组的长度是老数组长度的2n + 1
        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++;

        //计算新的阈值,在新容量*加载因子和最大值 + 1之间取最小值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);

        //将新数组赋值给table
        table = newMap;

        //双重遍历老数组,第一重遍历是遍历桶上的各个Entry,将老数组的节点移到新数组
        for (int i = oldCapacity ; i-- > 0 ;) {
            //第二重遍历是遍历桶上Entry后面接的链表
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {

                //将old赋值给e,e代表遍历到的节点
                Entry<K,V> e = old;

                //old指向他的后继节点,下次遍历就遍历到后继节点了
                old = old.next;

                //重新计算Entery的索引,因为newCapacity变了,所以index也会变
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;

                //把新桶上已存在链表节点作为当前节点的后继节点
                e.next = (Entry<K,V>)newMap[index];

                //遍历到的节点放到新数组的桶上,发现没?这是链表的头结点
                newMap[index] = e;
            }
        }
    }

    理解了HashMap的扩容后,再理解Hashtable的扩容就简单多了;需要注意的是,老数组上的链表,经过扩容后可能会被拆散;那些没被拆散的节点,也就是重新落入同一个桶的节点,他们的前驱和后继的关系反过来了,原来是前驱节点的,在新数组里面就是后继节点了,反之亦然。既然有put方法,那就必然有get方法:

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key.equals(k))},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @param key the key whose associated value is to be returned
     * @return the value to which the specified key is mapped, or
     *         {@code null} if this map contains no mapping for the key
     * @throws NullPointerException if the specified key is null
     * @see     #put(Object, Object)
     */
    @SuppressWarnings("unchecked")
    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        //计算key的hash
        int hash = key.hashCode();

        //根据hash值计算桶的索引
        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 (V)e.value;
            }
        }
        return null;
    }

    get方法超简单,不多说。增、改、查都分析了,下面分析删的动作:

    /**
     * Removes the key (and its corresponding value) from this
     * hashtable. This method does nothing if the key is not in the hashtable.
     *
     * @param   key   the key that needs to be removed
     * @return  the value to which the key had been mapped in this hashtable,
     *          or <code>null</code> if the key did not have a mapping
     * @throws  NullPointerException  if the key is <code>null</code>
     */
    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        //计算key的hash
        int hash = key.hashCode();

        //根据hash计算桶的索引index
        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的hash相等,而且key本身也相等,说明就是要删掉这个节点
            if ((e.hash == hash) && e.key.equals(key)) {
                //自增
                modCount++;

                //如果prev不为空,说明要删除的不是头结点
                if (prev != null) {
                    //那么将目标节点的头结点指向目标节点的后继节点
                    prev.next = e.next;
                } else {
                    //如果删除的就是头结点,那么将原来头结点的后继节点指定为头结点
                    tab[index] = e.next;
                }

                //元素总数 - 1
                count--;

                //取出要删除的节点的value
                V oldValue = e.value;

                //将e的value置空(需要吗?)
                e.value = null;

                //返回删除节点的value
                return oldValue;
            }
        }
        return null;
    }

    可以看到,remove方法也是非常简单的。
    Hashtable就讲这么多吧,其他的方法都差不多,基本上能都是通过hash计算桶的索引,然后操作链表,非常简单;我个人觉得Hashtable是HashMap的简化版,就是把HashMap中的红黑树去掉,同时在公开的方法增加了同步操作;只要理解了HashMap,理解Hashtable就水到渠成了,不管是理解哪个,都要理解链表和树。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容