集合源码学习(七):HashMap(Java8)

来源:互联网 发布:淘宝下单后商品下架了 编辑:程序博客网 时间:2024/06/16 13:43

Java8中,新加了很多新特性,特别是集合,分割迭代器,Stream,Functional Interface等等,Java8中的HashMap也和以往的实现略有不同。
这些天看了好久的HashMap,理清了HashMap的结构以及实现原理,听我慢慢分析。

HashMap是什么?

/** * 基于Map接口实现,允许null值和null键。  * HashMap和HashTable很相似,只是HashTable是同步的,以及不能为null的键 * HashMap有两个重要参数,capacity和load factor 默认的load factor大小为0.75 * iterator是fail-fast的。 *  */public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>,        Cloneable, Serializable {

如上,基本的特性在代码里面注释了,HashMap实现了Map接口,是一个基于散列表的Map类,Map接口的特性就是存储键值对。散列表是一种存储结构,它可以通过散列函数直接访问到目标数据值,所以在定位下标方面可认为为o(1)。

HashMap重要的字段

/**     * Hash的默认大小     */    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16    /**     * HashMap最大存储容量     */    static final int MAXIMUM_CAPACITY = 1 << 30;    /**     * 增长因子,意思就是当table已经用到table.length*0.75时,就需要扩容     */    static final float DEFAULT_LOAD_FACTOR = 0.75f;    /**     * 由链表存储转变为由树存储的门限,最少是8     */    static final int TREEIFY_THRESHOLD = 8;    /**     * 由树存储节点转化为树的节点,默认是6,即从8到6时,重新转化为链表存储     */    static final int UNTREEIFY_THRESHOLD = 6;    /**     * 当由链表转为树时候,此时Hash表的最小容量。 也就是如果没有到64的话,就会进行resize的扩容操作。     * 这个值最小要是TREEIFY_THRESHOLD的4倍。     */    static final int MIN_TREEIFY_CAPACITY = 64;

上述代码中解释了HashMap中重要字段的意思,相信大家一看就会有大概理解了。

由于在Java8的实现中,当经过hash函数计算得出的下标地址冲突到一定范围时,就会 把冲突的数据用链表的形式连起来,而当用链表数据大于一定范围时,就会将链表转化为红黑树存储。
关于链表,Java中典型应用就是LinkedList,可以看:LinkedList
而红黑树,Java中典型应用是TreeMap, 可以看:TreeMap

HashMap结构概括

首先HashMap会有一个基准数组table:

/**     * 存储数据的table集合,长度一定为2的倍数     */    transient Node<K, V>[] table;

第一步,table是一个数组,所以会有下标,HashMap首先会根据传入每个节点的(key,value)中的key,算出应该放到哪一个下标的数组中。
第二步,如果此下标数组为null,那么就直接放入,不为null,就走到第三步。
第三步,如果不为null,就说明冲突了,检查key的equals方法,看是否和原节点的key相同,相同就直接替换,否则进入第四步。
第四步,很明显冲突了,而且是不相等的冲突,这是检查是否需要将此下标的存储结构换为红黑树,不需要就是链表直接在末尾插入节点,否则进入第五步。
第五步,原有的链表结构不足以支撑存储了,所以换为红黑树存储了,此时就是往红黑树中插入该节点。
上述步骤省略了链表与红黑树之间转换。
整个存储结构图如下(没有放入红黑树存储结构)(省略了value值)
这里写图片描述

HashMap的存储节点

首先看HashMap的Node节点代码,这就是table数组所使用的结构。如果冲突的是链表存储,则直接是这种结构存储。

    static class Node<K, V> implements Map.Entry<K, V> {        final int hash;        final K key;        V value;        // 可能要连接下面的链表,所以会有个next        Node<K, V> next;        ...        省略

再看红黑二叉树存储的结构:

    static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {        TreeNode<K, V> parent; // red-black tree links,红黑树,保证是一棵平衡二叉树        TreeNode<K, V> left;   //左子树        TreeNode<K, V> right;  //右子树        TreeNode<K, V> prev; // 指向下一个节点,类似于线索二叉树, needed to unlink next upon                                // deletion,删除时记得置null        boolean red;        //红黑特性    ...    省略

这是当不用链表表示冲突值时候,用红黑树表示时候的节点。由上可知,TreeNode继承自LinkedHashMap.Entry,
而它的结构如下:

static class Entry<K,V> extends HashMap.Node<K,V>

所以,其实TreeNode是Node的一个子类,所以table中也是可以存放TreeNode的。

hash值的计算方法

这里首先介绍hash值的计算方法,这也是一门有学问有艺术性的东西。
在HashMap中要注意区分hashCode和hash两个方法,他们是不通的!!
这里就不细说hashCode了,看下面hash方法

/**     * 自己低位和高位异或操作,能够降低冲突 计算冲突,结合高16位与低16位     */    static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    }

hashCode()是一个native方法,意味着方法的实现和硬件平台有关,默认实现和虚拟机有关,对于有些JVM,hashCode()返回的就是对象的地址,大多时候JVM根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,并返回。
所以hashCode返回的是一个32位的2进制数值,而Java8中这样的实现,保证了对象的hashCode的高16位的变化能反应到低16位中,相比较而言减少了过多的位运算,是一种折中的设计。

table的容量只能是2的倍数

table容量为2的倍数时,有利于下一个缓解的计算table的下标,另一个方面,虽然在HashMap中,提供了一个构造方法:

public HashMap(int initialCapacity, float loadFactor) 

看似提供了初始容量的方法,但是这个方法最后一行代码中调用了另一个方法tableSizeFor来确定table的容量:

/**     * hashMap大小只能为map的倍数。 最终会返回一个最适合cap的2的倍数     * capacity.     */    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;    }

所以最终table的length只能是2的倍数。

table下标计算方法

有点基础的读者应该知道,一般索引,都是传入的是一个键(key),而这里的key是一个引用类型,通俗点,就是个类(class),既然是个class,那又怎么获取下标呢?即怎么与下标联系起来呢?看下面代码:

tab[(n - 1) & hash]

没错,就是通过这样的方式,其中hash=hash(key),n=table.length这行端代码就相当有艺术了。
由前面知道,table.length是一个2的倍数,随意化成2进制就是开头一个1,后面n个0。随意当减1后,就会变成一排1,
之后,在与刚刚得到的hash(通过高位和低位计算后得到的hash)值做二进制与操作,因为(n-1)的高位都是0,所以最终只会截
取到hash的后log(n)-1位,会得到一个范围在0~table.length的值,这个值,就是数组的下标。是不是很有艺术。

由于hash是由key的hashCode的高16位与低16位经过异或而得,混合了原始哈希码的高低位,大大的提升了随机性,也让碰撞机率大大降低。

put方法

前面已经讲了put方法的基本过程,下面再细看看putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) ,其中,如果key=null,那么hash(key)=0,所以是能够存放null值的。方法实现代码:

    /**     * 插入值, onlyIfAbsent,为真的话,就是不替换,无就插,有就不插 Implements Map.put and related     * methods evict,表示需要调整二叉树结构,LinkedHashMap中需要     */    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {        Node<K, V>[] tab;   //存放table        Node<K, V> p;      //存放以前存放在table[(n-1)&hash]的节点,如果有        int n, i;        if ((tab = table) == null || (n = tab.length) == 0)            n = (tab = resize()).length;          //判断是否需要扩容        if ((p = tab[i = (n - 1) & hash]) == null)            // 没有数据,就是放一个链表头节点            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))))                // 一模一样,连key也equals后相等时                e = p;            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) {                        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;                    //其中,如果key的equals也相等,就直接替换                    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;        if (++size > threshold)            resize();        //LinkedHashMap使用        afterNodeInsertion(evict);        return null;    }

具体代码分析已经注释到了代码里面。

get方法

如下代码:

    /**     * 根据key返回它的值。     */    public V get(Object key) {        Node<K, V> e;        return (e = getNode(hash(key), key)) == null ? null : e.value;    }

如上代码所示,可以获取null值。

    /**     * 根据key返回值。 也就是先算hash,在找到其位置,在看是否有因冲突而产生的链表或者二叉树。     */    final Node<K, V> getNode(int hash, Object key) {        Node<K, V>[] tab;   //指向table,这样如果对table加锁,自己还是能够只读的        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))))                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);            }        }        return null;    }

基本的由key获取节点的过程大致是这样,代码中已有注释,这里就不多讲。

remove方法

如下代码:

    /**     * 根据key,删掉这个节点。     */    public V remove(Object key) {        Node<K, V> e;        return (e = removeNode(hash(key), key, null, false, true)) == null ? null                : e.value;    }

接下来看具体的removeNode方法:

    /**     * 删除某一个节点。     * @param matchValue     *            如果为真,那么只有当value也想等时,才能删除。     * @param movable 能否删除     */    final Node<K, V> removeNode(int hash, Object key, Object value,            boolean matchValue, boolean movable) {        Node<K, V>[] tab;        Node<K, V> p;        int n, index;        if ((tab = table) != null && (n = tab.length) > 0                && (p = tab[index = (n - 1) & hash]) != null) {            //寻找node节点过程            Node<K, V> node = null, e;            K k;            V v;            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<K, V>) 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);                }            }            //node节点就是已经找到的,符合条件的要删除的节点。            if (node != null                    && (!matchValue || (v = node.value) == value || (value != null && value                            .equals(v)))) {                if (node instanceof TreeNode)                    ((TreeNode<K, V>) 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;    }

具体代码和前面的get方法相似,先找到节点,然后在判断哪种方法删除,以及删除之后的调整。

containsValue方法

和containsKey方法不同,它可以通过先散列,在判断key是否equals来判断是否含有这个key,而containsValue方法,则是直接暴力枚举所有value,然后得出有这个value,性能较差。

    /**     * 在map中如果至少有一个value的值为value,就返回true。 ,注意下面有个双重循环,一个是循环数组,一个是循环链表(二叉树)。     */    public boolean containsValue(Object value) {        Node<K, V>[] tab;        V v;        if ((tab = table) != null && size > 0) {            for (int i = 0; i < tab.length; ++i) {                for (Node<K, V> e = tab[i]; e != null; e = e.next) {                    //在TreeNode中,next属性也够用,因为TreeNode的父类是Node                    if ((v = e.value) == value                            || (value != null && value.equals(v)))                        return true;                }            }        }        return false;    }

resize方法

前面讲过,当table的使用量到达length*loadFactor时,机会触发扩容操作,扩容操作的基本流程为:
1、判断是否需要扩容
2、将老数组table的元素,一个一个遍历并插入到新数组newTable中
3、更改相应的字段属性值。

    /**     * 初始化使用,     * 或者将hashmap大小调整为2的倍数级使用。     */    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) {            // 如果当前size大于最大容量,则下一次就是int的最大值            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<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)                        // 当这个位置没有东西时候,就直接取莫放在这里。,重新计算hash值以便。                        newTab[e.hash & (newCap - 1)] = e;                    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) {                                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;    }

HashMap里面的iterator

HashMap里面具有下面几种Iterator:
HashIterator:普通Iterator的父类
KeyIterator:key的Iterator,继承自HashIterator
ValueIterator:Value的Iterator,继承自HashIterator
EntryIterator:key和value的Iterator,继承自HashIterator
同样的,HashMap里面也有Spliterator:
关于何为Spliterator,请看这篇: 集合源码学习:Spliterator
但是就目前Java8的源码来看,HashMap里面的分割列表,它是基于table的元素进行迭代的,啥意思呢?
在就是在trySplit方法里面,仅仅是对table进行横向的分割,类似于对数组的分割。
而在tryAdvance中,只会对table[current]进行以下 的遍历,即遍历链表或二叉树。如果current为null,则向下找一个不为空的table[current],找到后,只遍历一个table[current]找的终点则是本Spliterator的fence。
而在forEachRemaining中,遍历多个,和tryAdvance不同的时,它会遍历本Spliterator所有不为null的table[current]。

学习过程中,从很多文章中学到了知识:
http://www.importnew.com/20121.html
https://www.zhihu.com/question/20733617/answer/111577937
http://www.cnblogs.com/tonyluis/p/5671873.html
http://blog.csdn.net/ghsau/article/details/16843543

原创粉丝点击