Hashtable源码解析

1.HashTable简述

HashMap源码解析

参考路飞大佬HashTable解析

1.1HashMap与Hashtable继承体系

HashTable继承体系

可以看出,HashTable实现了Map接口,继承了Dictionary类。

在这里插入图片描述

HashMap继承体系

HashMap继承于AbstractMap,实现了Map接口。

在这里插入图片描述

1.2概述

  • HashTable是一个古老的(JDK1.0时就已存在)线程安全的容器,其核心方法都是synchronized修饰的。相反HashMap不是线程安全的。

关于key或者value为null的要求

  • HashTable不允许key或者value为NULL

        public synchronized V put(K key, V value) {
             // 不允许value为NULL。
             if (value == null) {
                 throw new NullPointerException();
             }
             //这里调用key的hashCode()方法,所以key也不能为NULL。
             int hash = key.hashCode();
     		// 。。。
         }
    
  • HashMap允许存在一个key为NULL的Entry,但是value为NULL的Entry的个数没有限制。

    static final int hash(Object key) {
            int h;
        	//如果key为NULL那么哈希值为0
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

1.3核心结构

1.3.1Entry节点

    private static class Entry<K,V> implements Map.Entry<K,V> {
        //key的hash值
        final int hash;
        final K key;
        V value;
        //产生hash冲突时要形成链表,next节点
        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;
        }

1.3.2重要属性

    //底层的Entry数组
	private transient Entry<?,?>[] table;
	
	//元素(Entry节点)个数
    private transient int count;
	
	//扩容阈值 (判断是否需要扩容 threshold = 哈希表长度 * 加载因子)
    private int threshold;
	
	//加载因子
    private float loadFactor;
	
	/*
	 * Java中的一种fail-fast(快速失败)机制,每次添加或删除元素(修改不会)modCount都会	   * +1,然后使用迭代器遍历时会先讲modCount的值赋给expectedModCount,然后在遍历的
	 * 时候会检查两者是否还相同(不相同说明在遍历期间有其他线程添加或者删除了元素,这时就	 * 会抛出ConcurrentModificationException异常)
	 */
    private transient int modCount = 0;

1.3.3构造方法

双参构造方法

   	/*
   	 * @param initialCapacity 初始化容量
   	 * @param loadFactor 加载因子
   	 *  就是根据传入的初始容量构造一个Entry数组,然后计算扩容阈值。
   	 */
	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。
        table = new Entry<?,?>[initialCapacity];
        
        /*
         *  MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
         *  阈值 = 数组长度 * 加载因子,这里跟INF - 7取一个min。
         */
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

其他构造器,调用的还是上面的双参构造方法

	//传入初始容量,调用的还是上面的双参构造器,加载因子默认为0.75
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }
	

	//空参构造器,默认的初始容量为11,加载因子为0.75
    public Hashtable() {
        this(11, 0.75f);
    }
	

	//传入一个Map,默认的初始容量为 max(2 * t.size(), 11),加载因子为11
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

总结

  • 这里跟HashMap的区别就是,HashMap调用无参构造器时,不会初始化Entry数组,只会为加载因子赋值为0.75,只有第一次put时才会创建Entry数组,且默认的数组长度为16.
  • HashTable调用无参构造器时,就会创建一个长度为11的Entry数组

1.4核心方法详解

1.4.1put()

  	/*
  	 * 同步方法。
  	 */
	public synchronized V put(K key, V value) {
        // value不允许为NULL。
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;
        
        //获取key的hashCode值
        int hash = key.hashCode();
		
        /*
         *  寻址算法
         * 0x7FFFFFFF = Integer.MAX_VALUE
         * (hash & 0x7FFFFFFF)的作用是将hash变为一个正整数
         *   直接对table.length进行取余,得到的值的范围就在 [0, len - 1]。
         */
        int index = (hash & 0x7FFFFFFF) % tab.length;
        
        @SuppressWarnings("unchecked")      
        //获取index位置的entry。
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        
        //遍历桶位,查找当前key是否已经存在于桶位的链表中。
        for(; entry != null ; entry = entry.next) {
            //hash值相等 并且 key的equals()结果也相等,进行替换
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                //替换
                entry.value = value;
                //返回旧的value。
                return old;
            }
        }
		//能走到这里,说明当前桶位中没有key相同的entry,需要将当前entry插入进去。
        addEntry(hash, key, value, index);
        //返回NULL。
        return null;
    }

