java1.8版本hashTable源码阅读

注:本文基于jdk1.8

hashTable与hashMap相同,都是使用散列表来实现存储结构,在功能上,他们两也基本相同,除了hashMap可以使用null的键值对和hashTable是线程安全的。

散列表的结构在jdk1.8之后略有不同,hashMap采用了链表+红黑树的方式来解决hash冲突,但hashTable仍只使用链表来解决。

hashTable所继承的类与HashMap不同,它是继承于Dictionary类,这是一个已经废弃的类,不建议使用。当然,hashMap和hashTable都实现了Map接口。

类的继承关系:

hashTable的主要成员变量:

    //hash表
    private transient Entry<?,?>[] table;

    //键值对的数量(注意不是hash表的长度)
    private transient int count;

    //加载阈值
    private int threshold;

    //加载因子
    private float loadFactor;

    //表结构变化次数,用于快速失败
    private transient int modCount = 0;

构造方法:

hashTable的构造方法与hashMap基本相同,但是无参的构造方法的默认配置不同:

    public Hashtable() {
        this(11, 0.75f);
    }

hashMap的默认大小为16,使用DEFAULT_INITIAL_CAPACITY存储;而hashTable的默认大小为11,且是硬编码的。


重要方法:

    public synchronized V put(K key, V value) {
        //确保value不为null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        //计算key的hash码。
        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) {
            //已存在相同的key,更新值
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        //执行添加操作
        addEntry(hash, key, value, index);
        return null;
    }
在put方法中只对value为null进行了处理,对key为null的处理实际上在object的hashCode方法中,这是一个native方法,会对null值抛出异常。

    private void addEntry(int hash, K key, V value, int index) {
        //结构变化次数加一
        modCount++;

        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            //超过加载阈值,rehash
            rehash();

            tab = table;
            hash = key.hashCode();
            //计算存储位置
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        //注意,原来的链表e放在了新链表的后头,也就是说新节点总是插入在链表的头部
        tab[index] = new Entry<>(hash, key, value, e);
        //键值对数量增加
        count++;
    }
addEntity方法总是将新加入的节点放置在链表的头部,在jdk1.7中hashMap也是这样,但是在jdk1.8中hashMap则会将新节点放置在链表的尾部。


    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 = newMap;
        //将老元素一一重新计算hash放置在新hash表中
        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;
            }
        }
    }
rehash操作对容量的变化与HashMap不同,hashMap总是变为原来的两边且保证为2的次幂,而hashTable为原来的两倍加一,保证为质数。这是因为他们的计算数组下标的方式不同导致的。

hashMap计算元素位置的方式为 hashCode&(length-1) ,hash码与长度减一进行与操作,当length为2的次幂时,length-1总为011**111形式,这样在与操作时都为自己本身,操作较快,且当出现某一位为0时,任何数只在该位不同的数操作的结果都是相同的,这大大增加了hash碰撞的几率,因此HashMap要求容量为2的次幂。

hashTable计算元素位置的方式为(hash & 0x7FFFFFFF) % tab.length,由于存在一部取余操作,所以当hash的低位相同高位不同时会很容易因为高位失效出现hash冲突,因此要求为质数。


    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        //计算hash码,这一步会进行key为null的处理
        int hash = key.hashCode();
        //计算位置
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        //遍历该位置的链表,找到对应的key将其移除
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

remove方法比较简单,找到对应的链表挨个查找将其除掉。

    public synchronized Object clone() {
        try {
            Hashtable<?,?> t = (Hashtable<?,?>)super.clone();
            t.table = new Entry<?,?>[table.length];
            for (int i = table.length ; i-- > 0 ; ) {
                t.table[i] = (table[i] != null)
                    ? (Entry<?,?>) table[i].clone() : null;
            }
            t.keySet = null;
            t.entrySet = null;
            t.values = null;
            t.modCount = 0;
            return t;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
hashTable的克隆方法为浅度克隆,仅仅克隆了结构并没有复制键值对,而是在克隆的对象中保存了一个键值对的地址,要实现深度克隆需要重写此方法。

hashTable的主要方法就为这些,注意上面的public方法都带有synchronized关键字,因此hashTable是线程安全的,但是这种线程安全的实现并不是很好,如果需要线程安全的类最好使用 Collections. synchronizedMap ()。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值