(四)Hashtable详解

1,038 阅读5分钟

往期推荐

Hashtable概述

Hashtable是实现Map接口的双列集合,底层基于数组+链表的数据结构实现的,无序且键和值都不允许存储null值,Hashtable是线程安全的(是基于synchronized实现).

Hashtable底层数据结构

image.png

Hashtable的类图

image.png

Hashtable的属性

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    
    //存储键值对Entry的哈希桶数组
    private transient Entry<?,?>[] table;

    //哈希桶数组table中存放的键值对Entry数量
    private transient int count;

    //哈希桶数组table扩容阈值
    //threshold=capacity(数组容量) * loadFactor(加载因子)
    private int threshold;

    //加载因子
    private float loadFactor;

    //Hashtable结构性修改次数
    private transient int modCount = 0;
    
}

Hashtable构造方法

  • 构造一个空的 Hashtable,使用默认容量(11)和加载因子(0.75f)
public Hashtable() {
    this(11, 0.75f);
}
  • 构造一个空的 Hashtable,使用自定义的初始容量,默认的加载因子(0.75f)
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
  • 构造一个空的 Hashtable,使用自定义的初始容量自定义的加载因子
public Hashtable(int initialCapacity, float loadFactor) {
    //如果自定义初始容量小于0,则抛IllegalArgumentException
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
                                           
    //如果自定义的加载因子小于0或者为非数值类型,则抛IllegalArgumentException
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);
    
    //如果自定义初始容量等于0,则使用1作为Hashtable的初始容量
    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    //新建一个指定容量的Entry数组
    table = new Entry<?,?>[initialCapacity];
    //计算扩容阈值
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

Hashtable的静态内部类Entry<K,V>

private static class Entry<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    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;
}

Hashtable的put(K key, V value)方法

  • put(K key, V value)实现往Hashtable中添加Entry键值对,其中调用了addEntry()方法实现具体的添加操作。
public synchronized V put(K key, V value) {
    //判断value是否为null,确保不存在null值
    if (value == null) {
        throw new NullPointerException();
    }

    //确定该键是否存在于哈希桶数组中
    Entry<?,?> tab[] = table;
    //获取键的哈希值
    int hash = key.hashCode();
    //计算键key在哈希桶数组table中的存储下标
    int index = (hash & 0x7FFFFFFF) % tab.length;
    
    @SuppressWarnings("unchecked")
    //将table数组index位置的值转换为Entry类型的数据
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    //如果index位置的键值对entry不为空,则遍历链表,找到键相同的Entry键值对
    for(; entry != null ; entry = entry.next) {
        //如果Hashtable中原来的键值对的哈希值等于待插入的键值对的哈希值
        //并且两个键值对对应的键key相等,则覆盖原来的键值对的值,无需插入
        if ((entry.hash == hash) && entry.key.equals(key)) {
            //保存旧值
            V old = entry.value;
            //覆盖旧值
            entry.value = value;
            //返回旧值
            return old;
        }
    }
    //哈希桶数组中不存在相同的键,调用addEntry()方法实现添加
    addEntry(hash, key, value, index);
    //添加新的键值对成功,返回null
    return null;
}
  • addEntry(int hash, K key, V value, int index)

private void addEntry(int hash, K key, V value, int index) {
    //结构性修改次数+1
    modCount++;
    //获取哈希桶数组table
    Entry<?,?> tab[] = table;
    //如果哈希桶数组中Entry键值对的数量大于扩容阈值threshold
    //则调用rehash()进行扩容
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();
        //获取扩容后哈希桶数组table
        tab = table;
        //重新获取键的哈希值
        hash = key.hashCode();
        //重新计算key在新的哈希桶数组中的下标index
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

   
    @SuppressWarnings("unchecked")
    //将index位置的值的类型转换为Entry<K,V>类型
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //新建一个Entry键值对,并将其存储到哈希表中index位置处
    tab[index] = new Entry<>(hash, key, value, e);
    //哈希桶数组中键值对数量+1
    count++;
}

Hashtable的扩容方法rehash()

扩容公式:(当前哈希桶数组的容量*2) + 1

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

    // 计算新的数组容量
    int newCapacity = (oldCapacity << 1) + 1;
    //如果新数组的容量大于Hashtable的最大容量
    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];
    //结构修改性次数+1
    modCount++;
    //重新计算扩容阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    //将扩容后的entry数组赋值给table
    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;
        }
    }
}

Hashtable与HashMap的区别

  • 线程安全: HashMap 是非线程安全的,Hashtable 是线程安全的;Hashtable 内部的方法大都经过 synchronized 修饰。关于synchronized可以参考另一篇文章# synchronized简介:

  • 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用ConcurrentHashMap );

  • 对 Null 键 和 Null 值的支持: HashMap 中,只能有一个键为null ,可以有一个或多个键所对应的值为 null。但是在 Hashtable 中 put 进的键值都不能为null,否则会抛NullPointerException异常。

  • 初始容量大小和每次扩容大小的不同:

  • 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1HashMap 默认的初始化大小为16,之后每次扩充,容量变为原来的2倍

  • 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小。

  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

  • 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

以上就是对Hashtable的介绍,如有错误还请大佬们留言指正... 默认标题_动态分割线_2021-07-15-0.gif

往期推荐