深入HashTable 源码分析

分析是基于jdk11,但与jdk1.8相差并不大
HashTable是一个安全散列集,它是以Key-Value对的形式存储,和HasMap一样它同样也存在哈希冲突的情况,因此HashTable采用了数组+单链表的方式来减少哈希冲突(简称拉链法),此外,它的所有涉及到并发的方法的被synchroniced关键字修饰(即在方法加了同步锁),所以在并发情况下是线程安全的,但又因为被sysnchronized修饰所以性能没有hashmap好(HashTable的性能还和初始化容量和加载因子有关,这影响着是否需要重新哈希)。

HashTable采用链表存储的数据结构,在进行添加修改的时候比较快,不需要移动元素,只需要修改指向(指针)即可,但在查找的时候就不能顺序查找了,只能通过遍历一次链表。

下图是大概的HashTable的数据结构图:

在这里插入图片描述
从图中就基本可以清楚知道HashTable是通过在数组和链表的结构来存储数据和减少哈希冲突的。下面开始分析源码:

  1. 首先看HasTable的存储结构

    //这个变量是HashTable的存储链表的数组
    private transient Entry<?,?>[] table;
    
    //该类是HashTable的内部静态类,它是一种链表结构,同时也是真正存储数据的类
     private static class Entry<K,V> implements Map.Entry<K,V> {
     //每个存储元素key的hash值
            final int hash;
          //k-v对
            final K key;
            V value;
            //后继指针
            Entry<K,V> next;
            //可以看到构建一个节点是采用头插法的,即把新元素在原来的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;
            }
    //中间省略了一下基本操作。。
    //这个是修改值,是直接修改并返回旧的数值
            public V setValue(V value) {
                if (value == null)
                    throw new NullPointerException();
    
                V oldValue = this.value;
                this.value = value;
                return oldValue;
            }
            //equeals是根据k-v是否相同来判断的
            public boolean equals(Object o) {
                if (!(o instanceof Map.Entry))
                    return false;
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
    
                return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
                   (value==null ? e.getValue()==null : value.equals(e.getValue()));
            }
           //这返回的是这个存储k-v这个结构的哈希码,计算是key的哈希值与value的哈希值进行异或就得到哈希码
            public int hashCode() {
                return hash ^ Objects.hashCode(value);
            }
        }
    
  2. HashTable的全局变量并分析相关作用

    //基础了Dirctionary这个类和实现了map接口
    public class Hashtable<K,V>
        extends Dictionary<K,V>
        implements Map<K,V>, Cloneable, java.io.Serializable {
    
        /**
         * 数组存储每一个链表头
         */
        private transient Entry<?,?>[] table;
    
        /*
        *计算元素的个数
         */
        private transient int count;
    
        /**
         *这是一个阈值,如果超过这个阈值就会进行rehash,性能会有所下降
         *它的计算公式是 当前容量*加载因子
         * @serial
         */
        private int threshold;
    
        /**
         *这个是加载因子
         * @serial
         */
        private float loadFactor;
    
        /*
        *修改操作的次数
         */
        private transient int modCount = 0;
    
        private static final long serialVersionUID = 1421746759512286392L;
    
    
  3. 构造函数是如何初始化的

    HashTable默认初始化容量是11,loadFactor=0.75

      
        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];
            //计算阈值,容量*加载因子 与最大容量比较取小的作为阈值
            threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        }
    
        /**
         *带容量的构造方法默认初始化加载因子是0.75
         */
        public Hashtable(int initialCapacity) {
            this(initialCapacity, 0.75f);
        }
    
        /**
         * 默认构建一个容量为11的entry数组,加载因子默认是0.75
         */
        public Hashtable() {
            this(11, 0.75f);
        }
    
        /**
         *带有map序列的构造方法 
         */
        public Hashtable(Map<? extends K, ? extends V> t) {
          //内部通过上面的构造函数初始化,容量是当前序列大小的两倍与11比较取大的,加载因子默认0.75
            this(Math.max(2*t.size(), 11), 0.75f);
            //初始化后进行元素添加//该方法在后面会分析
            putAll(t);
        }
    
    
  4. HashTable的put方法分析

    大概实现过程:添加元素前先根据key的哈希码来求出 当前元素应该添加在那个条拉链(链表)上(即先计算除在数组那个位置),然后获取那个位置的链表头,对链表进行遍历看是否已经存在,如果存在则把值修改为新值,返回旧值,如果不存在则调用 addEntry方法,addEntry方法根据key和哈希码进行链表插入(头插法),然后再把原来的头节点修改为这个添加完元素的链表的头接节点(即头节点会该为新添加这个元素)

           /*
           *批量添加只是通过遍历来添加
           */
    
        public synchronized void putAll(Map<? extends K, ? extends V> t) {
            for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
                put(e.getKey(), e.getValue());
        }
       
       //加了同步锁的put方法
       public synchronized V put(K key, V value) {
           //判空
            if (value == null) {
                throw new NullPointerException();
            }
    
            // 先把链表头数组拷贝一份
            Entry<?,?> tab[] = table;
            //计算key的哈希码,计算String的哈希码,1.8的计算是 hash*31+val[i]循环遍历,即每次哈希码*31+每个字符的ascall码
            int hash = key.hashCode();
            //根据hash计算应该存储在那条链表上(散列)
            int index = (hash & 0x7FFFFFFF) % tab.length;
            //根据下标获取链表头
            Entry<K,V> entry = (Entry<K,V>)tab[index];
            //循环遍历看是否已经存在,如果存在修改为当前值
            for(; entry != null ; entry = entry.next) {
                if ((entry.hash == hash) && entry.key.equals(key)) {
                    V old = entry.value;
                    entry.value = value;
                    return old;
                }
            }
    //如果不存在则调用链表的添加元素
            addEntry(hash, key, value, index);
            return null;
        }
    //改方法采用的是链表的头插入法
        private void addEntry(int hash, K key, V value, int index) {
        //拷贝一份数组
            Entry<?,?> tab[] = table;
            //判断当前元素的个数是否超过了阈值,如果超过了阈值则进行重新哈希,具体的重新哈希放到后面分析
            if (count >= threshold) {
                // Rehash the table if the threshold is exceeded
                rehash();
              //重新哈希后重新赋值给tab
                tab = table;
                //并重新获取key的hash
                hash = key.hashCode();
                //重新计算散列,得到具体的数组位置
                index = (hash & 0x7FFFFFFF) % tab.length;
            }
    
            // 获取对应位置的链表头
            @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>) tab[index];
            //采用头插法构建一个节点 并存储新的头节点,具体可以看开头的链表结构分析
            tab[index] = new Entry<>(hash, key, value, e);
            //修改个数
            count++;
            //修改操作次数
            modCount++;
        }
    
    
  5. remove方法的分析

    大致实现过程:计算key的哈希码,然后根据哈希码,进行散列计算在那条拉链(链表上),然后直接修改链表的指针即可

       //注意每个修改方法都被synchronized标识了
       public synchronized V remove(Object key) {
            Entry<?,?> tab[] = table;
            //计算key的哈希码
            int hash = key.hashCode();
            //进行散列
            int index = (hash & 0x7FFFFFFF) % tab.length;
            //获取链表头
            @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>)tab[index];
            //对e链表进行遍历寻址key和haxi都相同的
            for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            //如果是要找的元素
                if ((e.hash == hash) && e.key.equals(key)) {
                    //prev是e的前一个节点,判断prev不为空则让e的后继指向prev的后继节点,相当于跳个e这个节点的后继的节点指向prev的后继节点
                    if (prev != null) {
                        prev.next = e.next;
                    } else {
                    //否则是头节点,只需要把tab[index]的链表头改为后继节点即可
                        tab[index] = e.next;
                    }
                    //常规操作
                    modCount++;
                    count--;
                    V oldValue = e.value;
                    e.value = null;
                    return oldValue;
                }
            }
            return null;
        }
    
  6. get方法分析

    由于和上述remve前面部分相同则不进行分析了

        public synchronized V get(Object key) {
        //获取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 (V)e.value;
                }
            }
            return null;
        }
    
  7. HashTable的rehash分析

        //数组的最大容量
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
        /**
         * rehash方法是超过了阈值进行扩容并重新散列计算元素的位置
         */
        @SuppressWarnings("unchecked")
        protected void rehash() {
        //获取旧数组的容量
            int oldCapacity = table.length;
            //拷贝一份旧数组
            Entry<?,?>[] oldMap = table;
    
            //计算新的数组容量,但真正创建容量不一定是这个
            //扩容的计算公式是 旧数组的容量*2+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++;
            threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
            //把table指向新容量的数组
            table = newMap;
           //遍历旧数组进行重新散列
            for (int i = oldCapacity ; i-- > 0 ;) {
            //遍历每一条链表的元素
                for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                //获取头节点元素,并把old指向后继节点
                    Entry<K,V> e = old;
                    old = old.next;
    				//重新计算散列,来确定在新数组的那个位置
                    int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                    //把新数组的链表头指向e的后继节点
                    e.next = (Entry<K,V>)newMap[index];
                    //更新新数组的链表头
                    newMap[index] = e;
                }
            }
        }
    
  8. 获取entrySet和keySet方法

      //这几个方法就是获取一个set的集合或者key的集合或者vales就不分析了
       public Set<Map.Entry<K,V>> entrySet() {
            if (entrySet==null)
                entrySet = Collections.synchronizedSet(new EntrySet(), this);
            return entrySet;
        }
        
            public Set<K> keySet() {
            if (keySet == null)
                keySet = Collections.synchronizedSet(new KeySet(), this);
            return keySet;
        }
    
  9. 还有一些其他方法都和上面大同小异就不进行分析了都是重复的

总结:HashTable是通过同步锁线程安全的(和vector一样),每次扩容都是旧数组大小的两倍,如果超出最大容量则使用最大容量来进行扩容,在添加,修改、删除方面速度比较快,在查询方面需要进行变量,此外,要尽量避免重新散列,因为重新散列会对性能有所影响。
HashTable源码分析
LinkedList源码分析
Vector源码分析
CopyOnWriteArrayList源码分析
SynchorincedList源码分析

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值