美文网首页
hashmap源码分析

hashmap源码分析

作者: msrpp | 来源:发表于2018-07-19 23:51 被阅读59次

常量定义

hashmap实现是存储键值对的一种数据结构。首先看它的声明:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable{}

可以得知HashMap是继承自抽象类AbstractMap且实现了Map接口。(这里有一个疑问,既然抽象类AbstractMap也是实现了Map,是否可以直接写成:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Cloneable, Serializable{}

看下HashMap对应的一些常量,在此之前我们需要对hashmap的存储结构有所了解。实际是Node<K,V>[] table,数组的每个节点为一个Node链表,每个Node存储着键值对。我们将数组的每个元素称为bucket。Node的结构体如下,存储了Key的hash值,Key和Value,还有一个指向下个节点的指针。

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

HashMap内部定义的常量:

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

    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;
  • DEFAULT_INITIAL_CAPACITY:默认的buckets数目,值为16;
  • MAXIMUM_CAPACITY :最大的buckets数量,如果当前的buckets数量大于这个值,再次调用resize()将不会重新分配buckets的大小。
  • DEFAULT_LOAD_FACTOR:默认的rehash触发因子,键值对的数量大于buckets数量的75%,将发生resize()
  • TREEIFY_THRESHOLD: 大于等于这个值,该bucket下的链表将发生treeifyBin,从链表转化成红黑树。
  • UNTREEIFY_THRESHOLD: 小于等于这个值,该bucket下的节点如果是treenode,将转化成链表。
  • MIN_TREEIFY_CAPACITY:如果在treeifyBin的时候,当前的buckets数量小于这个值,将发生resize();
    实际上这种场景应该是很难触发的,在总数小于64,每次扩容两倍的情况下,就是32个buckets(16->32)根据0.75的加载因子,最大为24个键值对。即32个桶放了24个球,居然有8个球挤在一个桶里。这里的处理的确体现了作者的严谨。

常用方法

HashMap最常用的就是get,put 两个方法。下面我们拿一个简单的例子来说明HashMap的工作流程,现有如下代码:

         HashMap<String,Integer> map = new HashMap<>();

        map.put("abc",123);
        map.put("def",456);
        Integer result = map.get("abc");
        Boolean b1 = map.containsValue(123);
        Boolean b2= map.containsKey("def");
        map.forEach((key, value) -> {
            System.out.println(key+value);
        });
        map.remove("123");

首先调用默认构造函数:

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

put方法和注释:

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                  boolean evict) {
       Node<K,V>[] tab; Node<K,V> p; int n, i;
       if ((tab = table) == null || (n = tab.length) == 0)
           //首次调用put将第一次resize();
           n = (tab = resize()).length;
       if ((p = tab[i = (n - 1) & hash]) == null)
           //若p为null,则当前的桶是空的,可以直接插入数据。
           tab[i] = newNode(hash, key, value, null);
       else {
           //这个hash值对应的桶之前已经有数据了。
           Node<K,V> e; K k;
           if (p.hash == hash &&
               ((k = p.key) == key || (key != null && key.equals(k))))
               e = p;
           else if (p instanceof TreeNode)
                 //这里如果node是TreeNode的实例,则这个链表就已经变成红黑树了。
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           else {
               //循环查找链表的节点key是否和当前的key相等,binCount为链表长度,大于TREEIFY_THRESHOLD 触发treeifyBin
               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插入,直接覆盖即可。
                   p = e;
               }
           }
           if (e != null) { // existing mapping for key
               V oldValue = e.value;
               if (!onlyIfAbsent || oldValue == null)
                   e.value = value;
               afterNodeAccess(e);
               return oldValue;
           }
       }
       ++modCount;
       //size 为当前map存储的node数量,用来判断是否需要resize
       if (++size > threshold)
           resize();
       afterNodeInsertion(evict);
       return null;
   }

说下几个自我感觉的重点:

  • hash运算时h >>> 16 的作用:
    hash方法取到的值还不是桶的数量,实际是取了hash值的低位(如果是16个buckets,那就是4bit , 32则是5bit ...),h >>> 16 让hash值的高16位和低16位异或运算,可以让object的hash值的各位最大程度上参与运算。
  • 为什么HashMap可以存储null值?
    1.null的hash函数做了特异处理,为0,即放到第一个bucket中。
    2.对null值做了判断 用p.hash == hash && ((k = p.key) == key的方式让null值可以被更新。
  • 原本在不同buckets中的node,通过resize()会放到同一个bucket中吗?
    不会,resize()每次增大一倍,每个节点放置的bucket位置重新计算,如果旧table该节点在位置table[i],则新位置可能在table[i]或者table[i+oldCap]; 举个例子:buckets数量由16扩展至32,原node A在位置table[2],A的hash值是10010,则分配到新的table[2+16]。如果是01011,则依然停留在table[2]。而且不改变原node链表的相对位置关系。
  • resize()很耗时,因此在初次分配的时候指定好大概大小?
    resize()操作实际发生次数是比较少的,单次resize()是比较占用cpu,但只是一瞬间,resize()的时间复杂度是o(n)*平均链表长度 = o(n)。实际业务中,HashMap存储的数据量一般不会太大,大的数据量操作一般是借助存储服务来完成,所以实际上对性能是不会产生太大影响的,当然初次分配了一个合理的预估值,会减少一定的cpu时间。
  • 接下里的get 和remove也很简单了,分为以下步骤:
    (1) hash要查找/删除的key,找到对应的bucket
    (2) 循环链表找到要查找/删除的节点并返回/删除,其中删除的实现:pre.next = node.next;

注意下remove中几个参数的意思:node:要查找的目标节点;p:,如果node是bucket中的第一个节点,pre 即是node,否则是node的pre节点;matchValue:是否需要key,value都匹配才删除。
get的代码:

    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) {
            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);
            }
        }
        return null;
    }

remove的代码:

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

containsValue和containsKey方法

因为HashMap是按照Key来排序的,调用containsValue将循环遍历所有node来查找,效率低下,不建议过多使用containsValue函数。

forEach方法

因为HashMap不是线程安全的,如果在forEach执行过程中table中的内容发生了变化。modCount会发生变化,此时抛出一个ConcurrentModificationException,实现思想和乐观锁一致。

HashMap的迭代器。

附示例代码

        HashMap<String,String> ss = new HashMap<>();
        ss.put("1","a");
        ss.put("2","b");
        Iterator<Map.Entry<String ,String>> iterator = ss.entrySet().iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }

我们可以通过HashMap.entrySet()来获取一个该对象的Entry集合entrySet变量,开始我很疑惑,HashMap中并没有对这个变量做增加元素等操作。那怎么能平白产生数据呢?后来看到了HashMap的内部类复写了EntrySet的iterator方法。返回的是这个类对象。

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

其中nextNode实现

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> 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方法返回了Hashmap中的table存储的Node。需要注意的是,iterator对象中存储了table的位置,和当前节点的引用。这样在调用next的时候才可以返回下一个值。

LinkedHashMap

LinkedHashMap继承自HashMap,比其多了一个功能,就是可以按照插入顺序遍历出元素。原理: LinkedHashMap内部维护了一个链表,且重写了newNode方法,在新建节点的同时,就已经完成了链表的维护.LinkedHashMap#Entry<K,V>#newNode代码如下:

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

相关文章

网友评论

      本文标题:hashmap源码分析

      本文链接:https://www.haomeiwen.com/subject/jguxmftx.html