java深入学习Hashmap源码(JDK8)

关于JDK1.6、1.7、1.8三个版本,HaspMap的实现是有区别的,特别是1.8,对hashmap的结构进行了较大的变化。

1.6整体采用的是位桶+链表的方式,而1.8使用的是位桶+链表+红黑树实现,链表的阈值是8,超过后就由链表变成红黑树,大大增加了查询的效率。





1 涉及到的数据结构:处理hash冲突的链表和红黑树以及位桶

    //Node是单向链表,它实现了Map.Entry接口
    static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;
        //构造函数Hash值 键 值 下一个节点
        Node(int hash, K key, V value, Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + = + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry e = (Map.Entry)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

    //红黑树
    static final class TreeNode extends LinkedHashMap.Entry {
        TreeNode parent;  // 父节点
        TreeNode left;	//左子树
        TreeNode right;//右子树
        TreeNode prev;    // needed to unlink next upon deletion
        boolean red;	//颜色属性
        TreeNode(int hash, K key, V val, Node next) {
            super(hash, key, val, next);
        }

        //返回当前节点的根节点
        final TreeNode root() {
            for (TreeNode r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
    }

    transient Node[] table;//存储(位桶)的数组


以上3个数据结构,可以大致联想到HashMap的实现。首先有一个每个元素都是链表的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,所以说数组存放的是链表。而当链表长度超过8时,链表就转换为红黑树。


2.构造函数

 
  public HashMap(int initialCapacity, float loadFactor) {
        // 初始值为负
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 初始值大于最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 负载因子为负数或者空
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        // 新的扩容值
        this.threshold = tableSizeFor(initialCapacity);
    }


Map中,threshold = initialCapacity * loadFactor;所以在这里面,loadFactor越小,占用的空间就越多,查询检索的效率就越高;反之loadFactor越大,占用空间就小,而查询检索效率就低。由于haspMap的定义就是空间换时间,loadFactor负载因子值设置非常重要。

默认情况下,loadFactor为0.75,threshold 为12;如果loadFactor为1,threshold 就是16,为什么说loadFactor越大,空间占用小?比如一个为15的Map放入,那么在loadFactor在0.75时,是需要扩容的,threshold 就为24,此时初始空间为32;而为1的情况下不需要扩容
,所以占用空间就小。扩容后,位桶table也会增大,为旧的两倍,对比不扩容,位桶上hash冲突会减少很多,检索查询效率自然会高些。


3、扩容机制

构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length),就会进行扩容,由于扩容存在数组复制,扩容是比较耗费时间的。代码

 
  final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 旧的容量已经达到极限
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 旧的容量翻倍后没有到极限值,且大于等于默认值
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; //扩容值翻倍
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // 未设定使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) { //新建数组的情况下
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

        //数组转到到新的数组中,分红黑树和链表讨论
        table = newTab;
        
        if (oldTab != null) {
            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;
                    else if (e instanceof TreeNode)  // 若为红黑树,直接分离
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // 链表时,需要复制新的
                        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) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }


4、put/get时Node[] table位置

由于都是通过hash确定代码,

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

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

    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; //table为空的时候,n为table的长度
        if ((p = tab[i = (n - 1) & hash]) == null) // (n - 1) & hash 与1.6中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。
            tab[i] = newNode(hash, key, value, null); 
        else {
            // 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//相同则覆盖之
            else if (p instanceof TreeNode)
            // 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            // 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

整体过程:
1、判断键值对数组tab[]是否为空或为null,否则resize();
2、根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
3、判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理。
4、普通中关键代码 (n - 1) & hash; hash先由key值通过hash(key)获取,再通过 hash &(n即为length-1)得到所在数组位置。一般对于哈希表的散列常用的方法有直接定址法,除留余数法等,既要便于计算,又能减少冲突,但是取模中的除法运算效率很低,所以HashMap则通过hash &(length-1)。

为什么要减1?如图



一是保证使用 和运算时能够最大的在位桶上均分hash值,减少hash冲突,上图在未-1情况下,都只有一种结果;数组的length永远是2的N次方,那么length-1最后转成2进制,最后一位都是1,& 操作就可能出现0 或 1,如果不减1,那么2进制最后一位永远都是0,& 的运算结果只有0,明显增加了hash的冲突;
二是保证了结果都在length的长度内,& 操作后,结果永远都是 <= length 或者length -1,但在极端情况下出现 Node[length],就会出现问题,而Node[length -1]不会。

在HashMap实现中还可以看到如下代码取代了以前版本JDK1.6的while循环来保证哈希表的容量一直是2的整数倍数,用移位操作取代了循环移位。


    //这段代码保证HashMap的容量总是2的n次方
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }



参考:

http://www.2cto.com/kf/201505/401433.html
http://www.cnblogs.com/hfczgo/p/4033283.html?utm_source=tuicool&utm_medium=referral

猜你喜欢

转载自dongensi.iteye.com/blog/2318815