聊聊Java源码 — HashMap(1)

原帖地址:http://blog.csdn.net/roderick2015/article/details/52563782,转载请注明。

1. 简单介绍

    众所周知HashMap以键值对的形式存储数据,那什么是键值对呢?我们举个栗子,你带着银行卡和钞票去银行存钱,办完事你卡留着,下次通过这个卡就能拿到上次存进去的钞票,这种卡和钞票的关系在HashMap中叫做映射。

    起初呢,HashMap只是一个小小的数组,它拿到key之后,通过一个叫hash的算法,得到与该key对应的数组下标,并把key和value都存储到这个位置上,这就是HashMap的一个Node,计算这个下标的算法受数组长度和key值的影响,只要两者不变,位置就是固定的,所以在查询时花费的时间复杂度是O(1)。当你的key越来越多,出现两个甚至多个key被hash到同一个位置的情况,这就是hash碰撞,怎么办?我这只能存一个键值对啊。

    为了解决该问题,HashMap就引入了两个方案,一个是扩容,一个是链表,扩容就是Node到了一定数量后,我就扩大数组,增大你们Hash到不同位置上的机会,但是扩容后之前存储的Node又得重新Hash找位置,这个就是性能问题了。链表是在碰撞的位置上,建个链表,最早来的那个Node作为链首,再碰撞就依次往后排。但是下次查询这些key的时候,就得遍历链表,时间时间复杂度成了O(n),当然n其实也能接受,除非你真有这么多键值对产生了碰撞,但是在Java8中,为了优化链表问题,引入了红黑树,当链表长度超过某个值时,就转为红黑树存储,这样时间复杂度就下降到了O(log N),讲到这大家就清楚HashMap里借用了数组,链表和红黑树三种数据结构进行数据的存储啦,容貌如下图所示。

这里写图片描述

2. 继承体系

    首先呢,我们了解下HashMap的继承体系,如下图所示。
这里写图片描述

    其中HashMap实现的Cloneable和Serializable接口好理解,是为了提供克隆和序列化支持,而它继承的抽象类AbstractMap以及实现的Map接口是干嘛的呢,而且AbstractMap又去实现了Map接口,这有点绕啊。其实很好理解,我们来讲个故事。

    我叫HashMap,是Map门派的直系弟子,学过Java的肯定都认识我。我的师父,人称AbstractMap,真正接触过他的人并不多,毕竟掌门嘛神龙见首不见尾,苦活累活还是咱这些弟子来干。相传我派创立之初,Map祖师爷传下了一套心法口诀,凡我派弟子必须修行此心法 (就是Map接口里的抽象方法啦,那是一套标准必须遵守,实现了Map接口,你就能名正言顺的修行它)。怎奈这口诀过于高深,晦涩难懂,却没有配套的修行功法。好在我师父乃是百年难遇的奇才,为大部分口诀配了一套详细的功法 (抽象类中的具体实现),这下好了,我们对着学就行了 (是一套公众标准,子类可以直接使用),不用走火入魔啦。可是和大家功法都一样,还怎么成为师傅的得意门生?师傅说过想要牛B,必须走出自己的道路,所以我结合自身的特性,在师傅功法的基础之上又创立了自己的功法 (子类在父类基础上进行扩展),自此我HashMap才能被你们耳闻,像其他师兄弟比如ConcurrentHashMap、TreeMap、IdentityHashMap可都是走出了自己独特的道路哦。

    看完上面这个故事我想大家对之前提出的问题有了一个自己的理解,Map接口定义的是一套抽象标准,实现这个接口的类都属于这个体系,而且需要提供标准的具体实现,而AbstractMap是Map接口的直接实现,同时是抽象类无法实例化,它则提取出公共的实现方法,或者一套标准流程。子类可以直接使用,也可以在此基础上扩展,这是对开闭原则的很好实现。另外一个好处就是子类继承抽象类,就不用实现Map接口中所有的抽象方法了,按需食用是不是很棒。有兴趣的读者可以去熟悉一下Map和AbstractMap再接着往下看,会有更多的收获。

3. 基本成员

