HashTable源码分析

一、前言

前面几篇介绍了List相关的几个类。本篇开始分析Map相关的集合常用类的源码,OK,从HashTable开始分析。我们知道HashTable是线程安全的,但是其实现实中我们使用它的概率却比线程不安全的HashMap要低,为什么呢?

二、源码分析

内部数据结构

// 哈希表,内部使用单链表解决冲突,容量不足也会自动增长,transient保证不会被序列化
private transient Entry<?,?>[] table;
// 当前容量大小
private transient int count;
// HashTable扩容的阈值,它为int (table.length * loadFactor)
private int threshold;
// 负载因子
private float loadFactor;
// 注意,以上几个参数在HashMap中也是类似的,务必需要理解

// HashTable的数据结果,在HashMap中已经是Node了
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;
    }

    // ...

    public V setValue(V value) {
        if (value == null)
            throw new NullPointerException();

        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
           (value==null ? e.getValue()==null : value.equals(e.getValue()));
    }

    public int hashCode() {
        return hash ^ Objects.hashCode(value);
    }

    public String toString() {
        return key.toString()+"="+value.toString();
    }
}

其实从这里就可以看出HashTable的整个数据结构了:数组 + 链表。下面就通过实践来验证下。

还是通过一个简单的小Demo,通过debug来分析源码。


public class Demo05 {

    public static void main(String[] args) {
        Map<Integer, String> table = new Hashtable<Integer, String>() ;
        table.put(1, "java");
    }
}

// 默认构造函数
public Hashtable() {
    // 初始化容量大小为11,初始化负载因子0.75
    this(11, 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);

    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    // 在初始化table数组
    table = new Entry<?,?>[initialCapacity];
    // 默认为 11 * 0.75 = 8 
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); 
}

从上面可以看出,HashTable在构造函数主要完成了两件事

  1. 初始化table数组
  2. 计算threshold阈值
    ok,我们继续查看插入数据方法
// 线程安全但同时效率不高
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    // 这里可以看出HashTable的value不可以为null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    // 调用key的hashCode()说明HastTable的key不可以为null
    int hash = key.hashCode();
    // 高位取余
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    // 从哈希表中获取对应index位置的entry
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    // 从该entry中遍历其链表,如果有hash相同或者key相等的则覆盖返回旧值
    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;
}

从这个方法我们可以看出,它使用了synchronized锁,如果看其它的方法,其实HashTable的每个方法都条件了synchronized,它保证了线程安全。但是我们知道这样实现肯定浪费性能,尤其是在并发情况下

这个方法主要完成了如下几件事情

  1. 判断value是否为null,即HashTable不允许value为null
  2. 计算key的hashcode,也可以看出不允许key为null,否则也会报空指针异常
  3. 通过hashcode计算应该插入的角标值index
  4. 遍历当前table数组,查看是否有hash值相等并且key相等的entry,如果有则覆盖,如果没有调用addEntry()方法

继续跟踪addEntry方法

// 添加一个entry
private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    // 如果当前table的容量超过阈(yu)值
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();
        // 重新复制扩容后的table、hash、index
        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++;
}

此方法主要完成:

  1. 判断当前table容量是否达到阈值,如果达到阈值则进行扩容
  2. 如果没有达到阈值,则直接new Entry()并添加到Entry中

因为我们第一次添加,因此肯定不会进入到扩容。但是我们还是看一下扩容的实现吧。

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

    // overflow-conscious code
    // 当前容量*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;
    // 将old里的entry重新计算hash定位到new
    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的扩容步骤:

  1. 计算新数组的容量:(old << 1 ) + 1
  2. 根据新容量创建新数组大小,重新计算阈值
  3. 将old数组里面的元素重新计算新的index并赋值

三、总结

本篇主要介绍了HashMap整体数据结构,数据扩容机制几点。只是介绍了put()方法,因为其它的像get()、remove()方法比较简单,就没有分析了。可能还有我没有注意的难点,欢迎留言~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值