java源码解析之Hashtable(jdk1.8)

前言

前面两篇我们介绍了HashMap和LinkedHashMap。HashMap是jdk中最常用的哈希表实现,使用数组加链表的结构来组织数据,扩容操作会将之前的键值对顺序打乱。为了解决这个问题,jdk提供了LinkedHashMap。通过重写内部类Entry,新增两个成员属性LinkedHashMap.Entry类型的after和before,使所有的键值对序列都包含在一个双向链表上,并且不会受到扩容操作的影响。尽管LinkedHashMap解决了键值对无序的问题,但在并发访问下,两者都是线程不安全的,所以,jdk为我们提供了Hashtable来解决这个问题。

Hashtable概述

同样的我们对于类申明语句之前的说明文档翻译一下,看看作者的设计意图和使用中的注意事项。

1.此类实现了哈希表,任何非null的Object可以被用来作为key或者value(言外之意,不允许null值的存在,不光null键不可以,值也不可以)。
2.为了从Hashtable成功的存储和检索对象,被用做key的对象必须实现hashCode和equals方法。
3.有两个参数会影响Hashtable实例的表现:分别是初始容量和加载因子。capacity容量就是哈希表所拥有的桶的数量,初始容量就是Hashtable创建时候的容量。加载因子是扩容之前用来测量哈希表饱和程度的标准。初始容量和负载因子参数只是实现的提示。关于何时以及是否重新散列的确切细节依赖于方法调用的实现。
4.通常而言。0.75的加载因子在时间和空间消耗上提供了一个较好的平衡。较高的值削减了空间上的消耗,但会增加查找单一entry的时间。这会影响到Hashtable的大多数操作的表现如put和get。
5.初始容量在空间开销和再哈希操作所需开销之间维护着平衡,再哈希操作很耗时。初始容量超过Hashtable可以承载的最大entry数量除以加载因子时,再哈希操作将不会发生。然而初始容量设置太高,会浪费空间。
6.如果大量的entry同时在Hashtable中被创建,将Hashtable创建为拥有足够大的容量要比被动的扩容时候再哈希更加高效。
7.此类的迭代器也遵循fail-fast(快速失败机制)。如果这个类在迭代器被创建之后的任何时候发生了结构上的变化,不包括iterator自身的remove方法,将会抛出ConcurrentModificationException(并发修改异常)。因此,面对并发修改时,迭代器会快速而干净地失败,而不会冒风险在未来某个不确定的时间做出任意的、不确定的行为。将Hashtable的key和元素返回为枚举的方法并不是fail-fast的。
8.在java框架结合中,和一些新的集合实现不同,Hashtable是线程安全的,如果一个线程安全的实现不被需要,建议使用HashMap取代Hashtable。如果期望一个高并发的集合实现的话,建议使用java.util.concurrent.ConcurrentHashMap来替代Hashtable。

构造方法

同样的我们对于Hashtable的探究也从构造方法开始

//构造方法1
public Hashtable(int initialCapacity, float loadFactor) {//接受两个参数初始容量和加载因子和HashMap并无差异
        if (initialCapacity < 0)//参数校验
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))//加载因子必须为正且必须是数字。
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)//如果初始容量为0则让其等于1
            initialCapacity = 1;
        this.loadFactor = loadFactor;//初始化加载因子
        table = new Entry<?,?>[initialCapacity];//创建一个Entry类型的数组长度为参数initialCapacity的值。
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        //计算阀值在初始容量乘以加载因子的值和后者之间取最小值。
    }
    ——————————————————————————————————————————————————————————————————————————
    //构造方法二
    public Hashtable(int initialCapacity) {//只接受一个参数指定初始容量
        this(initialCapacity, 0.75f);//调用第一个构造方法创建一个指定初始容量和默认加载因子为0.75的Hashtable。
    }
    ——————————————————————————————————————————————————————————————————————————
    //无参数构造
    public Hashtable() {
        this(11, 0.75f);//这里要注意了,不指定初始容量时。Hashtable初始容量默认为11。Hashmap和LinkedHashMap中是16.
    }
    ————————————————————————————————————————————————————————————————————————
    //构造方法4
    public Hashtable(Map<? extends K, ? extends V> t) {//参数为Map的实现类
        this(Math.max(2*t.size(), 11), 0.75f);//确定Hashtable的初始容量一般情况下是参数map的二倍。
        putAll(t);//调用此方法将参数中的键值对插入到新的Hashtable中,putAll()方法会在后面介绍。
    }