变量

   /**
     * Node数组,hashmap存储映射数据的容器,后面简称容器数组
     */
    transient Node<K,V>[] table;

    /**
     * 用于缓存Entry集合
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * 记录实际存储的元素个数,注意与table(容器)的大小区分
     */
    transient int size;

    /**
     * 记录对HashMap结构性改变的次数,就是在增加、删除或者扩容的时候modCount+1,
     * 所以对value的修改其实是不记录的。
     * 
     * 因为HashMap是非线程安全的,所以在迭代数据时会比较这个值,如果不等说明有其他线程进行了修改,
     * 于是抛出ConcurrentModificationException,这个叫fail-fast机制,但也因为这个机制你在
     * 迭代Map数据的过程中,做了结构性的修改后继续迭代,那么也会抛出这个异常。
     */
    transient int modCount;

    /**
     * 临界值 = table(容器)的大小 * loadFactor,当实际元素的个数(size)大于它的时候,
     * HashMap就会开始扩容
     */
    int threshold;

    /**
     * 加载因子,比如默认值是0.75,那么实际元素个数超过容器大小3/4的时候就会开始扩容,
     * 这个值可以在实例化的时候修改,但是一般咱用不着,而且它是final声明的,你只能赋值1次。
     */
    final float loadFactor;

    变量对应的各种默认值,就是你不指定,那我就用它们。

    /**
     * table(容器)的默认初始大小,这里使用的是位运算,
     * 对二进制进行直接操作,左移4位就是0001 0000 = 2<sup>4</sup> = 16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    /**
     * table(容器)的最大容量
     * 左移30位,2<sup>30</sup>是int最大值+1的一半 <br>
     * 我们知道1个int类型占4个字节(byte),1个字节8位(bit),所以它的范围是-2<sup>31</sup>到2<sup>31</sup>-1
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 加载因子的默认值
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 由链表转为树存储的临界值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 由树退为链表存储的临界值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 维持树的最小容量
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

- 构造函数

   /**
     * 一般常用的就是这个构造函数啦,所有值均采用默认值
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * 这个构造函数可以让你在实例化HashMap的时候指定容器的初始容量。
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 可以指定容器的初始容量和加载因子。
     * 比如你打算存13个键值对,而HashMap默认的threshold是12,这样就会执行一次扩容操作,
     * 所以你可以指定一个大于18的初始容量。
     */
    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;
        //注意取得容器大小后是给threshold赋值,也就是说现在table(容器)依旧为null,
        //进一步的操作我们可以在put方法中找到
        this.threshold = tableSizeFor(initialCapacity);
    }

    我们再看下第三个构造函数中的tableSizeFor方法。

    /**
     * 根据你给定的容量大小,返回一个2的n次方的值,
     * 也就是对你设定的值进行一个修整,至于干嘛这么做在hash方法中我们会讲到,
     * 比如上面我们说的想存储13个键值对,如果你指定18的初始容量,那么会被修改为32,而你直接指定32容量的话,就不会被修改了。
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1; //首先n无符号右移1位,再与n本身作位或运算
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16; //最后高位移低位,位或运算后确保结果为2的n次方
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

- 节点

    /**
     * 链表节点,其实数组用的也是这个节点
     */
    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        V value;
        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
        boolean red;
        //其余代码省略.....
    }

4. 常用方法

