HashMap详解

2018-12-07  本文已影响0人  小狼星I

HashMap详解篇

HashMap介绍

先看下Map的类结构,可以看到TreeMap是基于树实现的,HashMap,HashTable,ConcurrentHashMap是基于Hash来实现的,而HashTable和HashMap的底层实现基本一致,只不过是HashTable通过synchronized关键字修饰实现了线程安全性,是一个同步容器,而ConcurrentHashMap是一个并发容器,再多线程并发的场景下性能要比HashTable好很多;


image

HashMap Put分析

HashMap其实就是key-value结构,如下代码:

    Map map=new HashMap<String,String>();
    map.put("1", "one");
    map.put("2", "two");
    map.put("3", "three");
    map.put("4", "four");
    map.put("5", "five");
    map.put("6", "six");

我们再来看下HashMap的源码,来看几个重要的要素:


    /**
     * HashMap底层是使用数组来存储数据节点的,此table变量就是HashMap用于存储的数组
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;
    
    /**
     * HashMap的逻辑长度
     * The number of key-value mappings contained in this map.
     */
    transient int size;
    
    /**
     * HashMap的修改次数
     */
    transient int modCount;

    /**
     * 默认的负载因子,当HashMap的剩余容量<总容量*DEFAULT_LOAD_FACTOR时,就触发扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    
    /**
     * 用于存储的数据节点
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //每个key的hash值
        final K key;    //用于存储的key
        V value;        //用于存储的value
        Node<K,V> next; //用于指向下一个节点的内存地址,可用于Hash碰撞时产生单向链表
        
        //Node的构造函数
        Node(int hash, K key, V value, Node<K,V> 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;
        }
    }
    
    
    /**
     * HashMap的构造函数一,使用默认的初始化容量16,和默认的负载因子0.75
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    /**
     * HashMap的构造函数二,指定初始化容量          
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    /**
     * HashMap的构造函数三,指定初始化容量和负载因子    
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }


上面HashMap已经初始化完毕,下面看下HashMap的put方法,我们传入key和value,然后在put方法内部调用了putVal方法,而在putVal方法要传入key的hash值;

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
   
   /**
   * 根据key生成hashCode
   */
   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

继续看putVal方法,会先判断HashMap的容器是否为空,如果为空则触发resize,接下来又判断是否发生Hash碰撞,如果发生碰撞该如何处理;
需要注意的是:在JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率;

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果table为空,放入第一个元素时会触发resize,生成一个长度为16但是空的Node<K,V>[] table数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //这里使用(n - 1) & hash来计算出一个数组下标,代表这个key应该存放在table数组的哪个位置,       
        //然后根据这个数组下标去数组中查询数据,如果该下标没有数据则new一个Node节点来存放key-value
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //如果根据(n - 1) & hash来计算出的数组下标有数据存在,也就是说产生了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)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //如果产生了Hash碰撞则new一个新的Node节点,并将当前下标对应的Node的nexe指针指向生成的新的Node节点,
                        //也就是转成了一个单向链表结构 
                        p.next = newNode(hash, key, value, null);
                        //当binCount>=TREEIFY_THRESHOLD-1时将单向链表转为红黑树treeifyBin,TREEIFY_THRESHOLD默认为8
                        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) { // 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;
    }

HashMap数据修改分析

        Map<String, Person> persionMap = new HashMap<String, Person>();
        persionMap.put("张三", new Person("张三",21));
        persionMap.put("李四", new Person("李四",19));
        persionMap.put("王五", new Person("张三",25));
        persionMap.put("赵六", new Person("张三",26));
        persionMap.put("孙七", new Person("张三",32));

执行完以上代码后在内存堆里面结构如下:


image

在Map中一个Key对应一个Value,如果key已经存在了,那么Map会直接覆盖value值,针对下面这段代码作分析;

        Person oldPerson1 = personMap.put("张三", new Person("新张三", 21));
        Person oldPerson2 = personMap.put("孙七", new Person("新孙七", 32));

依然需要看putVal方法,在这个方法中,可以看到首先会判断p是否为空,因为传进来的key已经存在,所以这里的p不会为空,如果hash值相等,key也相等,或者equals相等,赋值给e,然后一直往下看if (e != null) { ...},在这段代码里可以看到如何将传进来的value值覆盖e节点的value值,如下代码所示:

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果table为空,放入第一个元素时会触发resize,生成一个长度为16但是空的Node<K,V>[] table数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //这里使用(n - 1) & hash来计算出一个数组下标,代表这个key应该存放在table数组的哪个位置,       
        //然后根据这个数组下标去数组中查询数据,如果该下标没有数据则new一个Node节点来存放key-value
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //如果根据(n - 1) & hash来计算出的数组下标有数据存在,也就是说产生了Hash碰撞
            Node<K,V> e; K k;
            //如果hash值相等,key也相等,或者equals相等,赋值给e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                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) {
                        //如果产生了Hash碰撞则new一个新的Node节点,并将当前下标对应的Node的nexe指针指向生成的新的Node节点,
                        //也就是转成了一个单向链表结构 
                        p.next = newNode(hash, key, value, null);
                        //当binCount>=TREEIFY_THRESHOLD-1时将单向链表转为红黑树treeifyBin,TREEIFY_THRESHOLD默认为8
                        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;
                }
            }
            //如果Map中存在相同的key
            if (e != null) { // existing mapping for key
                V oldValue = e.value;//定义一个变量来存旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//用传进来的value将e的value直接覆盖
                afterNodeAccess(e);
                return oldValue;//返回旧的值
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap Put数据的过程总结:

  1. 计算key的hash值,算出元素在底层数组中的下标位置。
  2. 通过下标位置定位到底层数组里的元素(也有可能是链表也有可能是树)。
  3. 取到元素,判断放入元素的key是否==或equals当前位置的key,成立则替换value值,返回旧值。
  4. 如果是树,循环树中的节点,判断放入元素的key是否==或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。
  5. 否则就顺着元素的链表结构循环节点,判断放入元素的key是否==或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。

总结:

HashMap的最底层是数组来实现的,数组里的元素可能为null,也有可能是单个对象,还有可能是单向链表或是红黑树。

Ref:
https://zhuanlan.zhihu.com/p/28587782

上一篇 下一篇

猜你喜欢

热点阅读