zoukankan      html  css  js  c++  java
  • HashMap结构解析

    一 简介

    java.lang.Object  
    
    ↳     java.util.AbstractMap  
    
    ↳           java.util.HashMap  
    
    public class HashMap  
    
    extends AbstractMap  
    
    implements Map, Cloneable, Serializable { }  
    

    HashMap是基于哈希表实现的,每一个元素是一个key-value对,实现了Serializable、Cloneable接口,允许使用null值和null键。不保证映射的顺序,内部通过单链表解决冲突问题,容量超过(容量*加载因子)时,会自动增长。(除了不同步和允许使用null之外,HashMap类与Hashtable大致相同)
    HashMap不是线程安全的,如果想获取线程安全的HashMap

    • 1通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
        Map map = Collections.synchronizedMap(new HashMap());
      
    • 2使用concurrent并发包下的concurrentHashMap。

    二 数据结构

    HashMap由数组+链表组成的,主干是一个Entry数组,每一个entry包含一个(key-value)键值对,链表则是主要为了解决哈希冲突而存在的,HashMap通过key的hashCode来计算hash值,当hashCode相同时,通过“拉链法”解决冲突,如下图所示。

    如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,只需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以性能考虑,HashMap中的链表出现越少,性能才会越好。

    三 源码分析

    1 关键属性

    //默认初始化化容量,即16  
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    //最大容量,即2的30次方  
    static final int MAXIMUM_CAPACITY = 1 << 30;  
    //默认加载因子,当容器使用率达到75%的时候就扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  
    //HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态  
    static final Entry<?,?>[] EMPTY_TABLE = {};  
    //空的存储实体  
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  
    //实际存储的key-value键值对的个数
    transient int size;
    //扩容的临界点,如果当前容量达到该值,则需要扩容了.
    //如果当前数组容量为0时(空数组),则该值作为初始化内部数组的初始容量
    int threshold;
    //由构造函数传入的指定负载因子
    final float loadFactor;
    //修改次数,用于快速失败机制
    transient int modCount;
    //默认的threshold值  
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
    

    2 构造方法

    构造方法主要完成容量和加载因子的设置

          /**
         * 通过初始容量和状态因子构造HashMap 
         * @param initialCapacity 容量
         * @param loadFactor 加载因子
         */
        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 = initialCapacity;  
            //init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
            init();
        }  
    
       /**
         * 通过扩容因子构造HashMap,容量去默认值,即16 
         */
        public HashMap(int initialCapacity) {  
            this(initialCapacity, DEFAULT_LOAD_FACTOR);  
        }  
    
          /**
         * 加载因子取0.75,容量取16,构造HashMap 
         */
        public HashMap() {  
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
        }  
    
        /**
         * 通过其他Map来初始化HashMap,容量通过其他Map的size来计算,加载因子取0.75
         */
        public HashMap(Map<? extends K, ? extends V> m) {  
            this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
            //初始化HashMap底层的数组结构
            inflateTable(threshold);
            //添加m中的元素
            putAllForCreate(m);  
        }  
    

    3 存储数据(put)

        /**
         * 存入一个键值对,如果key重复,则更新value
         * @param key 键值名
         * @param value 键值
         * @return 如果存的是新key则返回null,如果覆盖了旧键值对,则返回旧value
         */
        public V put(K key, V value) {
            //如果数组为空,则新建数组
            if (table == EMPTY_TABLE) {
             //初始化HashMap底层的数组结构
                inflateTable(threshold);
            }
            //如果key为null,则把value放在table[0]中
            if (key == null)
                return putForNullKey(value);
            //生成key所对应的hash值
            int hash = hash(key);
            //根据hash值和数组的长度找到:该key所属entry在table中的位置i
            int i = indexFor(hash, table.length);
            /**
             * 数组中每一项存的都是一个链表,
             * 先找到i位置,然后循环该位置上的每一个entry,
             * 如果发现存在key与传入key相等,则替换其value。然后结束方法。
             * 如果没有找到相同的key,则继续执行下一条指令,将此键值对存入链表头
             */
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            //map操作次数加一
            modCount++;
            //查看是否需要扩容,并将该键值对存入指定下标的链表头中
            addEntry(hash, key, value, i);
            //如果是新存入的键值对,则返回null
            return null;
        }
    
    /**
      *  将该键值对存入指定下标的链表头中
         * @param hash hash值
         * @param key 键值名
         * @param value 键值
         * @param bucketIndex 索引
      */
    void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                //当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
            createEntry(hash, key, value, bucketIndex);
        }
    
     /**
         * 将键值对与他的hash值作为一个entry,插入table的指定下标中的链表头中
         * @param hash hash值
         * @param key 键值名
         * @param value 键值
         * @param bucketIndex 索引
         */
        void createEntry(int hash, K key, V value, int bucketIndex) {
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
        }
    

    4 hash算法

    static int hash(int h) {
        //此功能确保hash码不同,有限数量的碰撞
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    5 调整容量(resize)

     /**
         * 对数组扩容,即创建一个新数组,并将旧数组里的东西重新存入新数组
         * @param newCapacity 新数组容量
         */
        void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length
            //如果当前数组容量已经达到最大值了,则将扩容的临界值设置为Integer.MAX_VALUE(Integer.MAX_VALUE是容量的临界点)
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
            //创建一个扩容后的新数组
            Entry[] newTable = new Entry[newCapacity];
            //将当前数组中的键值对存入新数组
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            //用新数组替换旧数组
            table = newTable;
            //计算下一个扩容临界点
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
        }
    
      /**
         * 将现有数组中的内容重新通过hash计算存入新数组
         * @param newTable 新数组
         * @param rehash  
         */
        void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            //遍历现有数组中的每一个单链表的头entry
            for (Entry<K,V> e : table) {
                //查找链表里的每一个entry
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    //根据新的数组长度,重新计算此entry所在下标i
                    int i = indexFor(e.hash, newCapacity);
                    //将entry放入下标i处链表的头部(将新数组此处的原有链表存入entry的next指针)
                    e.next = newTable[i];
                    //将链表存回下标i
                    newTable[i] = e;
                    //查看下一个entry
                    e = next;
                }
            }
        }
    

    6 数据读取(get)

    /**
         * 返回此hashmap中存储的键值对个数
         * @return 键值对个数
         */
        public int size() {
            return size;
        }
    
        /**
         * 根据key找到对应value
         * @param key 键值名
         * @return 键值value
         */
        public V get(Object key) {
            //如果key为null,则从table[0]中取value
            if (key == null)
                return getForNullKey();
            //如果key不为null,则先根据key,找到其entry
            Entry<K,V> entry = getEntry(key);
            //返回entry节点里的value值
            return null == entry ? null : entry.getValue();
        }
    
        /**
         * 返回一个set集合,里面装的都是hashmap的value。
         * 因为map中的key不能重复,set集合中的值也不能重复,所以可以装入set。
         * 在hashmap的父类AbstractMap中,定义了Set<K> keySet = null;
         * 如果keySet为null,则返回内部类KeySet。
         * @return 含有所有key的set集合
         */
        public Set<K> keySet() {
            Set<K> ks = keySet;
            return (ks != null ? ks : (keySet = new KeySet()));
        }
    
        /**
         * 返回一个Collection集合,里面装的都是hashmap的value。
         * 因为map中的value可以重复,所以装入Collection。
         * 在hashmap的父类AbstractMap中,定义了Collection<V> values = null;
         * 如果values为null,则返回内部类Values。
         */
        public Collection<V> values() {
            Collection<V> vs = values;
            return (vs != null ? vs : (values = new Values()));
        }
    

    7 移除数据(clear remove)

        /**
         * 删除hashmap中的所有元素
         */
        public void clear() {
            modCount++;
            //将table中的每一个元素都设置成null
            Arrays.fill(table, null);
            size = 0;
        }
    
    /**
         * 根据key删除entry节点
         * @param key 被删除的entry的key值
         * @return 被删除的节点的value,删除失败则返回null
         */
        public V remove(Object key) {
            Entry<K,V> e = removeEntryForKey(key);
            return (e == null ? null : e.value);
        }
    
    /**
         * 根据key删除entry节点
         * @param key 被删除的entry的key值
         * @return 被删除的节点,删除失败则返回null
         */
        final Entry<K,V> removeEntryForKey(Object key) {
            if (size == 0) {
                return null;
            }
            //计算key的hash值
            int hash = (key == null) ? 0 : hash(key);
            //计算所属下标
            int i = indexFor(hash, table.length);
            //找到下标所存储的单链表的头节点
            Entry<K,V> prev = table[i];
            Entry<K,V> e = prev;
            //迭代单链表找到要删除的节点
            while (e != null) {
                Entry<K,V> next = e.next;
                Object k;
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    modCount++;
                    size--;
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    e.recordRemoval(this);
                    return e;
                }
                prev = e;
                e = next;
            }
            return e;
        }
    

    8 Fail-Fast机制

    “快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。 注意 :记住是有可能,而不是一定。 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

    四 JDK1.7和1.8 HashMap的区别

    1 数据结构

    使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构。如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个桶位。如果同一个桶位里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(logn)的开销。也就是说put/get的操作的时间复杂度最差只有O(log n)。
    注意:有一个限制:key的对象,必须正确的实现了Compare接口。如果没有实现Compare接口,或者实现得不正确(比方说所有Compare方法都返回0)。那JDK1.8的HashMap其实还是慢于JDK1.7的。

    2 hash算法

    /**
       * @param key 
       * @return 
       */
    static final int hash(Object key) {
         int h;
         // h = key.hashCode() 为第一步 取hashCode值
         // h ^ (h >>> 16)  为第二步 高位参与运算
         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    作者:LiuJian-Android
    链接:https://juejin.im/post/5a49931351882533f01ee996
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    前中后序建立树或者直接历遍
    Leetcode:面试题 04.03. 特定深度节点链表
    按层数层序历遍
    Solidity函数修饰符
    无线传感网定位技术
    无线传感器网络概述,传感器网络结构
    Solidity高级用法
    智能合约交互
    内存
    CPU的态
  • 原文地址:https://www.cnblogs.com/love-cj/p/8341847.html
Copyright © 2011-2022 走看看