HashMap源码分析

基于jdk1.8

Map接口下常用的集合有HashMap、HashTable、LinkedHashMap等。相比HashMap,Hashtable是线程安全的,Hashtable中的方法都用了synchronized进行了同步,下面开始源码解读。

继承结构

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable 

实现了Map接口,以及Cloneable和Serializable接口,继承了AbstractMap类。

HashMap与Hashtable的第一个区别在于此,HashMap继承了AbstractMap,而Hashtable继承的是Dictionary抽象类,后面介绍到。

看过源码的人可能都有这样一个疑问:AbstractMap也实现了Map接口,为什么HashMap既继承AbstractMap抽象类还需要实现Map接口吗???

从功能上来说:HashMap实现Map是没有任何作用的。

从结构上来说:由于我们一般是面对接口编程,为了维护结构清晰和完整,是需要实现Map接口的。

而HashMap继承AbstractMap的作用为:AbstractMap 提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作。

构造函数

    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);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }


    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

 可以看到有四个构造方法。

第一个构造函数:public HashMap(int initialCapacity, float loadFactor)方法用来生成HashMap对象,其它的构造函数都是调用此构造函数来实现的。
参数的说明:
        initialCapacity:分配数组的大小,默认大小为16,且只能是2的幂次方
        loadFactor:加载因子,作用为:当数组中存储的数据大于了分配空间的总长度*loadFactor之后就进行扩容

 该方法涉及到了tableSizeFor方法,源码如下:

    //tableSizeFor的作用就是,如果传入A,当A大于0,小于定义的最大容量时,
    //如果A是2次幂则返回A,否则将A转化为一个比A大且差距最小的2次幂。
    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;
    }

将我们输入的任意值转化为大于等于此值的2的幂次方。而对比去看,Hashtable的构造函数中对数组table进行了空间的分配,即在构造函数中直接使用了table = new Entry[initialCapacity]。而在HashMap中却不是在构造函数中分配的。如果不在构造函数中进行数组table空间的配,则一定是在第一次使用put函数存储数据时分配,追踪了下源码,发现HashMap确实是这样实现的。不过下面先看一下HashMap的属性

属性

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

    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认装载因子0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //默认阈值
    static final int TREEIFY_THRESHOLD = 8;
    //如果链表的长度小于6,退化为红黑树
    static final int UNTREEIFY_THRESHOLD = 6;
    //HashMap的长度大于64的时候,转为红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;

可以看到都是定义的几个常量。

而且还定义了一个静态内部类,源码如下:

    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 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;
        }

        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;
        }
    }

这段源码定义了Node的结构,包含数值项,对应的key以及hash值,还有一个next指针。

    //初始化并可以进行动态扩容的table
    transient Node[] table;
    //为了调用keys和values方法而设置的属性
    transient Set> entrySet;
    //hashMap中k,v的个数
    transient int size;
    //修改的次数
    transient int modCount;
    //阈值
    int threshold;
    //装载因子
    final float loadFactor;

成员方法

上面提到了put方法,那么我们首先来看put方法。

put(K key, V value)

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

可以看到调用了putVal方法。

putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

此方法的思想为:首先根据key得到hashcode,根据hashcode得到要存储的位置i=hash&(n-1),其中n为数组的长度(只有n为2的幂次方时,这句话才与hash%n等价,这就解释了为什么了HashMap的容量必须为2的幂次方)。

得到存储位置i之后,检查此位置是否已经有元素,如果没有,则直接存储在该位置即可,如果有,则在位置的所有节点中遍历是否含有该key,如果已经有了该key,则更新其value即可,如果没有该key,则在该链表的末尾加入该新节点即可。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //如果HashMap为空
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;////重新开辟一个Node的数组
        //如果得到位置i处没有元素,那么直接创建新的Node
        //根据key的hash值找到要存储的位置,
        //如果该位置还没有存储元素,则直接在该位置保存值即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //检查在位置的链表中是否有了该key,
            //在下面的代码中,是先检查头结点是否为该key,如果不等于,则在剩余的节点中寻找
            //有元素的话,进行下面处理
            Node e; K k;
            //如果对应p的hash值相等并且key值相等
            //如果这个位置的old节点与new节点的key完全相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //p是TreeNode类型
            //如果p已经是树节点的一个实例,既这里已经是树了
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            //在剩余的节点中寻找key的位置
            //将节点(key,value)加到链表的末尾
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//p的下一个节点是null
                        p.next = newNode(hash, key, value, null);//指向新的节点
                        //下面的if判断,如果链表长度大于等于8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);///将链表转为红黑树
                        break;
                    }
                    //如果遍历过程中链表中的元素与新添加的元素完全相同,则跳出循环
                    //简单解释来看,也就是说,hashmap中已经存在了这个key对应这个value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //跳出循环
                        break;
                    //继续遍历链表下面的元素
                    //等同于p = p.next;
                    p = e;
                }
            }
            //如果e为空,则说明是添加的新节点,
            //如果e不为空,则说明该key已经存在,只需要更新value
            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;
    }

