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

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[] table;

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

HashMap的存储节点

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

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

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

    static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
        TreeNode parent; // red-black tree links,红黑树,保证是一棵平衡二叉树
        TreeNode left;   //左子树
        TreeNode right;  //右子树
        TreeNode 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[] tab;   //存放table
        Node 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 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) 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 e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

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

    /**
     * 根据key返回值。 也就是先算hash,在找到其位置,在看是否有因冲突而产生的链表或者二叉树。
     */
    final Node getNode(int hash, Object key) {
        Node[] tab;   //指向table,这样如果对table加锁,自己还是能够只读的
        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 && // 总是检查是否为头节点。
                    ((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获取节点的过程大致是这样,代码中已有注释,这里就不多讲。

remove方法

如下代码:

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

接下来看具体的removeNode方法:

    /**
     * 删除某一个节点。
     * @param matchValue
     *            如果为真,那么只有当value也想等时,才能删除。
     * @param movable 能否删除

     */
    final Node removeNode(int hash, Object key, Object value,
            boolean matchValue, boolean movable) {
        Node[] tab;
        Node p;
        int n, index;
        if ((tab = table) != null && (n = tab.length) > 0
                && (p = tab[index = (n - 1) & hash]) != null) {
            //寻找node节点过程
            Node 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) 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) 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[] tab;
        V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node 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[] resize() {
        Node[] 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[] 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)
                        // 当这个位置没有东西时候,就直接取莫放在这里。,重新计算hash值以便。
                        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;
    }

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

你可能感兴趣的:(集合源码学习)