工作之后,我对 HashTable 有了新的理解

写在前面的话

  1. 照旧,本文并没有完整的把所有 HashTable 的知识点和源码都解析完,只是挑了我个人觉得比较重要和有意思的点,如果解答了你的一个小困惑,记得给我点赞啊,你赞一下我可太爽了啊!
  2. HashTable 的这篇文章是我在学习完 HashMap 和 ConcurrentHashMap 之后才写的,我本来不打算看 HashTable 的源码的,因为在我实际的工作中根本不会用到 HashTable,但是无奈发现面试题都拿它来进行对比,所以才回过头去看了下。
    因此,如果是学生党,我觉得可以仔细看看,有助于后续看 HashMap 和 ConcurrentHashMap。如果是急于准备面试的同行,我觉得可以快速过一下就好了。之前写的关于 HashMap 和 ConcurrentHashMap 的文章也贴在这里了:

源码理解

一些关键变量

/**  
 * The hash table data. */private transient Entry<?,?>[] table;  
  
/**  
 * The total number of entries in the hash table. */
 private transient int count;  
  
/**  
 * The table is rehashed when its size exceeds this threshold.  (The * value of this field is (int)(capacity * loadFactor).) * * @serial  
 */  
private int threshold;  
  
/**  
 * The load factor for the hashtable. * * @serial  
 */  
private float loadFactor;  
  
// 有 modCount 的话,是不是意味着快速失败呢?后文细讲。
private transient int modCount = 0;

private static class Entry<K,V> implements Map.Entry<K,V> {  
    final int hash;  
    final K key;  
    // 注意,HashTable 的 Entry 节点中没有使用 volatile 修饰
    V value;  
    Entry<K,V> next;
} 

HashTable 的基础属性不多,依旧是常规的 table 数组,就不展开讲解了。


构造方法

HashTable 的构造方法也很常规,不过有两个注意点

  1. 默认容量是 11 ,而不是如 HashMap 那样要求是 2 的幂次方,可以复习一下 HashMap 这么设置容量的原因。
  2. table 数组在构造方法中就会被初始化,HashMap 则是懒加载的,可以复习一下这么做的优劣。
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);  
  
    if (initialCapacity==0)  
        initialCapacity = 1;  
    this.loadFactor = loadFactor;  
    table = new Entry<?,?>[initialCapacity];  
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);  
}  
  
 public Hashtable(int initialCapacity) {  
    this(initialCapacity, 0.75f);  
}  
  
public Hashtable() {  
    this(11, 0.75f);  
}  

public Hashtable(Map<? extends K, ? extends V> t) {  
    this(Math.max(2*t.size(), 11), 0.75f);  
    putAll(t);  
}

常规方法概览

列举几个常见方法的定义,在方法维度直接使用了 synchronized 修饰,因此关于 HashTable 为什么线程安全的原因已经明确了,简单粗暴。
多提一句,我觉得不能看到 synchronized 就本能地觉得这个代码差,优劣都是相对的概念,实际上是一种性价比的抉择。在我们实际的开发中 synchronized 并不少见,在一些并发度要求没那么高的场景下使用 synchronized 对业务来说也几乎是无感的。

  
public synchronized int size() {  
    return count;  
}  

public synchronized boolean isEmpty() {  
    return count == 0;  
}  
  
public synchronized Enumeration<K> keys() {  
    return this.<K>getEnumeration(KEYS);  
}  
  
public synchronized V get(Object key) {  
  // 先忽略 
}

public synchronized V put(K key, V value) {  
  // 先忽略
}

等。。。

get() 方法

概括一下步骤。

  1. 获取哈希值。
  2. 将哈希值和 0x7FFFFFFF 进行 & 操作,然后取模获得下标 index 。
  3. 获取 table 数组下标 index 处的元素,遍历匹配相等的元素。
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;  
}
  • & 0x7FFFFFFF 这个操作又出现了,还记得我们在 ConcurrentHashMap 的文章中专门对这块的解释吗?遗忘的话可以复习一下,简单来说就是获得一个大于 0 的哈希值。

  • 对 table 数组对应下标位置的元素的遍历中,没有出现对二叉树的判断,所以 HashTable 对哈希冲突的处理就是链表。


put() 方法

概括一下步骤。

  1. 对 value 值进行校验,如果为null则抛出空指针。
  2. 计算下标 index,并对 table 数组对应下标位置元素进行判断。
  3. 如果对应元素不为空,则说明存在哈希冲突,此时遍历到相等于的元素并进行更新。
  4. 如果对应元素为空,则说明是新增元素的场景,进入 addEntry 方法。(涉及扩容)
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();  
    int index = (hash & 0x7FFFFFFF) % tab.length;  
    @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)) {  
            V old = entry.value;  
            entry.value = value;  
            return old;  
        }  
    }  
  
    addEntry(hash, key, value, index);  
    return null;  
}

