集合源码学习之路---hashMap(jdk1.8)

hashMap简单介绍

hashMap是面试中的高频考点,或许日常工作中我们只需把hashMap给new出来,调用put和get方法就完了。但是hashMap给我们提供了一个绝佳的范例,展示了编程中对数据结构和算法的应用,例如位运算、hash,数组,链表、红黑树等,学习hashMap绝对是有好处的。
废话不多说,要想学习hashMap,必先明白其数据结构。在java中,最基础的数据结构就两种,一种是数组,另外一个就是模拟指针(引用),一起来看下hashMap结构图:
集合源码学习之路---hashMap(jdk1.8)_第1张图片

类定义

从类定义上看,继承于AbstractMap,并实现Map接口,其实就是里面定义了一些常用方法比如size(),isEmpty(),containsKey(),get(),put()等等,Cloneable,Serializable 的作用在之前list章节已讲述过就不再重复了,整体来说类定义还是蛮简单的

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable 

源码分析

接下来会带领大家阅读源码,有些不重要的,会咔掉一部分。

//初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子,用来计算threshold
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转成树的阈值,当桶中链表长度大于8时转成树
static final int TREEIFY_THRESHOLD = 8;
//进行resize操作室,若桶中数量少于6则从树转成链表
static final int UNTREEIFY_THRESHOLD = 6;
//当桶中的bin树化的时候,最小hashtable容量,最少是TREEIFY_THRESHOLD 的4倍
static final int MIN_TREEIFY_CAPACITY = 64;
//在树化之前,桶中的单个bin都是node,实现了Entry接口
 static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;

        Node(int hash, K key, V value, Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

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

为什么hashMap容量总是2的幂次方?

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

hashMap中的hash算法,影响hashMap效率的重要因素之一就是hash算法的好坏。hash算法的好坏,我们可以简单的通过两个因素判断,1是否高效2是否均匀。
大家都知道key.hashCode调用的是key键值类型自带的哈希函数,返回int散列值。int值得位数有2的32次方,如果直接拿散列值作为下标访问hashMap主数组的话,只要hash算法比较均匀,一般是很难出现碰撞的。但是内存装不下这么大的数组,所以计算数组下标就采取了一种折中的办法,就是将hash()得到的散列值与数组长度做一个与操作。如下函数:

//或许大家会发现这个方法是jdk1.7,为什么不用1.8的呢?那是因为1.8里已经去掉这个函数,直接调用,为了讲解方便,我从1.7中找出此方法方便学习
static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

hashMap的长度必须是2的幂次方,最小为16.顺便说一下这样设计的好处。因为这样(length-1)正好是一个高位掩码,&运算会将高位置0,只保留低位数字。我们来举个例子,假设长度为16,16-1=15,15的2进制表示为
00000000 00000000 00000000 00001111.随意定义一个散列值做&运算,结果如所示:

    10101010 11110011 00111010 01011010
  & 00000000 00000000 00000000 00001111
  -------------------------------------
    00000000 00000000 00000000 00001010 

也就是说实际上只截取了最低的四位,也就是我们计算的索引结果。但是只取后几位的话,就算散列值分布再均匀,hash碰撞也会很严重,如果hashcode函数本身不好,分布上成等差数列的漏洞,使最后几个低位成规律性重复,这就无比蛋疼了。这时候hash()函数的价值就体现出来了

h=key.hashcode()    11111011 01011111 00011100 01011011
        h>>>16    ^ 00000000 00000000 11111011 01011111
                  -------------------------------------
                    11111011 01011111 11100111 00000100
                  & 00000000 00000000 00000000 00001111
                  -------------------------------------
                    00000000 00000000 00000000 00000100

(h = key.hashCode()) ^ (h >>> 16),16正好是32的一半,其目的是为了将自己的高半区和低半区做异或,混合高低位信息,以此来加大低位的随机性,而且混合后的低位存在部分高位的特征,算是变相的保留了高位信息。由此看来jdk1.8对于hash算法和计算索引值的设计就基本暴露在我们的眼前了,不得不佩服设计之巧妙。

//返回大于等于cap且距离最近的一个2的幂   
//例子:cap=2 return 4;  cap=9 return 16;
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;
    }