上面使用了resize方法进行扩容。下面是resize方法的源码。在HashMap所有的构造函数中,都没有对数组table分配存储空间。而是将这一步放入到了在put方法中进行table检测,如果为空,则调用resize方法进行扩容(或者说是为了给其开辟空间)。

resize()方法

此方法实现的思想为:

处理了一下两种情况

1)原table为null的情况,如果为空,则开辟默认大小的空间

2)原table不为空的情况,则开辟原来空间的2倍。由于可能oldCap*2会大于最大容量,因此也对其这种溢出情况进行了处理。

分配空间之后,然后将原数组中的元素拷贝到新数组中即可。

    final Node[] resize() {
        //保存现有的数组内容
        Node[] oldTab = table;
        //获取原来数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //保存原来的扩充后的数组的长度
        int oldThr = threshold;
        //用来保存新长度,新扩充的大小
        int newCap, newThr = 0;
        //HashMap有值的情况
        //扩容肯定执行这个分支
        if (oldCap > 0) {
            //是否大于最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //扩大为原来的两倍,并且小于最大值
            //DEFAULT_INITIAL_CAPACITY  默认16
            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;
        //HashMap没有初始化的时候
        //不执行
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;//16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75
        }
        //不执行
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将新的临界值赋值赋值给threshold
        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)//直接确定元素位置
                        //扩容前的元素地址为 (oldCap - 1) & e.hash ,
                        //所以这里的新的地址只有两种可能,一是地址不变,
                        //二是变为 老位置+oldCap
                        newTab[e.hash & (newCap - 1)] = e;//该链表上只有一个元素的情况
                    else if (e instanceof TreeNode)//红黑树情况
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //遍历链表上的元素,重新定位
                        //这里如果判断成立,那么该元素的地址在新的数组中就不会改变。因为oldCap                          
                        //的最高位的1,在e.hash对应的位上为0,
                        //所以扩容后得到的地址是一样的,位置不会改变 ,
                        //在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
                        //如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,
                        //也就是oldCap最高位的1,在e.hash对应的位置上也为1,
                        //所以扩容后的地址改变了,在后面的代码中会放到hiHead中,
                        //最后赋值给newTab[j + oldCap]
                        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;
    }

剩下部分明天继续补充。

下面是具体的一个例子:

举个栗子来说一下上面的两种情况:
设:oldCap=16 二进制为:0001 0000
	oldCap-1=15 二进制为:0000 1111
	e1.hash=10 二进制为:0000 1010
	e2.hash=26 二进制为:0101 1010
e1在扩容前的位置为:e1.hash & oldCap-1  结果为:0000 1010 
e2在扩容前的位置为:e2.hash & oldCap-1  结果为:0000 1010 
结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。

现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1
那么在扩容后应该也这么计算呀,扩容后的容量为oldCap*2=32 0010 0000 newCap=32,新的计算
方式应该为
e1.hash & newCap-1 
即:0000 1010 & 0001 1111 
结果为0000 1010与扩容前的位置完全一样。
e2.hash & newCap-1 
即:0101 1010 & 0001 1111 
结果为0001 1010,为扩容前位置+oldCap。
而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位
是0,还是1。如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。
再来分析下loTail loHead这两个的执行过程(假设(e.hash & oldCap) == 0成立):
第一次执行:
e指向oldTab[j]所指向的node对象,即e指向该位置上链表的第一个元素
loTail为空,所以loHead指向与e相同的node对象,然后loTail也指向了同一个node对象。
最后,在判断条件e指向next,就是指向oldTab链表中的第二个元素
第二次执行:
lotail不为null,所以lotail.next指向e,这里其实是lotail指向的node对象的next指向e,
也可以说是,loHead的next指向了e,就是指向了oldTab链表中第二个元素。此时loHead指向        
的node变成了一个长度为2的链表。然后lotail=e也就是指向了链表中第二个元素的地址。
第三次执行:
与第二次执行类似,loHead上的链表长度变为3,又增加了一个node,loTail指向新增的node
   ......