addEntry() 新增元素方法

  1. 先对 modCount 进行自增
  2. 判断是否达到扩容条件,如果是的话调用 rehash 方法进行扩容,并根据新的数组长度获得计算出新的 index 下标
  3. 在 table 数组对应 index 下标位置插入元素
  4. count 自增。
private void addEntry(int hash, K key, V value, int index) {  
    modCount++;  
  
    Entry<?,?> tab[] = table;  
    if (count >= threshold) {  
        // 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);  
    count++;  
}

rehash() 扩容方法

  1. 计算扩容后的数组长度,并于最大值进行校验。如果已经达到最大值,则直接退出。
  2. 创建新数组,计算新的扩容阈值。
  3. 倒序遍历旧 table 数组,计算新的 index 位置,放入新 table 对应位置。
@SuppressWarnings("unchecked")  
protected void rehash() {  
    int oldCapacity = table.length;  
    Entry<?,?>[] oldMap = table;  
    
    // 将原来的 table 数组长度乘以2再1。(二进制下左移一位就是x2)
    // overflow-conscious code  
    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;  
	// 倒序遍历旧 table 数组,计算新的 index 位置,放入新 table 对应位置。
    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;  
        }  
    }  
}

一个有意思的问题

对 Fail-Fast 快速失败机制的争吵

本文开头的链接文章中我们讲过 HashMap 的 Fail-Fast 机制,可以复习一下。
这个问题非常有意思,因为我看到网上关于这个问题的博客下经常有人吵架,吃瓜属性迫使我要一探究竟!
重点来了:

  1. HashMap 只有一个 Iterator 迭代器,它在遍历的时候判断了 expectedModCount != modCount 抛出 ConcurrentModificationException 异常,所以它是 Fail-Fast 的。
  2. HashTable 有两个迭代器,两个!一个也是 Iterator 迭代器,也是 Fail-Fast 的,另一个是 Enumerator 迭代器,这个迭代器在遍历的时候没有判断 expectedModCount != modCount ,也没有抛出 ConcurrentModificationException 异常,所以这个迭代器是 Fail-Safe 的。

所以,吵半天两人吵的不是一个方法吧?

HashMap 中的 Enumerator 迭代器
public boolean hasMoreElements() {  
    Entry<?,?> e = entry;  
    int i = index;  
    Entry<?,?>[] t = table;  
    /* Use locals for faster loop iteration */  
    while (e == null && i > 0) {  
        e = t[--i];  
    }  
    entry = e;  
    index = i;  
    return e != null;  
}  
  
@SuppressWarnings("unchecked")  
public T nextElement() {  
    Entry<?,?> et = entry;  
    int i = index;  
    Entry<?,?>[] t = table;  
    /* Use locals for faster loop iteration */  
    while (et == null && i > 0) {  
        et = t[--i];  
    }  
    entry = et;  
    index = i;  
    if (et != null) {  
        Entry<?,?> e = lastReturned = entry;  
        entry = e.next;  
        return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);  
    }  
    throw new NoSuchElementException("Hashtable Enumerator");  
}


HashMap 中的 Iterator 迭代器
@Override  
public synchronized void forEach(BiConsumer<? super K, ? super V> action) {  
    Objects.requireNonNull(action);     // explicit check required in case  
                                        // table is empty.    final int expectedModCount = modCount;  
  
    Entry<?, ?>[] tab = table;  
    for (Entry<?, ?> entry : tab) {  
        while (entry != null) {  
            action.accept((K)entry.key, (V)entry.value);  
            entry = entry.next;  
  
            if (expectedModCount != modCount) {  
                throw new ConcurrentModificationException();  
            }  
        }  
    }  
}

本地自测方法,别光看,跑一跑代码吧,Iterator 这个不仅会报错,还会死循环。
Enumerator 迭代器,通过 .keys() 方法获得
public static void main(String[] args) {  
    Hashtable<String, String> ht = new Hashtable<>();  
    ht.put("key1", "好");  
    ht.put("key2", "很好");  
    ht.put("key3", "非常好");  
    ht.put("key4", "好到爆炸");  
  
    Enumeration<String> keys = ht.keys();  
    while (keys.hasMoreElements()) {  
        ht.remove("key1");  
        String value = keys.nextElement();  
        System.out.println(value);  
    }  
}

Iterator 迭代器,通过 .keySet().iterator() 方法获得
public static void main(String[] args) {  
    Hashtable<String, String> ht = new Hashtable<>();  
    ht.put("key1", "好");  
    ht.put("key2", "很好");  
    ht.put("key3", "非常好");  
    ht.put("key4", "好到爆炸");  
  
    Iterator<String> iterator = ht.keySet().iterator();  
    while (iterator.hasNext()) {  
        ht.remove("key1");  
        String value = iterator.next();  
        System.out.println(value);  
    }  
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值