HashMap

Posted jxkun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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:当前数组容量,始终保持 2^n,最大容量为2^30,可以扩容,扩容后数组大小为当前的 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值, 再比较是否相等;

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

  4. 先检查是否需要扩容, 若需要扩容则先扩容,扩容之后重新计算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++;
     }
    • 扩容时链表成环:

      参考: https://blog.csdn.net/hhx0626/article/details/54024222

二、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;
}

以上是关于HashMap的主要内容,如果未能解决你的问题,请参考以下文章

HashMap原理:哈希函数的设计

HashMap深度解析

JDK源码阅读之 HashMap

ArrayList 和 HashMap 的默认大小是多数?

如何将 Parcelable 与 HashMap 一起使用

hashmap冲突的解决方法以及原理分析: