从零开始的Java容器学习(六):Hashtable

275 阅读4分钟

Hashtable的简要介绍

上一节学了HashMap,这次来了解一下这个过时的容器:Hashtable。Hashtable跟HashMap一样也是基于哈希表实现的,表内元素都是Key-Value键值对映射,K、V都不允许null值,发生Hash冲突时通过单链表解决冲突,在容量不足时会扩容并重新hash,扩容是原大小(默认初始大小为11)乘2再加1;Hashtable通过synchronized对整个表枷锁来实现线程安全。

Hashtable继承自Dictionary,实现了Map、Cloneable、Serializable接口。、

从源码分析HashTable

成员变量

private transient Entry<?,?>[] table;//这个就是用于存放元素的哈希表了
private transient int count;//表中包含的元素数量
private int threshold;//扩容阈值,由当前容量*负载因子决定
private float loadFactor;//默认是0.75

private static class Entry<K,V> implements Map.Entry<K,V> {//Entry内存有hash、key、value、next
    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;
    }
    /*...以下省略...*/
}

构造方法

public Hashtable() {//有四种构造方法,默认初始大小为11,负载因子为0.75
    this(11, 0.75f);
}

添加

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }
    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();//hash值是key的hashcode
    int index = (hash & 0x7FFFFFFF) % tab.length;//下标index是hash值&第一个是0,后面全为1的值(32位),这一步操作之后能确保是一个正值,接着再对表长度进行取模
    @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)) {//如果找到key相同就更新value并返回旧value
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    addEntry(hash, key, value, index);//如果没有找到key相同就增加一个Entry然后返回null
    return null;
}
private void addEntry(int hash, K key, V value, int index) {
    modCount++;
    Entry<?,?> tab[] = table;
    if (count >= threshold) {//每次增加之前判定一下是否需要rehash扩容
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }
    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);//next==e说明是头插法
    count++;
}

删除

public synchronized boolean remove(Object key, Object value) {
    Objects.requireNonNull(value);
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();//hash值是key的hashcode
    int index = (hash & 0x7FFFFFFF) % tab.length;//hashToIndex方式以上已经说明
    @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) && e.value.equals(value)) {//找到了key、value都相同的值
            modCount++;
            if (prev != null) {//prev不是链表头结点
                prev.next = e.next;
            } else {//prev是头结点的情况
                tab[index] = e.next;
            }
            count--;
            e.value = null;
            return true;
        }
    }
    return false;
}

扩容

protected void rehash() {//每一步添加操作前都要计算以下容量和扩容阈值,超过就要进行扩容
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;
    // overflow-conscious code
    int newCapacity = (oldCapacity << 1) + 1;//每次扩容是(*2+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 ;) {//以从后往前的顺序将旧table中元素加入新table里
        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;
        }
    }
}

更新和查询

更新

public synchronized boolean replace(K key, V oldValue, V newValue) {//只有满足key,旧value,同时符合时才更新为新的value
    Objects.requireNonNull(oldValue);
    Objects.requireNonNull(newValue);
    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 (; e != null; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            if (e.value.equals(oldValue)) {
                e.value = newValue;
                return true;
            } else {
                return false;
            }
        }
    }
    return false;
}
public synchronized V replace(K key, V value);//同理这个不管三七二十一直接更新就对了
public synchronized V put(K key, V value);//这个也可以作为更新

查询

public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;//hashToIndex
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}
public synchronized V getOrDefault(Object key, V defaultValue) {//这个是若key为空就返回默认值
    V result = get(key);
    return (null == result) ? defaultValue : result;
}

遍历

简单地说一般有五种方法:(1)通过EntrySet返回键值对集合。(2)通过keySet获取Set类型对象的Iterator遍历。(3)通过values获取Collection类型对象的Iterator遍历。(4)通过keys获取枚举类型对象遍历。(5)通过elements获取枚举类型对象对象遍历。详细的就不写了,参考以前的文章和Hashtable源码。

总结

Hashtable实现了线程安全的key-value键值对映射集合,其底层是数组+单链表来解决hash冲突问题,每次插入新的键值对映射前会计算是否超过rehash阈值,超过则进行rehash扩容,扩容是(*2+1),插入是头插法(即在链表头插入新的元素),在修改时是快速失败机制但是在迭代器中快速失败无法保证。如果不需要线程安全的实现,建议使用HashMap代替Hashtable。如果需要线程安全的并发实现,那么建议使用ConcurrentHashMap代替Hashtable。