java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > HashMap原理

java8中的HashMap原理详解

作者:feiyingHiei

这篇文章主要介绍了java8中的HashMap原理详解,HashMap是日常开发中非常常用的容器,HashMap实现了Map接口,底层的实现原理是哈希表,HashMap不是一个线程安全的容器,需要的朋友可以参考下

java8 HashMap实现原理

HashMap是日常开发中非常常用的容器,HashMap实现了Map接口,底层的实现原理是哈希表,HashMap不是一个线程安全的容器,jdk8对HashMap做了一些改进,作为开发人员需要对HashMap的原理有所了解,现在就通过源码来了解HashMap的实现原理。

首先看HashMap中的属性

    //Node数组
    transient Node<K,V>[] table;
     //当前哈希表中k-v对个数,实际就是node的个数
    transient int size;
    //修改次数
    transient int modCount;
    //元素阈值
    int threshold;
    //负载因子
    final float loadFactor;

这里的threshold = loadFactor * table.length,hash表如果想要保持比较好的性能,数组的长度通常要大于元素个数,默认的负载因子是0.75,用户可以自行修改,不过最好使用默认的负载因子。

Node是用来存储KV的节点,每次put(k,v)的时候就会包装成一个新的Node, Node定义

    static class Node<K,V> implements Map.Entry<K,V> {
        //hash值
        final int hash;
        final K key;
        V value;
        //hash & (capacity - 1) 相同的Node会形成一个链表
        Node<K,V> next;
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

put操作

写入操作是map中最常用的方法,这里看看hashmap的put方法代码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

这里先计算key的hash值,然后调用putVal()方法,其中hash方法是内部自带的一个算法,会对key的hashcode再做一次hash操作

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

pubVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; //如果数组为空,先初始化一下
        if ((p = tab[i = (n - 1) & hash]) == null) //如果对应的数组为空的话,那么就直接new一个node然后塞进去
            tab[i] = newNode(hash, key, value, null);
        else { //如果有值,说明发生了冲突,那么就先用拉链法来处理冲突
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p; //如果头结点的key和要插入的key相同,那么就说明找到了之前插入的节点
            else if (p instanceof TreeNode) //如果链表转成了红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { //如果之前没有put过这个节点,那么就new一个新的节点
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                        //另外要检查一下当前链表的长度,如果超过8那么就将链表转化成红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //如果找到了之前的节点,那么就跳出
                        break;
                    p = e;
                }
            }
            if (e != null) {
                V oldValue = e.value; 
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); //在当前类中NOOP
                return oldValue;
            }
        }
        ++modCount;
        //如果当前元素数量大于门限值,就要resize整个hash表,实际上就是把数组扩大一倍,然后将所有元素重新塞到新的hash表中
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict); //在该类中NOOP
        return null;
    }

在hashtable中默认的出现冲突的时候就会将冲突的元素形成一个链表,当链表长度大于8的时候就会将链表变成一个二叉树,这是java8中做出的改进,因为在使用hash表的时候在key特殊的情况下最坏的时候hash表会退化成一个链表,那么原有的O(1)的时间复杂度就变成了O(n),性能就会大打折扣,但是引用了红黑树之后那么在最好的情况下时间复杂度就变成了O(log(n))。

resize方法

final Node<K, V> [] resize() {
......
//去掉了一些代码,只关注最核心的node迁移
//resize会新建一个数组,数组的长度是原来数组长度的两倍
    for (int j = 0; j < oldCap; ++j) {//遍历原来的数组
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;
            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e; //如果没有形成链表的话,就直接塞到新的hash表中
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //红黑树操作??
            else { // preserve order
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;
                do {
                    next = e.next;
                    if ((e.hash & oldCap) == 0) { //如果hash值小于oldCap的时候,那么就还在原来那个数组的位置,就把这个节点放到low链表中
                        if (loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    else { //否则的话就是因为扩展数组长度,就把原来的节点放到high链表中
                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null);
                if (loTail != null) {
                    loTail.next = null;
                    newTab[j] = loHead; //low链表还放在原来的位置
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead; //high链表放到j+oldCap位置上
                }
            }
        }
    }
}

resize操作就是创建一个先的数组,然后把老的数组中的元素塞到新的数组中,注意java8中的hashMap中数组长度都是2的n次幂,2、4、、8、16….. 这样的好处就是可以通过与操作来替代求余操作。当数组扩大之后,那么每个元素所在的位置是可以预期的,就是要不就待在原来的位置,要不就是到j+oldCap位置上,举个栗子,如果原来数组长度为4,那么hash为3和7 的元素都会放在index为3的位置上,当数组长度变成8的时候,hash为3的元素还待在index为3的位置,hash为7的元素此时就要放到index为7的位置上。

resize操作是一个很重要的操作,resize会很消耗性能,因此在创建hashMap的时候最好先预估容量,防止重复创建拷贝。

另外hashmap也是非线程安全的,在多线程操作的时候可能会产生cpu100%的情况,主要的原因也是因为在多个线程resize的时候导致链表产生了环,这样下次get操作的时候就会容易进入死循环。

get方法()

get的实现比较简单

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) //如果节点不为空而且头结点与查找的key相同就返回
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);//从红黑树中查找
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null); //遍历链表查找key相同的node
        }
    }
    return null;
}

到此这篇关于java8中的HashMap原理详解的文章就介绍到这了,更多相关HashMap原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文