JDK源码之HashTable解析

HashTable和HashMap的对比

  1. 两者的继承体系有区别

    两者都实现了MapSerializableCloneable接口,但HashTable继承了Dictionary抽象类,HashTable继承了AbstractMap抽象类

  2. HashTable比HashMap多了两个方法

    HashTableHashMap多了**elments()方法和contions()**方法。

  3. 二者的键和值对null的支持不同

    HashTable中键和值都不允许为nullHashMap中只允许一个键为null一个或多个键对应的值为null

  4. HashTable是线程安全的,HashMap是线程不安全的

    HashTable线程安全的,但是效率会低一些HashMap不是线程安全的,但是效率会高一些

  5. 二者的初始容量和扩容机制不一样

    HashTable初始容量为11,扩容时扩容为原来容量的2倍加1,HashMap初始容量为16,扩容时扩容为原来容量的2倍

  6. 二者计算哈希值的方法不一样

    HashTable使用对象自身的hashcode()计算哈希值,HashMap使用位运算等一些方法计算哈希值

HashTable

1. 继承体系

在这里插入图片描述

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
  • 继承了Dictionary抽象类(与HashMap不同,HashMap继承了AbstractMap)
  • 实现了Map接口,具备了集合的基本框架,存储的元素类型是键值对
  • 实现了Cloneable接口,具备了克隆功能
  • 实现了Serializable接口,可以被序列化和反序列化

HashTable注意点:键和值都不允许为null(不同于HashMap)

2. 源码

属性
//底层数组
private transient Entry<?,?>[] table;
//集合中元素个数
private transient int count;
//阈值,当集合中元素数量达到阈值就要进行扩容,阈值 = 容量 * 负载因子
private int threshold;
//负载因子
private float loadFactor;
//集合底层修改次数,可以实现fail-fast机制,在迭代时如果有其他线程进行结构修改,会抛出异常
private transient int modCount = 0;
//序列版本号
private static final long serialVersionUID = 1421746759512286392L;
构造方法
//指定容量和负载因子
public Hashtable(int initialCapacity, float loadFactor) {
        //指定容量小于0,抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        //负载因子小于等于0或者负载因子非法,抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);
        //指定负载因子为0的话至少分配一个空间
        if (initialCapacity==0)
            initialCapacity = 1;
        //赋值负载因子
        this.loadFactor = loadFactor;
        //创建指定容量空间
        table = new Entry<?,?>[initialCapacity];
        //阈值取容量和负载因子的乘积与最大数组容量+1的最小值
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }
//指定容量
public Hashtable(int initialCapacity) {
        //默认负载因子为0.75
        this(initialCapacity, 0.75f);
    }
//无参构造方法
public Hashtable() {				//由此可见无参构造方法默认容量为11,默认负载因子为0.75
        //默认容量为11,默认负载因子为0.75
        this(11, 0.75f);
    }
//指定集合的构造方法
public Hashtable(Map<? extends K, ? extends V> t) {
        //取元素数量和11的最大值,负载因子为0.75
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }
成员方法
get(Object key)

由键获取对应的值

public synchronized V get(Object key) {		
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();				//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;							//没找到返回null
    }

步骤

  1. 获取锁
  2. 获得哈希值和对应索引位置
  3. 找到返回对应的值
  4. 没找到返回null
put(K key, V value)

插入键值对的键和值

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;
            }
        }
		//没有则在改索引处插入,并返回null
        addEntry(hash, key, value, index);
        return null;
    }

步骤

  1. 获取synchronized锁
  2. 插入的键的值不允许为空,为空则抛出异常
  3. 计算key的哈希值和对应的索引位置
  4. 在索引处的链表寻找是否有相等的键,有相等的键覆盖值并返回旧值
  5. 没有相等的键就插入
addEntry(int hash, K key, V value, int index)

哈希值应存储的位置存储键值对

private void addEntry(int hash, K key, V value, int index) {
        modCount++;     //集合修改次数加1

        Entry<?,?> tab[] = table;       //集合底层数组
        if (count >= threshold) {       //集合中元素数量达到阈值
            // Rehash the table if the threshold is exceeded
            rehash();           //扩容

            tab = table;
            hash = key.hashCode();  //计算新的hash值
            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. 看是否需要扩容
  2. 使用头插法将键值对添加到链表的首部
void rehash()

扩容

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++;
        //计算阈值,阈值为 新容量 * 负载因子 和最大数组容量加1的最小值
        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;
            }
        }
    }

步骤

  1. 将新容量扩容为原来容量的2倍加1
  2. 判断新容量是否超过了最大数组容量,超过则新容量赋值为最大数组容量
  3. 创建新数组,计算阈值
  4. 将旧数组中得键值对对应的新数组中
remove(Object key)

由键值移除对应的键值对

public synchronized V remove(Object key) {
        //集合底层数组
        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(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
             //寻找是否有相等的键,相等则删除
            if ((e.hash == hash) && e.key.equals(key)) {
                //找到集合底层数组修改次数加1
                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;     //没有返回null
    }

步骤

  1. 先获取synchronized锁
  2. 获取对应的索引和哈希值
  3. 在索引处的单链表遍历看是否可以找到相等的键,可以找到则删除对应的键值对,并返回旧值,没有找到则返回null
hashCode()

返回哈希值

public synchronized int hashCode() {
        int h = 0;
        if (count == 0 || loadFactor < 0)
            return h;  // Returns zero

        loadFactor = -loadFactor;  // Mark hashCode computation in progress
        Entry<?,?>[] tab = table;
        for (Entry<?,?> entry : tab) {
            while (entry != null) {
                h += entry.hashCode();
                entry = entry.next;
            }
        }

        loadFactor = -loadFactor;  // Mark hashCode computation complete

        return h;
    }

HashTable的方法是直接使用对象的hashcode()。hashcode()是根据对象的地址或者字符串或者数字计算出来的哈希值,然后再用除留取余法获得最终的位置(使用除法比较耗时)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值