仔细观察我们发现在初始化阶段,Hashtable就已经为table属性开辟了内存空间,这和HashMap中不同,HashMap是在第一次调用put方法时才为table开辟内存的。还有就是默认初始容量不同。除此之外Hashtable与HashMap并无太大的区别。为了探究其数据组织方式我们来看看其put方法:

 public synchronized V put(K key, V value) {//特别注意此方法用synchronized修饰符修饰表明他是同步方法。调用此方法必须的到Hashtable对象的锁
        // Make sure the value is not null
        if (value == null) {//value不能为空
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;//迭代table,将其赋值给局部变量tab
        int hash = key.hashCode();//这里也能看到他与HashMap的put方法的不同了
        //在HashMap中key的哈希值的计算是由HashMap提供的hash方法完成的。而这里直接
        //使用了key对象的hashCode方法,所以建议实现该方法。
        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)) {//这里的判断是基于hashCode和equals方法的。条件成立代表是键的再插入。
                V old = entry.value;
                entry.value = value;
                return old;
            }
      //这个方法就这么完了???好像哪里不对——他只是判断了键重复的状况,当键不冲突的时候又该怎么办呢接着往下看
        }
		//新键的插入执行下面这个方法
        addEntry(hash, key, value, index);//让我们看看这个方法做了什么
        return null;
    }
    ———————————————————————————————————————————————————————————————————————————
    //addEntry方法
    private void addEntry(int hash, K key, V value, int index) {
        modCount++;//何为modCount呢就是此Hashtable被结构性修改的次数何为结构性修改就是对键值对的增加或者移除。单单是改变已有键对应的值并不是结构性修改。

        Entry<?,?> tab[] = table;
        if (count >= threshold) {//count表示哈希表中总共的entry数量,当其大于阀值执行再哈希操作
            // Rehash the table if the threshold is exceeded
            rehash();//我们稍后在看这个方法

            tab = table;//此时table引用发生了变化重新将其赋值
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];//将之前数组index位置的节点引用赋值给e
        tab[index] = new Entry<>(hash, key, value, e);//调用构造方法完成键的插入,来看看这个构造方法把
        count++;
    }
    ————————————————————————————————————————————————————————————————————————
    //构造方法
     protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;//关键 这里将传入的next赋值给next变量。意思就是新插入的键会被放在链表的头部这跟HashMap还是有区别的,HashMap是放在链表的尾部的。
        }
  • 让我们来看看rehash()方法:
 protected void rehash() {
        int oldCapacity = table.length;//定义一个int类型的变量来存储旧的数组的长度
        Entry<?,?>[] oldMap = table;//定义一个数组表示旧的数组

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;//位运算即新的数组长度为旧的乘以2再加1
        if (newCapacity - MAX_ARRAY_SIZE > 0) {//MAX_ARRAY_SIZE数组内存空间的上限
            if (oldCapacity == MAX_ARRAY_SIZE)//即再哈希前数组长度已经等于上限
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];//定义了一个新的容量变化了的entry数组

        modCount++;
        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;//计算节点所在链表在新数组中的位置。
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;//还是最近插入的节点放在链表头部
            }
        }
    }

说到这里Hashtable的数据结构已经清晰明了,我们没有说明的一点是,Hashtable中用来存储键值对的Entry跟HashMap的Node类似。所以说整体上而言Hashtable跟HashMap差不多,都是数组加链表的组织结构。只不过HashMap中新插入的值放在链表的末端,而Hashtable则放在链表的头部。

  • 再来看看其get方法
	public synchronized V get(Object key) {//也用synchronized修饰同步方法
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();//根据key计算哈希值
        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;//遍历链表匹配key并返回
            }
        }
        return null;//没有匹配到返回null
    }

还有contains方法和containsValue、containsKey方法,前两个方法是一样的,他们都是同步方法。内部实现不复杂,读者可以自行查看。

  • remove方法
   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) {//遍历链表
            if ((e.hash == hash) && e.key.equals(key)) {//使用hashCode和equals方法匹配key
                modCount++;//匹配到了意味着可以移除则该计数器加一
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;//将头节点指向e的下一个节点
                }
                count--;//此计数器减1
                V oldValue = e.value;
                e.value = null;
                return oldValue;//返回旧的数据
            }
        }
        return null;//key不存在 返回null
    }

总结

至此Hashtable源码之旅就先告一段落了。Hashtable是哈希表的一种实现,不允许null值或者null键的存在。他的常用方法put、get、contains等都是同步方法,需要持有Hashtable的锁,通常情况下,在线程安全的类并不被需要的时候,建议使用HashMap。因为锁的释放与获取有一定的时间开销。本博客依旧基于jdk1.8,因本人水平有限,望多包涵。

猜你喜欢

转载自blog.csdn.net/weixin_40606398/article/details/88759812
今日推荐