美文网首页
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