//hashMap数组,保留着链表的头结点或者红黑树的跟结点。
//当第一次使用的时候,会对其初始化,当需要扩容时,会调用resize方法。长度一定是2的幂
transient Node[] table;
//用来遍历的set集合,速度快于keySet
transient Set> entrySet;

transient int size;
//用来检测使用iterator期间结构是否发生变化,变化则触发fail-fast机制
transient int modCount;
//当容器内映射数量达到时,发生resize操作(threshold=capacity * load factor)
int threshold;
//加载因子,默认0.75
final float loadFactor;

get方法解析

public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode)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);
            }
        }
        return null;
    }

hash函数之前已经研究过了,直接锁定getNode()吧。通过hash函数算出hash值&上数组长度从而计算出索引值,然后遍历比较key,返回对应值。

put和resize方法解析

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[] tab; Node p; int n, i;
        //若table为空,则调用resize()进行初始化,并将长度赋值给n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据(n-1)&hash算出索引,得到结点p,若p为null,则生成一个新的结点插入。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //若p不为null,则将p和插入结点的key与其hash值进行比较,若相同将p的引用同时赋值给e
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //若不同,且结点p属于树节点,则调用putTreeVal()
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
            //若不同,则将p当做链表的头结点,循环比较,若为null则新增节点,且循环次数大于等于TREEIFY_THRESHOLD - 1则从链表结构转为树结构
                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;
                    }
                    //若链表中其中一结点的key与hash与插入结点一致,则跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果插入的key存在,则替换旧值并返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //若插入的key在map中不存在,则判断size>thresold
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    //初始化数组和扩容使用
    final Node[] resize() {
        Node[] 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; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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[] newTab = (Node[])new Node[newCap];
        table = newTab;
        //若原数组不为空,则对其中元素进行重新排列
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node 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)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node 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;
    }

entrySet和keySet

hashMap中遍历的方式是通过entrySet和keySet,为了保证其效率,建议用entrySet因为他的存储结构和hashMap一致。hashMap是如何维护entrySet的呢?通过阅读源码,发现在put的时候,并没有对entrySet进行维护,且源码中
entrySet方法只是new了个对象,那这个entrySet视图的数据从哪而来呢?

public Set> entrySet() {
        Set> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    final class EntrySet extends AbstractSet> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator> iterator() {
            return new EntryIterator();
        } 
    }
    
    final class EntryIterator extends HashIterator
        implements Iterator> {
        public final Map.Entry next() { return nextNode(); }
    }
    
    abstract class HashIterator {
        Node next;        // next entry to return
        Node current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        final Node nextNode() {
            Node[] t;
            Node e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    }

通过阅读EntrySet,我们发现其iterator() 调用了EntryIterator(),而在对其进行实例化的时候会对其父类HashIterator进行实例化,从HashIterator的构造方法和nextNode我们发现,其返回的视图就是作用于table的,所以无需重新开辟内存。

总结

本篇文章主要分析hashMap的存储结构,分析了hashMap为什么容量始终是2的幂,分析了其hash算法的好坏和影响其效率的因素,同时也了解到了在put和get时做了哪些操作和其中数据结构的变化。最后通过hashMap常见的遍历方式,得出entrySet是便利效率最高的,且hashMap维护entrySet的方式。通过学习,发现hashMap的设计非常优秀,但无奈能力有限,无法将其精妙之处全部剖析开来。
下节预告:分析一下并发下的hashMap有可能造成的闭环问题和concurrentHashMap

你可能感兴趣的:(集合源码学习之路---hashMap(jdk1.8))