- put方法

    /**
     * 将键(key)和值(value)以映射关系存入HashMap中,
     * 用于新增或修改(覆盖)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    这里是先通过hash方法,对key进行了改造,再传入的putVal方法。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //看容器是不是空的,要是空的就开始扩容,顺带把容器数组的长度赋给n,数组引用赋给了tab。
        //当你塞入第一个键值对得时候,他当然就是空的啦,就算你在实例化时,指定了一个初始容量,
        //也只是暂时赋给了threshold。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //通过(n - 1) & hash 立马获取到key值在数组中对应的下标,
        //啥,这样就拿到下标了?别急,请看后面的hash方法分析。
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果这个位置的节点是空的,那就是新增操作,new一个新节点,
            //注意这里next参数传的是null:
            //我只是个数组元素又不是链表,干嘛去记下个节点。
            tab[i] = newNode(hash, key, value, null);
        else { 
            //最后一种情况就是,这个位置已经有节点了,那该怎么处理呢?
            Node<K,V> e; K k;

            //如果你两相等,那就赋给引用e不管了,最后都是对e判断是否覆盖当前的值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

            //好家伙,你是个树节点?那就交给putTreeVal方法了。
            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) {
                        //都遍历到链表尾部了,那说明是新值,new一个新节点。
                        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) { //如果上面执行的是新增操作,那e的值是null。
                V oldValue = e.value;

                //onlyIfAbsent为true时,不会执行覆盖操作,但之前存的是null值,得覆盖。
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;

                //这个和afterNodeInsertion方法都是留给LinkedHashMap的方法,
                //HashMap中未实现,供子类扩展。
                afterNodeAccess(e); 
                return oldValue; //修改操作,返回旧值,后面的代码不执行了。
            }
        }

        ++modCount; //修改次数+1
        if (++size > threshold)
            //如果元素个数大于临界值,执行扩容操作。
            resize();

        afterNodeInsertion(evict);
        return null; //新增操作,啥也不给。
    }

    最后我们再看看它对key做了些什么手脚。

    static final int hash(Object key) {
        int h; 
        //无符号右移16位,即hash值本身高位与低位的异或运算。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 
    }

    这个方法是对键(key)的哈希值进行位运算,然后在put方法中通过(n - 1) & hash计算出散列在数组中的位置。这里需要面对3个问题:
    1.需在很短的时间(O(1))内计算出索引位置且必须在容器内,不能出现越界。
    2.必须散列均匀,如果hash碰撞过多,那就成链表查询了。
    3.当数据达到一定量时,必将会有更多的碰撞,那链表越来越长时,该如何处理?

    第一个问题我们可以使用key与table.length(容器数组的长度)作取模运算获得,HashMap也是这么干的,但是它干的更好。我们需要知道当n(容器数组的长度)的长度是2的n次方时,(n - 1) & hash是等价于hash % n的,这也就是为什么HashMap会对你指定的初始容量进行修改,而位于运算显然更优于取模运算。
    第二个问题HashMap是通过hash方法以及适当的扩容来实现优化的,该方法的重点是让高位也参与了计算,这样可以得到更多、更均匀的结果,比Java7中HashMap的优化更进一步。
    第三个问题在Java7中并没有优化处理,因此在碰撞较多的情况下,效率并不理想。Java8中则将超过一定长度的链表转为红黑树存储,时间复杂度由n减少到了log N。因此碰撞越多优化效果越明显,但代码复杂度也是直线上升啊。

- get方法

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

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        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<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);
            }
        }
        //根本就没有,你骗我,给你个null。
        return null;
    }

- containsKey方法

    /**
     * 调用get方法寻找Node,找到则返回true
     */
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

- remove方法

    /**
     * 移除指定键值对,返回被移除的value
     */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

    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;
       //判断该索引位置上,有没有node
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            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; //这个node的key和给定的key相等,那就是它了。
            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);
                }
            }

            //matchValue如果传的是true,则通过后面条件判断,也就是value的值相等时,才会执行删除操作
            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; //修改次数+1
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

- keySet方法

    transient volatile Set<K> keySet;
    /**
     * 返回HashMap中的键集合
     */
    public Set<K> keySet() {
        Set<K> ks;
        return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
    }

    首次调用keySet方法时,变量keySet的值是null,也就是说在你调用该方法的时候,HashMap才会生成KeySet对象,再由keySet变量缓存起来供下次使用,那么KeySet是一个怎样的类呢?

    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        //其余代码省略.....
    }

    原来KeySet就是一个集合类,里面的其他方法都好理解,iterator()方法稍微绕了一下,里面new了一个KeyIterator对象,他其实就是一个key的迭代器,用于HashMap中key的遍历,而且这个方法也是我们经常使用到的,平时使用的for循环,也是拿到这个迭代器,然后对key进行迭代的。相对应的还有values()和entrySet()方法原理都是一样的,因为这些迭代器不过是抽象父类HashIterator的扩展,有兴趣的读者可以去尝试一下,这里就不铺开了。

    好了HashMap就暂且讲到这,上面的知识已经足够你灵活使用HashMap啦,而它的扩容机制以及红黑树的操作,我会放到下篇帖子中讲解!

    对本帖代码感兴趣的读者可以点击此处查看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值