hiTail与hiHead的执行过程与以上相同,这里就不再做解释了。
由此可以看出,loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍            
历完链表。   这是(e.hash & oldCap) == 0成立的时候。
(e.hash & oldCap) == 0不成立的情况也相同,其实就是把oldCap遍历成两个新的链表,
通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与 
newTab[j+oldCap]上面去    

以上就是整个resize的过程。

get(Object key)

    public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

这里调用了getNode方法,源码如下:

    final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
        //如果hashMap没有元素直接返回,如果有元素,下面成立
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果根据hash得到的位置,只有一个元素,直接返回
            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;
    }

整体思想来看就是,首先根据key得到hashcode,然后根据hashcode得到该key在数组table的存储位置,接着在该位置寻找key值和hashcode值一致的节点即可,如果没有找到,返回null。

putMapEntries(Map m, boolean evict)

当利用一个构造方法传入一个map创建一个HashMap的时候会发现调用该方法。

     final void putMapEntries(Map m, boolean evict) {
        //获取传入map的大小
        int s = m.size();
        //如果有元素
        if (s > 0) {
            //真个HashMap为空,直接构造一个新的HashMap
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //如果大于阈值,那么进行resize
            else if (s > threshold)
                resize();
            //否则遍历传入的map,将对应的元素放入新创建的hashMap中,看到调用了putVal方法
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

containsKey(Object key)

可以看到该方法也是通过getNode实现的。 

    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

remove(Object key)

     public V remove(Object key) {
        Node e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

看到该方法调用了removeNode方法。removeNode方法的思想为:先根据key的hash值找到table的位置i,然后在该位置下的链表寻找key和hash均满足条件的节点。删除节点和链表删除节点方法一致。

removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)

    final Node removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node[] tab; Node p; int n, index;
        //判断hashmap此时是否有元素,有元素成立
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //定义几个使用的变量
            Node node = null, e; K k; V v;
            //p此时代表tab[index = (n - 1) & hash])--头结点
            //如果p的位置此时就是要删除的位置
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {//不是头结点
                if (p instanceof TreeNode)//红黑树情况
                    node = ((TreeNode)p).getTreeNode(hash, key);
                else {//循环遍历寻找对应的元素,找到了对应的元素
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //如果没有找到返回null
            //找到了条件成立
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)//红黑树情况处理
                    ((TreeNode)node).removeTreeNode(this, tab, movable);
                else if (node == p)//如果第一个节点就是我们要找的节点
                    tab[index] = node.next;
                else//其他情况的删除元素处理
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

 comparableClassFor(Object x)

    static Class comparableClassFor(Object x) {
        if (x instanceof Comparable) {  // 判断是否实现了Comparable接口
            Class c; Type[] ts, as; Type t; ParameterizedType p;
            if ((c = x.getClass()) == String.class) 
                return c;   // 如果是String类型,直接返回String.class
            if ((ts = c.getGenericInterfaces()) != null) {  // 判断是否有直接实现的接口
                for (int i = 0; i < ts.length; ++i) {   // 遍历直接实现的接口
                    // 该接口实现了泛型
                    if (((t = ts[i]) instanceof ParameterizedType) &&   
                        //获取接口不带参数部分的类型对象
                        //该类型是Comparable 
                        ((p = (ParameterizedType)t).getRawType() ==  Comparable.class) &&
                        (as = p.getActualTypeArguments()) != null &&// 获取泛型参数数组
                        //只有一个泛型参数,且该实现类型是该类型本身
                        as.length == 1 && as[0] == c)
                        return c;   // 返回该类型
                }
            }
        }
        return null;
    }

getOrDefault()方法是jdk1.8新增的方法。当Map集合中有这个key时,就使用这个key值,如果没有就使用默认值defaultValue。

其他新增方法不再一一介绍。

其他方法如size(),isEmpty(),clear(),keySet(),values()等不在一一粘贴。

你可能感兴趣的:(JDK源码分析测试)