一篇文章带你搞定HashTable

在咱们开讲源码之前,首先需要了解下什么是哈希表?

散列表(Hash table 又称哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中的一个位置来访问记录,以加快查找的速度.这个映射函数就叫做散列函数,存放记录的数组叫做散列表.              —— 百度百科

如图:

在Java中HashTable以数组+链表来实现,相对于HashMap来说要简单得多.HashTable不同于HashMap,它内部不允许插入null值,同时它是线程安全的,所有的读写操作都进行了锁保护,但也难以避免的对读写效率产生了较大影响.因此在日常开发中为保证线程安全一般建议使用ConcurrentHashMap.

为啥Java中Hashtable中的t要小写? 这不符合驼峰命名规则啊

stack overflow

大意: Hashtable创建于Java1,而集合的统一命名规范是后来在Java2中建立的,而当时又发布了新集合来代替它,再加上大量Java程序使用Hashtable类,考虑到兼容问题不可能将Hashtable改为HashTable.同时Hashtable已经过时了,不建议在代码中使用.

源码分析

结构图

继承关系

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {

Dictionary是JDK1.0里引入的抽象类,用于存储键/值对,作用和Map类似.注意Dictionary类已经过时了,在实际开发中,可以通过实现Map接口来完成存储键值对的功能.

类中属性

    /** 内部维护了一个 Entry 数组 */
    private transient Entry<?,?>[] table;

    /** 哈希表里的元素数量 */
    private transient int count;

    /** 触发扩容的阈值 */
    private int threshold;

    /** 加载因子 默认 0.75 */
    private float loadFactor;

    /** 记录 涉及到结构变化的次数(offer/remove/clear等)  */
    private transient int modCount = 0;

    /** 版本号 */
    private static final long serialVersionUID = 1421746759512286392L;

table数组里存的Entry实际上是一个单向链表,哈希表的键值对都是存在table里的.

 private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
        
        ...
    }

构造函数

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

    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);

        //初始容量最小为1
        if (initialCapacity==0) initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

Hashtable的默认容量是11,loadFactor默认加载因子0.75.threshold
数组容量 * loadFactor.

核心函数

添加函数
public synchronized V put(K key, V value) {
    if (value == null) {
        throw new NullPointerException();
    }
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();

    // &运算取正值,再取模计算位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    Entry<K,V> entry = (Entry<K,V>)tab[index];

    // 如果这个hash和key都已经存在了,就把原来的value替换掉
    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;
}

put函数通过关键字synchronized保证了线程安全.第一行就表明了Hashtable中value都不能为null.通过hash & 0x7FFFFFFF来规避掉负数,再进行分配位置.如果key经常存在了,则覆盖旧值并返回旧值.核心通过addEntry来添加元素.

    private void addEntry(int hash, K key, V value, int index) {
        modCount++;

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

        // 若当前容量已经大于 阈值 进行rehash扩容
        if (count >= threshold) {  // threshold = count * loadFlor
            // Rehash the table if the threshold is exceeded
            rehash();
            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        Entry<K,V> e = (Entry<K,V>) tab[index];
        // 最新插入的 排在最前面
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

addEntry函数 先会判断如果需要扩容 当前数量 >= 阈值,调用rehash进行扩容,否则链表叠加.

    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // 新容量为老容量的一倍+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;

        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函数先会把容量扩大1倍+1,然后创建一个newMap,把oldMap里的元素遍历复制到新的newMap里,这个过程是比较耗时的,同时此操作后数组和链表里元素的位置都会发生改变.

删除函数
    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        Entry<K,V> e = (Entry<K,V>)tab[index];
        // 遍历链表 e 当前节点, prev 上一个节点  next 下一个节点
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            // 性能优化 先 进行hash 判断
            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函数比较简单,通过key定位在数组中的位置,再遍历链表删除元素.

获取函数
    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) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

get函数和remove函数比较类似,都是先通过key定位在数组中的位置,再迭代链表找到元素并直接返回.

迭代器

Hashtable里迭代器使用的是比较老的Enumeration接口,其作用和Iterator类似,只提供了遍历的功能.虽然Enumeration还未被废弃,但现在代码里已经很少使用了.本篇文章就不再讲述了,有兴趣的可以去翻翻代码~

public interface Enumeration<E> {
    /** 判断是否还有元素  */
    boolean hasMoreElements();

    /** 如果还有元素则返回下一个元素,否则抛NoSuchElementException异常   */
    E nextElement();
}

结语

Hashtable整体是比较简单的,其内部充斥着大量的遍历操作,当数据量大的时候操作会非常耗时,非特殊情况下是用不到的.
一般来说,在日常开发中非并发场景推荐使用HashMap,并发场景下虽然可以用Hashtable,但是更推荐使用ConcurrentHashMap.
ConcurrentHashMap内部虽然也是使用Synchronized,但它是针对单个对象的锁,相比于Hashtable里锁的粒度更细,效率更高.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值