美文网首页
HashMap实现原理

HashMap实现原理

作者: Bill_Lin | 来源:发表于2019-03-16 10:49 被阅读0次

基本构造

内部包含了一个 Entry 类型的数组 Node,存放四个字段,hashCode, key, value, Entry next。每个位置当做一个桶,存放了一个链表,链表存放hash值相同的Entry。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。

解决冲突方法

常用的是链地址法:hashmap默认大小为16,通过除留余数法得到桶下标,以头插法方式进行插入到链表中

  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

开放地址法:发生冲突时,寻找下一个空的散列地址

再哈希法:用其他哈希函数计算地址

put操作

put操作,如果已经存在键为key的键值对,则进行更新并返回oldValue值

        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }

取模

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算,即h & (length - 1)

扩容

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)

hashmao扩容的时候capacity也是按2的次方来扩容;同时,扩容含有临界值threshold和装载因子loadFactor(默认0.75),键值对数量size超过threshold则需要进行扩容(rehashing),扩为原来的两倍

扩容时需要将oldTable所有键值对重新经过哈希,放到newTable中,过程比较费时

在线程安全方面,多线程竞争map的扩容时,可能会导致ABA的死循环问题

未扩容前,链表太长如何解决

从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树,同时做删除操作后链表长度小于6则将红黑树转换为链表

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;

转换过程

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

与HashTable对比

  • hashTable用sy同步,保证线程安全
  • 不允许null-key,null-value
  • ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好
  • HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常

关于fail-fast迭代器

Fail-fast和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。

为什么使用String,Integer这样的wrapper类适合作为键?

因为String是不可变的,也重写了equals和hashcode方法,防止键的改变,Integer同理

相关文章

网友评论

      本文标题:HashMap实现原理

      本文链接:https://www.haomeiwen.com/subject/qvmjmqtx.html