java集合系列八:HashTable源码解析

你应该先阅读java集合系列一:前传

介绍

  • 实现Map接口(这意味着以键/值对的形式存储数据)
  • 哈希表实现
  • 不支持null值或null键
  • Hashtable是同步的
  • 无序的
  • 具有fail-fast机制
  • 默认初始容量11(尽量维持奇数)
  • 默认加载因子.75f

继承关系 继承类介绍

Dictionary:虽然HashTable继承自Dictionary,但是此类已过时
Map:实现Map接口(这意味着以键/值对的形式存储数据)

变量解析(所有代码都不是完整的源码,而是精简过后的源码!结合源码食用更佳)

	//存放bucket的数组
    private transient Entry<?, ?>[] table;
    //count表示table的实际元素数量
    private transient int count;
    扩容临界值   table的实际元素数量超过此值执行扩容
    private int threshold;
    //默认加载因子
    private float loadFactor;
    //结构被修改的次数
    private transient int modCount = 0;
    //table的最大容量大小
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

源码解析 常用方法解析

put(K key, V value)

作用:将指定值与指定键关联,如果此映射中已经包含键的映射,则替换旧值

  //同步方法
    public synchronized V put(K key, V value) {
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        //计算index
        //hash & 0x7FFFFFFF可以将首位固定为0(0x7FFFFFFF = 0111 1111 1111 1111 1111 1111 1111 1111),结果就为整数
        //length为奇数取模运算的结果更加均匀
        int index = (hash & 0x7FFFFFFF) % tab.length;
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        //该index位置的节点不为Null 则遍历该链表 如果有节点满足key与hash的条件 则直接替换value
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        //该index位置的节点为null,则不存在冲突 执行添加
        addEntry(hash, key, value, index);

  //添加到链表首部
  private void addEntry(int hash, K key, V value, int index) {
    //结构被修改
    modCount++;
    Entry<?,?> tab[] = table;
    //判断实际容量是否超过扩容阈值 超过即需要扩容
    if (count >= threshold) {
        // 扩容
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }
    //将当前节点添加到链表的首部
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

//扩容
protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;
    //原容量*2+1
    int newCapacity = (oldCapacity << 1) + 1;
    //创建新的数组
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    modCount++;
    //计算扩容阈值 = 容量 * 加载因子
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
     //遍历所有节点 重新计算index位置 存入新数组
    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;
        }
    }
}

有点长总结一下put流程:

  • index的计算公式是(hash & 0x7FFFFFFF) % tab.length
  • key存在直接替换value
  • 判断是否需要扩容,新容量为原容量的两倍+1(尽量得到奇数)
  • 将新的节点添加到index对应的位置,如果该位置已经有节点存在,则新节点作为链表头部

remove(Object key)

作用:删除key及其对应的值

 //同步方法
public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //根据计算的index 获取对应位置的节点
    Entry<K,V> e = (Entry<K,V>)tab[index];
    //该节点可能是由多个节点组成的链表 这个循环是为了遍历该链表的所有节点 取出与key对应的value返回
    //且需要维护删除后链表的新顺序
    for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            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;
}

replace(K key, V value)

作用:替换指定键映射的值,如果键存在!相当于map.put(key, value)

 //同步方法 
 public synchronized V replace(K key, V value) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    //和remove类似 它也是遍历index位置链表的所有节点 找到key对应的节点后替换value
    //和remove不同的是它不需要维护新的链表顺序 故而更为简单一些
    for (; e != null; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }
    return null;
}

get(Object key)

作用:返回指定键映射到的值

//同步方法
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //和replace类似 这个就更简单了 它也是遍历index位置链表的所有节点 找到key对应的节点后直接返回value
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

扩展知识点

  • HashTable和HashMap的区别(如果这两个源码你都看过了,你应该试着写出他们之间的区别,写完参看我下面的写法)
    • HashMap默认初始容量为16,每次扩容为原容量2倍
    • HashTable默认初始容量为11,每次扩容为原容量2倍+1
    • HashTable尽量使用奇数作为容量,这样取模运算的结果会更加均匀,也就提高了遍历的效率
    • HashMap使用2的幂作为容量,使用位运算来得到index,效率要高于HashTable的取模运算
    • HashMap使用位运算取代HashTable的取模运算引入了新的问题,哈希分配不均匀,所以HashMap改进这一问题,加入扰动计算
    • HashTable是线程安全的
    • 当哈希冲突时,HashTable是将节点添加到链表首部,而HashMap是将节点添加到链表尾部
    • 数据结构不同,HashMap使用数组+链表+红黑树,HashTable使用数组+链表,当链表长度超过8之后HashMap的迭代效率高于HashTable(已树形化)
    • 继承关系不同,HashMap继承自AbstractMap,而HashTable继承自Dictionary
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值