zoukankan      html  css  js  c++  java
  • HashMap

    HashMap

    • HashMap jdk7与8的实现存在差异;
    • 在HashMap7的基础上, 8对7进行了优化;

    一、HashMap(jdk7)

    1.1 HashMap的结构

    • HashMap 里面是一个Entry<K,V>[]类型数组table,然后数组中每个元素是一个单向链表。
      上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
    • capacity:当前数组容量,始终保持 2n,最大容量为230,可以扩容,扩容后数组大小为当前的 2 倍。默认容量(DEFAULT_INITIAL_CAPACITY) 为16;
    • loadFactor:负载因子,默认负载因子(DEFAULT_LOAD_FACTOR)为 0.75。
    • threshold:扩容的阈值,等于 capacity * loadFactor
    • size属性, 记录Entry实例的数量;
    • modCount 属性, 记录HashMap实例修改的次数;

    1.2 Entry节点的结构

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash; // 存key的hash值, key为null, 则为0
    
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    }
    
    • Entry节点为一个单向链表的结构;
    • Entry节点重写了HashCode方法, hashCode值为key的hash值与value的hash值向异或;
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    
    • Entry节点的equals方法, 先查看比较的Object是否为Map.Entry类型, 若是则先比较key是否相等, 若相等则比较value是否相等;
     public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    

    1.3 hash(Object obj)

    多次移位运算, 目的是为了让hash值更均匀的分布在hash表中;

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    1.4 put方法

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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;
            }
        }
    
        modCount++; // 记录HashMap实例的变化次数
        addEntry(hash, key, value, i);
        return null;
    }
    
    1. 校验key是否为null, key为null则遍历table[0]处的链表, 若链表不为空则将value更新, 返回旧的value; 若链表为空, 则创建Entry节点,并将其插入到table[0]头部;具体创建Entry及插入步骤参考4
    2. key不为null, 先获取key的hash值, 通过hash值与map长度-1相与, 获得key在table中的索引, 遍历该索引处的链表, 若链表中存在相同的key, 则更新对应的value, 将旧value返回;

    判断key相同的条件: e.hash == hash && ((k = e.key) == key || key.equals(k)); 先比较hash值, 再比较是否相等;

    1. 若索引链表没有相同的key, 调用创建Entry实例, 将Entry实例插入到链表头部; 具体创建及插入步骤参考4

    2. 先检查是否需要扩容, 若需要扩容则先扩容,扩容之后重新计算key的hash值,索引值, 创建Entry节点然后插入到链表头部;

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, 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++;
    }
    

    1.5 HashMap扩容

    扩容条件: 在put新的key-value是, Entry节点的数量size大于capacity与loadFactor乘积threshold, 且恰好key所在的索引处已经存在节点了, 于是触发扩容

    (size >= threshold) && (null != table[bucketIndex])

    HashMap中每次扩容为之前的两倍, 然后将table中的节点放到新的newTable中,将table更新为newTable;

    void addEntry(int hash, K key, V value, int bucketIndex) {
       if ((size >= threshold) && (null != table[bucketIndex])) {
           resize(2 * table.length);
           hash = (null != key) ? hash(key) : 0;
           bucketIndex = indexFor(hash, table.length);
       }
       createEntry(hash, key, value, bucketIndex);
    }
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        Entry[] newTable = new Entry[newCapacity];
        // 遍历hash表table, 将 entry节点放到新的 newTable中;
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                // 重新计算索引值
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    

    1.6 get方法

    • get方法获取key值对应的value;

    • 若key为null, 则直接从table[0] 中查找, 若table0出链表不为空, 则返回value值;

    • key不为null, 计算出key的hash值, 通过hash值与map长度-1相异或获取key在table中的索引, 遍历索引处的链表, 若找到一个相等的key则将其Entry返回;若没找到则返回null;

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
    

    1.7 问题汇总

    • 问题1: put过程中为什么entry节点要从头部插入?

    在多线程情况下, 如果一个线程a正在get key1, 当线程a正在遍历链表时, 另一线程b在put key2, 而恰好key1, key2在table中的索引相同, 若put将key2-value插入到链表尾部, 则会被线程a所访问到, 产生数据不一致问题; 而若put将key2-value插入到链表头部, 则不会存在这个问题, 在一定程度上保证线程安全;

    • 问题2: 扩容过程中entry节点位置的变化规律?

    每次扩容, 会将map的length翻倍, 而key在table中的索引是由 hash & (length - 1) 计算出来的, 相比而言每次扩容索引值相当于hash值多暴露一bit位, 若暴露的bit位为0, 则索引与之前相比不发生变化, 若暴露的bit位为1, 则索引与之前相比则右移了length/2长度(length指的是扩容后的长度);

    • 问题3: HashMap的put操作存在的线程安全问题?

    主要有两个线程安全问题:

    • put时, entry节点被覆盖: 当线程a, b同时执行完获取索引头结点后, 线程a抢到执行CPU权, 向下执行将自己设置为头结点, a执行结束; b执行, 由于索引处头结点并没有更新, b将自己的entry设置为头结点, 此时将a线程设置的头结点覆盖, a线程put操作key-value丢失;
    void createEntry(int hash, K key, V value, int bucketIndex) {
         Entry<K,V> e = table[bucketIndex]; // 先获取索引处头结点, 线程a, 线程b;
         // 将头结点设置为为新entry的next节点
         table[bucketIndex] = new Entry<>(hash, key, value, e); 
         size++;
     }
    

    二、HashMap(jdk8)

    HashMap在jdk8, 使用数组 + 链表 + 红黑树的方式组成, 在 Java8 中,当链表中的元素超过了8个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

    Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。

    2.1 Node 与 TreeNode

    • Node节点, 与jdk7实现的Entry节点类型; 是一个单向链表节点的结构;
    static class Node<K,V> implements Map.Entry<K,V> {
      final int hash;
      final K key;
      V value;
      Node<K,V> next;
    
      Node(int hash, K key, V value, Node<K,V> next) {
          this.hash = hash;
          this.key = key;
          this.value = value;
          this.next = next;
      }
    }
    
    • TreeNode节点, 继承与LinkedHashMap.Entry<K,V>, 而LinkedHashMap.Entry<K,V>继承于HashMap.Node<K,V>, 因此TreeNode为Node类的子类; 为一个红黑树节点结构;
     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;
      TreeNode(int hash, K key, V val, Node<K,V> next) {
          super(hash, key, val, next);
      }
    }
    

    2.2 HashMap新增的几个属性

    static final int TREEIFY_THRESHOLD = 8; // 
    static final int UNTREEIFY_THRESHOLD = 6; // 
    static final int MIN_TREEIFY_CAPACITY = 64; // 
    transient Node<K,V>[] table; // Node数组, 存储key-value对的hash表; jdk1.7时, 为Entry[]类型
    

    2.3 hash方法

    HashMap在jdk8中hash值计算,采用key的hash值与key的hash值的高16位相异或, 好处在于: key在数组中的索引是通过hash & (length - 1) 计算出来的, 计算key的hash值时采用高位与低位相异或, 让高位也能尽可能影响到索引的计算, 可以使key在数组中分布的更均匀;

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    2.4 get方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    /**
     * 根据key获取对应Node节点;
     * @param  hash key的hash值, hash(key)
     * @param  key  key对象
     * @return      key所在的Node节点;
     */
    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 && // 先检查头结点, key若在头结点直接返回;
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) { // 不在头结点, 遍历后面的节点
                // 若节点类型为TreeNode, 为红黑树结构调用方法获取TreeNode
                if (first instanceof TreeNode) 
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do { // 节点类型不为TreeNode, 为链表结构, 则遍历后面的节点查找;
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

    2.5 put方法

    1. 计算出key所在的索引, 若此索引处为null, 则创建节点直接添加到索引处; 不为null, 则走2
    2. 检查索引头结点处的key是否与要插入的key-value对的key相等, 若想等则将之前的旧值更新, 返回旧value; 若不等走3
    3. 检查头结点类型是否为TreeNode, 若是, 则调用TreeNode的putTreeVal方法插入key-value; 若否则走4
    4. 循环遍历, 使用一个binCount变量计数, 若key与其中的Node节点key相等,则退出循环,走5; 若不相等, 遍历到尾节点, 将key-value插入到尾节点, 检查binCount的值, 若binCount大于等于7, 则将这条链表转换成红黑树结构;
    5. 若e不为null, 则返回e.value, 即旧的key值; 若e为null, 则++size, 判断size是否大于等于threshold, 若为true则进行扩容;
    • put方法特点, 先添加节点后检查是否要扩容; 而jdk7则是先检查扩容, 后添加节点;
    • jdk8新节点插入到链表末尾, 而jdk7新节点插入到链表头部;
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    /**
    * 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;
        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<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 {
                // 当binCount大于等于7时, 将tab中哈希值为hash对应索引处的链表转换为红黑树结构
                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;
                    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大于等于threshold则扩容;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    
  • 相关阅读:
    es6 学习小计
    aligin-items与aligin-content的区别
    编写Shader时的一些性能考虑
    Shader预处理宏、内置状态变量、多版本编译等
    Unity内置的shader include files
    Vertex and Fragment Shader
    对于资源上MissingScript的清理方案讨论
    Surface Shader
    LOD设置
    《蛙》
  • 原文地址:https://www.cnblogs.com/jxkun/p/9477877.html
Copyright © 2011-2022 走看看