当前的哈希表中不存在与当前entry的key相等的entry,这时就需要将当前的entry插入的哈希表中,调用的是addEntry()方法,我们看一下addEntry()方法的源码

    private void addEntry(int hash, K key, V value, int index) {
        //添加操作 modCount + 1
        modCount++;

        Entry<?,?> tab[] = table;
        
        //count = 当前哈希表中的元素个数,大于扩容阈值,所以需要扩容。
        if (count >= threshold) {
            //扩容操作,下面详细分析
            rehash();
           	/*
           	 * 扩容之后,当前entry寻址后的index会发生变化,
           	 * 所以重新计算。
           	 */
            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

      	//获取当前index桶位的头结点
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        
        //将当前的entry插入到桶位的头结点,它的next节点是e(原头结点)
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

总结

put大致流程

  • 前置条件:key和value都不允许为NULL
  • 调用key的hashCode获取hash值,对length取余寻址,获取桶位索引index。
  • 遍历当前桶位,判断是否存在相同key的entry,存在直接替换value,然后返回旧的value。
  • 不存在,调用addEntry(),判断是否需要扩容,需要扩容就去扩容,然后重新寻址
  • 获取寻址后的桶位index,将当前entry直接插入到桶位头节点。结束。

流程图

在这里插入图片描述

与HashMap的几点区别

  • HashTable中的entry的key的最终的hash值就是其hashCod()方法的返回值,而HashMap中的Entry的key的最终的hash值是 hashCode ^ (hashCode >>> 16)

  • HashTable的寻址算法为(hash % tab.length),而HashMap的寻址算法是(hash & tab.length - 1)

1.4.2rehash()

总结

扩容操作很简单,(一般情况)直接扩容为 oldCapacity * 2 + 1,然后将原哈希表中的每个Entry重新寻址后插入到新表中(头插法)。

    @SuppressWarnings("unchecked")
    protected void rehash() {
        //原哈希表长度
        int oldCapacity = table.length;
        
        //oldMap引用原哈希表
        Entry<?,?>[] oldMap = table;

        /*
         * 一般情况: 扩容为原oldCapacity * 2 + 1 
         */ 
        int newCapacity = (oldCapacity << 1) + 1;
        
  		//情况很少。一般size不会是INF级别的。
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        
        //根据newCapacity创建一个新Entry数组
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
		
        modCount++;
        
        //重新计算扩容阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        //table引用新的Entry数组
        table = newMap;
		
        /*
         * 遍历原哈希表,将所有的entry重新寻址插入到新的哈希表中
         */
        for (int i = oldCapacity ; i-- > 0 ;) {
            //遍历每一个桶位
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                //e指向当前节点
                Entry<K,V> e = old;
                //old向后走
                old = old.next;
                //重新寻址
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                //当前节点指向桶位头结点
                e.next = (Entry<K,V>)newMap[index];
                //桶位头节点变为当前节点,完成插入操作。
                newMap[index] = e;
            }
        }
    }

1.4.3remove()

    /*
     * 同步方法。
     * 寻址,然后遍历桶位寻找待删除节点,找到后直接删除即可。
     */
	public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
		
        /*
         * 获取hash值然后寻址
         */
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        
        //获取桶位头结点
        Entry<K,V> e = (Entry<K,V>)tab[index];
        
        /*
         *  prev指向当前节点的前驱节点 e指向当前节点
         */
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            //找到了要删除的entry
            if ((e.hash == hash) && e.key.equals(key)) {
                //modCount + 1
                modCount++;
                //prev != null 表示e非头结点
                if (prev != null) {
                    //直接将e干掉
                    prev.next = e.next;
                //e是头结点
                } else {
                    //将头结点变为e的next
                    tab[index] = e.next;
                }
                //元素个数-1
                count--;
                //获取value
                V oldValue = e.value;
                //将e.value置为NULL,帮助GC 
                e.value = null;
                //将旧值返回
                return oldValue;
            }
        }
        //不存在 返回NULL。
        return null;
    }

1.4.4get()

	/*
	 *  同步方法,获取指定key的value。
 	 */
    public synchronized V get(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) {
            //查找成功,返回value。
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        //查找失败,返回NULL。
        return null;
    }

1.5总结

  • Hashtable是线程安全的容器(方法都加了synchronized),底层只有数组 + 链表

  • Hashtable不允许key或者value为NULL。

  • Hashtable存在fast-fail机制,modCount实现。

  • hash值是key的hashCode()的返回值

  • 寻址算法 hash % table.length

  • 调用无参构造器默认初始容量为11,加载因子为0.75

  • 一般情况下,扩容为 oldCap * 2 + 1

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shstart7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值