HashMap

HashMap 是用于存储 Key-Value 键值对的集合。
(1)HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,所以具有很快的访问速度,但遍历顺序不确定。
(2) HashMap 中键 key 为 null 的记录至多只允许一条,值 value 为 null 的记录可以有多条
(3) HashMap 非线程安全,即任一时刻允许多个线程同时写 HashMap,可能会导致数据的不一致。

hashmap 的继承

java hashMap将某个元素放到最后 hashmap存放对象_链表

1、hashmap 的存储结构

hashmap 的存储结构:数组+链表+红黑树(JDK1.8后增加了红黑树部分)

java hashMap将某个元素放到最后 hashmap存放对象_数组_02


(1)数组

HashMap 是一个用于存储 Key-Value 键值对的集合,每一个键值对也叫做一个 Entry;这些 Entry 分散的存储在一个数组当中,该数组就是 HashMap 的主干。

java hashMap将某个元素放到最后 hashmap存放对象_java_03

(2)链表

因为数组 Table 的长度是有限的,使用 hash 函数计算时可能会出现 index 冲突的情况,所以我们需要链表来解决冲突;数组 Table 的每一个元素不单纯只是一个 Entry 对象,它还是一个链表的头节点,每一个 Entry 对象通过 next 指针指向下一个 Entry 节点;当新来的 Entry 映射到冲突数组位置时,只需要插入对应的链表位置即可

java hashMap将某个元素放到最后 hashmap存放对象_红黑树_04

(3)红黑树

当链表长度超过阈值 8 时,会将链表转换为红黑树,使 HashMap 的性能得到进一步提升。

java hashMap将某个元素放到最后 hashmap存放对象_数组_05

2、类定义源码

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

      private static final long serialVersionUID = 362498820763181265L;    
       // Table 数组默认大小 16
      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;      
       // 最大容量是必须是2的幂30
      static final int MAXIMUM_CAPACITY = 1 << 30;      
      // 负载因子默认为0.75,hashmap每次扩容为原hashmap的2倍
      static final float DEFAULT_LOAD_FACTOR = 0.75f;      
      // 链表的最大长度为8,当超过8时会将链表装换为红黑树进行存储
      static final int TREEIFY_THRESHOLD = 8;  
      /** 用来确定何时解决hash冲突的,红黑树转变为链表
      * The bin count threshold for untreeifying a (split) bin during a
      * resize operation. Should be less than TREEIFY_THRESHOLD, and at
      * most 6 to mesh with shrinkage detection under removal.
      */
      static final int UNTREEIFY_THRESHOLD = 6;
       /** 当想要将解决hash冲突的链表转变为红黑树时,需要判断下此时数组的容量,若是由于数组容量太小(小于MIN_TREEIFY_CAPACITY)而导致hash冲突,则不进行链表转为红黑树的操作,而是利用resize()函数对HashMap扩容  */  
      static final int MIN_TREEIFY_CAPACITY = 64;     
      transient Node<K,V>[] table;
      transient Set<Map.Entry<K,V>> entrySet;
      transient int size;
      // 记录HashMap发生结构性变化的次数(value值的覆盖不属于结构性变化)      
      transient int modCount;
      // threshold的值应等于table.length*loadFactor,size超过这个值时会进行resize()扩容
      int threshold;
      /**
      * The load factor for the hash table.   
      */
      final float loadFactor;
      
      static final int hash(Object key) {
         int h;
         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
      public V get(Object key) {
          Node<K,V> e;
          return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
      public V put(K key, V value) {
          return putVal(hash(key), key, value, false, true);
        }
        
      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;
          }

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

(1)Node<K,V> 类用来实现数组及链表的数据结构

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;     	// 保存节点的hash值
        final K key;		 //保存节点的key值
        V value;		//保存节点的value值
        Node<K,V> next;		 // next是指向链表结构下当前节点的next节点,红黑树TreeNode节点中也用到next

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

(2)TreeNode<K,V> 用来实现红黑树相关的存储结构:

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

        /**
         * Returns root of tree containing this node.
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }

3、确定元素位置

(1)计算 hashCode

public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

hashCode 的存在主要是用于查找的快捷性,如 Hashtable,HashMap 等,hashCode 是用来在散列存储结构中确定对象的存储地址的
HashMap 之所以速度快,因为他使用的是散列表,根据 key 的 hashcode 值生成数组下标(通过内存地址直接查找,没有任何判断)。
在 JDK 中,Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法直接返回对象的内存地址。
JDK 中,我们经常把 String 类型作为 key,那么 String 类型是如何重写 hashCode 方法的呢?

public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }

就是使用 String 的 char 数组的数字每次乘以 31 再叠加最后返回,因此,每个不同的字符串,返回的 hashCode 肯定不一样。那么为什么使用 31 呢?

之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。可以看到,使用 31 最主要的还是为了性能。

如果 key 是我们的自定义对象,我们可以重写 Object 的 hashcode 方法,从而适应所需场景

(2)根据 Key 值计算出 Key 的 hash 值

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

key 的 hash 值计算是通过 hashCode() 的高 16 位异或低 16 位实现的使用位运算替代了取模运算,在 table 的长度比较小的情况下,也能保证 hashcode 的高位参与到地址映射的计算当中,同时不会有太大的开销。
1)为什么要移位 16?

将原始哈希码无符号右移16位,即高16位被移到了低16位,理由:低位不确保有没有1,但高位肯定有1,拿无符号右移后的值与原始哈希码做异或操作,可以得到一个 1 的分布在高低位相对更加均匀的结果,从而更好的均匀散列表的下标,这样根据不同 key 得出的数组索引下标尽可能分散,就不容易发生哈希碰撞,也就降低了一开始往 HashMap 中添加数据时链表的产生几率

(3)根据数组长度及 hash 值计算索引位置(数组位置)

i = (n - 1) & hash

其中 n 是数组的长度,其实该算法的结果和模运算的结果是相同的。
1)为什么使用 & 与运算代替模运算?

上述算法的结果和模运算的结果是相同的,但是对于现代的处理器来说除法和求余数(模运算)是最慢的动作
a % b == (b-1) & a :当 b 是 2 的指数时,等式成立。

2)HashMap 的 Table 数组大小(哈希桶长度)为什么建议是 2的幂次方?

当 n 为 2 的幂次方的时候,减 1 之后就会得到 得到 1111*** 的数字,这样保证了 & 中的二进制位全为 1,这样不同 key 得出的数组索引下标会尽可能分散,就不容易发生哈希碰撞,从而使 entryset 均匀的分布在桶(数组)中。

4、put 存储元素

java hashMap将某个元素放到最后 hashmap存放对象_hashmap_06


上图的 HashMap 的 put 方法执行流程图,可以总结为如下主要步骤:

1)判断数组 table 是否为null,若为 null 则执行 resize() 扩容操作

2)根据键 key 的值计算 hash 值得到插入的数组索引 i,若table[i] == nulll,则直接新建节点插入,进入步骤 6;若 table[i] 非 null,则继续执行下一步。

3)判断 table[i] 的首个元素 key 是否和当前 key 相同(hashCode 和 equals 均相同),若相同则直接覆盖 value,进入步骤6,反之继续执行下一步。

4)判断 table[i] 是否为 treeNode,若是红黑树,则直接在树中插入键值对并进入步骤6,反之继续执行下一步。

5)遍历 table[i],判断链表长度是否大于 8,若 >8,则把链表转换为红黑树,在红黑树中执行插入操作;若 <8,则进行链表的插入操作;遍历过程中若发现key已存在则会直接覆盖该key的value值。

6)插入成功后,判断实际存在的键值对数量 size 是否超过了最大容量 threshold,若超过则进行扩容

源码如下:

static final int hash(Object key) {
         int h;
         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
     }
 
     
 public V put(K key, V value) {
         //插入节点,hash 值的计算调用 hash(key) 函数,实际调用 putVal() 插入节点
         return putVal(hash(key), key, value, false, true);
     }


 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是否已初始化,否则进行初始化table操作
         if ((tab = table) == null || (n = tab.length) == 0)
             n = (tab = resize()).length;
         // 根据 hash 值确定节点在数组中的插入的位置,即计算索引存储的位置,若该位置无元素则直接进行插入
         if ((p = tab[i = (n - 1) & hash]) == null)
             tab[i] = newNode(hash, key, value, null);
         else {
             // 节点若已经存在元素,即待插入位置存在元素
             Node<K,V> e;    K k;
             // 对比已经存在的元素与待插入元素的 hash 值和 key 值,执行赋值操作
             if (p.hash == hash &&
                 ((k = p.key) == key || (key != null && key.equals(k))))
                 e = p;
             // 判断该元素是否为红黑树节点
             else if (p instanceof TreeNode)
                 // 红黑树节点则调用putTreeVal()函数进行插入
                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
             else {
                 // 若该元素是链表,且为链表头节点,则从此节点开始向后寻找合适的插入位置
                 for (int binCount = 0; ; ++binCount) {
                     if ((e = p.next) == null) {
                         //找到插入位置后,新建节点插入
                         p.next = newNode(hash, key, value, null);
                         //若链表上节点超过TREEIFY_THRESHOLD - 1,即链表长度为8,将链表转变为红黑树
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                             treeifyBin(tab, hash);
                         break;
                     }
                     // 若待插入元素在 HashMap 中已存在,key 存在了则直接覆盖
                     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);
                 // 若存在key节点,则返回旧的 key 值
                 return oldValue;
             }
         }
         // 记录修改次数
         ++modCount;
         // 判断是否需要扩容
         if (++size > threshold)
             resize();
         // 空操作
         afterNodeInsertion(evict);
         // 若不存在key节点,则返回null
         return null;
     }


  final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
        Class<?> kc = null;
        boolean searched = false;
        TreeNode<K,V> root = (parent != null) ? root() : this;
        //从根节点开始查找合适的插入位置
        for (TreeNode<K,V> p = root;;) {
            int dir, ph; K pk;
            if ((ph = p.hash) > h)
                //若dir<0,则查找当前节点的左孩子
                dir = -1;
            else if (ph < h)
                //若dir>0,则查找当前节点的右孩子
                dir = 1;
            //hash值或是key值相同
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            //1.当前节点与待插入节点key不同,hash值相同
            //2.k是不可比较的,即k未实现comparable<K>接口,或者compareComparables(kc,k,pk)的返回值为0
            else if ((kc == null && (kc = comparableClassFor(k)) == null) ||
                      (dir = compareComparables(kc, k, pk)) == 0) {
                //在以当前节点为根节点的整个树上搜索是否存在待插入节点(只搜索一次)
                if (!searched) {
                    TreeNode<K,V> q, ch;
                    searched = true;
                    if (((ch = p.left) != null &&
                         (q = ch.find(h, k, kc)) != null) ||
                         ((ch = p.right) != null &&
                         (q = ch.find(h, k, kc)) != null))
                         //若搜索发现树中存在待插入节点,则直接返回
                         return q;
                 }
                 //指定了一个k的比较方式 tieBreakOrder
                 dir = tieBreakOrder(k, pk);
             }
            TreeNode<K,V> xp = p;
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                //找到了待插入位置,xp为待插入位置的父节点,TreeNode节点中既存在树状关系,又存在链式关系,而且还是双端链表
                Node<K,V> xpn = xp.next;
                TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                if (dir <= 0)
                    xp.left = x;
                else
                    xp.right = x;
                xp.next = x;
                x.parent = x.prev = xp;
                if (xpn != null)
                    ((TreeNode<K,V>)xpn).prev = x;
                //插入节点后进行二叉树平衡操作
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
     }
 static int tieBreakOrder(Object a, Object b) {
	 int d;
     if (a == null || b == null ||
          (d = a.getClass().getName().
           compareTo(b.getClass().getName())) == 0)
          //System.identityHashCode()实际是比较对象a,b的内存地址
          d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                 -1 : 1);
     return d;
   }

假如调用 hashMap.put(“a”,0) 方法,将会在 HashMap 的 table 数组中插入一个Key为 “a” 的元素:通过 hash() 函数来确定该 Entry 的插入位置,而 hash() 方法内部会调用 hashCode() 函数得到 “a” 的 hashCode;然后 putVal() 方法经过一定计算得到最终的插入位置 index(把 Key 的 hashCode 与 HashMap 的容量 取余得出该 Key 存储在数组所在位置的下标 (源码定位 Key 存储在数组的哪个位置是以 hashCode & (HashMap 容量-1)算法得出)),最后将这个 Entry 插入到 table 的 index 位置。

5、get 获取元素

java hashMap将某个元素放到最后 hashmap存放对象_数组_07

1)首先定位到键所在的数组的下标,并获取对应节点 n。

2)判断 n 是否为 null,若 n 为 null,则返回 null 并结束;反之,继续下一步。

3)判断 n 的 key 和要查找的 key 是否相同(key 相同指的是 hashCode 和 equals 均相同),若相同则返回n并结束;反之,继续下一步。

4)判断是否有后续节点 m,若没有则结束;反之,继续下一步。

5)判断 m 是否为红黑树,若为红黑树则遍历红黑树,在遍历过程中如果存在某一个节点的 key 与要找的 key 相同,则返回该节点;反之,返回 null;若非红黑树则继续下一步。

6)遍历链表,若存在某一个节点的 key 与要找的 key 相同,则返回该节点;反之,返回 null。

源码:

// 实际上是根据输入节点的hash值和key值,利用getNode方法进行查找
 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

 
 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) {
            	// 若定位到的节点是TreeNode节点,则在树中进行查找
                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;
    }


 // 从根节点开始,调用 find() 方法进行查找
 final TreeNode<K,V> getTreeNode(int h, Object k) {
      return ((parent != null) ? root() : this).find(h, k, null);
    }


 final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                // 首先进行 hash 值的比较,若不同则令当前节点变为它的左孩子 or 右孩子
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                // 若 hash 值相同,进行 key 值的比较
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                 // 执行到这里,说明了 hash 值是相同的,key 值不同
                 // 若 k 是可比较的并且 k.compareTo(pk) 的返回结果不为 0,则进入下面的else if
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                // 若 k 是不可比较的,或者 k.compareTo(pk) 返回结果为 0,则在整棵树中查找,先找右子树,没找到则再到左子树找
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
            return null;
        }

举例:

java hashMap将某个元素放到最后 hashmap存放对象_数组_08


如上图,hashmap 查找 key:

1)得到 key 在数组中的位置:根据上图,当我们获取 key 为“a”的对象时,那么我们首先获得 key 的 hashcode97%3 得到存储位到数组下标为 1;
2)匹配得到对应 key 值对象:得到数组下表为 1 的数据 “a” 和 “c” 对象, 然后再根据 key.equals() 来匹配获取对应 key 的数据对象

6、重写 hashcode() 和 equals()

HashMap 的添加、获取时需要通过 key 的 hashCode() 进行 hash(),然后计算下标 ( n-1 & hash),从而获得要找的数组下标位置。当发生冲突(碰撞)时,利用 key.equals() 方法在链表或树中查找对应的节点。

1)如果两个对象相同(即用 equals 比较返回 true),那么它们的 hashCode 值一定要相同;
2)如果两个对象的 hashCode 相同,它们并不一定相同(即用 equals 比较返回 false)

由于 String 和 Integer 已经重写了 hashcode() 和 equals(),因此我们可以直接使用 String 和 Integer 类型对象作为 HashMap 的 key。如果 key 是其他类型的对象或者自定义的类,默认的 hashcode() 和 equals() 可能不能符合我们的要求,所以必须重写。如果没有进行重写,那么很可能某两个对象明明是“相等”,而 hashCode 却不一样。这样,当你用其中的一个作为键保存到 hashMap、hashTable 或 hashSet 中,再以“相等的”找另一个作为键值去查找他们的时候,则根本找不到。

7、resize 扩容源码

扩容是为了防止 HashMap 中的元素个数超过了阀值,从而影响性能所服务的。其中,threshold 和 loadFactor 两个属性决定着是否扩容:threshold=Length * loadFactor,Length 表示 table 数组的长度(默认值为16),loadFactor 为负载因子(默认值为 0.75);阀值 threshold 表示当 table 数组中存储的元素个数超过该阀值时,即需要扩容;如数组默认长度为 16,负载因子默认 0.75,此时threshold=16*0.75=12,即当 table 数组中存储的元素个数超过 12 个时,table 数组就该进行扩容了。

扩容条件:哈希表中的条目数超出了加载因子与当前容量的乘积,并且要存放的位置已经有元素了(hash碰撞)

由于数组是无法自动扩容的,因此 HashMap 的扩容是申请一个容量为原数组大小两倍的新数组,然后遍历旧数组,重新计算每个元素的索引位置,并复制到新数组中。对旧数组中的元素如何重新映射到新数组中?由于 HashMap 扩容时使用的是 2 的幂次方扩展的,即数组长度扩大为原来的 2 倍、4 倍、8 倍、16 倍…,因此在扩容时(Length-1)这部分就相当于在高位新增一个或多个 “1” 位(bit),所以重新计算后的索引位置要么在原来位置不变,要么就是“原位置+旧数组长度”

如下图12,HashMap 扩大为原数组的两倍为例。

java hashMap将某个元素放到最后 hashmap存放对象_红黑树_09


如上图12所示,(a)为扩容前,key1 和 key2 两个 key 确定索引的位置;(b)为扩容后,key1 和 key2 两个 key 确定索引的位置;hash1 和 hash2 分别是 key1 与 key2 对应的哈希“与高位运算”结果。

(a) 中数组的高位 bit 为“1111”,所以扩容前 table 的长度 n 为 16;(b) 中 n 扩大为原来的两倍,其数组大小的高位 bit 为“1 1111”,所以扩容后 table 的长度 n 为 32;

(a) 中的 n 为 16,(b) 中扩大两倍 n 为 32,相当于 (n-1) 这部分的高位多了一个 1,然后和原 hash 码作与操作,最后元素在新数组中映射的位置要么不变,要么向后移动 16 个位置,如下图所示。

java hashMap将某个元素放到最后 hashmap存放对象_数组_10


HashMap 中数组扩容两倍后位置的变化:

java hashMap将某个元素放到最后 hashmap存放对象_数组_11


因此,我们在扩充 HashMap,复制数组元素及确定索引位置时不需要重新计算 hash 值,只需要判断原来的 hash 值新增的那个 bit 是 1,还是 0;若为 0,则索引未改变;若为 1,则索引变为“原索引+oldCap”;如图14,HashMap 中数组从16扩容为32的resize图。

java hashMap将某个元素放到最后 hashmap存放对象_数组_12


这样设计有如下几点好处:

1)省去了重新计算 hash 值的时间(由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快),只需判断新增的一位是0或1;
2)由于新增的1位可以认为是随机的0或1,因此扩容过程中会均匀的把之前有冲突的节点分散到新的位置(bucket槽),并且位置的先后顺序不会颠倒

JDK1.8 与 JDK1.7 的扩容区别:

1)JDK1.8 省去了重新计算 hash 值的时间,只需判断新增的一位是 0 或 1(由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快);
2)JDK1.7 中扩容时,旧链表迁移到新链表的时候采用头插法,若出现在新链表的数组索引位置相同情况,则链表元素会倒置;JDK1.8 中扩容时,旧链表迁移到新链表的时候采用尾插法,在新链表的数组索引位置时链表元素不会倒置

java hashMap将某个元素放到最后 hashmap存放对象_红黑树_13

8、线程安全问题

HashMap 是线程不安全的。
(1)JDK 1.7 HashMap 采用数组 + 链表的数据结构,多线程背景下,在数组扩容的时候,存在 Entry 链死循环和数据丢失问题。

JDK1.7 中 HashMap 的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点

2)JDK 1.8 HashMap 采用数组 + 链表 + 红黑二叉树的数据结构,优化了 1.7 中数组扩容的方案,解决了 Entry 链死循环和数据丢失问题。但是多线程背景下,put 方法存在数据覆盖的问题

假设两个线程 A、B 都在进行 put 操作,并且 hash 函数计算出的插入下标是相同的,当线程 A 执行过程中由于时间片耗尽导致被挂起,而线程 B 得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程 A 获得时间片,由于之前已经进行了 hash 碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程 B 插入的数据被线程 A 覆盖了,从而线程不安全。

9、JDK 1.8 与 JDK 1.7 的不同点

(1)最重要的一点是底层结构不一样:1.7是数组+链表,1.8则是数组+链表+红黑树结构;

(2) jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;

(3)插入键值对的put方法的区别:1.8中会将节点插入到链表尾部,而1.7中是采用头插

当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

(4)扩容后数据存储位置的计算方式不一样:在 JDK1.7 的时候是直接用 hash 值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞);在 JDK1.8 的时候直接用了 JDK1.7 的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是 JDK1.7 的那种异或的方法。但是这种方式就相当于只需要判断 Hash 值的新增参与运算的位是 0 还是 1 就直接迅速计算出了扩容后的储存方式。
(5) 扩容时 1.8 会保持原链表的顺序,而 1.7 会颠倒链表的顺序;而且 1.8 是在元素插入后检测是否需要扩容,1.7 则是在元素插入前;
(6)jdk1.8是扩容时通过 hash&cap==0 将链表分散,无需改变 hash 值,而 1.7 是通过更新 hashSeed 来修改hash值达到分散的目的

(7)扩容策略:1.7中是只要大于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。
(8)在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容,但是在 1.8 的时候是先插入再扩容的,优点其实是可以减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容

10、常见问题

(1)哈希表如何解决 Hash 冲突

java hashMap将某个元素放到最后 hashmap存放对象_链表_14


(2)为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化

java hashMap将某个元素放到最后 hashmap存放对象_链表_15


(3)为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

java hashMap将某个元素放到最后 hashmap存放对象_hashmap_16


(4)HashMap 中的 key若 Object类型, 则需实现哪些方法

java hashMap将某个元素放到最后 hashmap存放对象_链表_17

11、总结

1)HashMap 中实际存储的键值对的数量通过 size 表示,table 数组的长度为 Length
2)HashMap 的哈希桶初始长度 Length 默认为16,负载因子默 loadFactor 认值为 0.75,threshold 阀值是 HashMap 能容纳的最大数据量的 Node 节点个数,threshold=Length*loadFactor

3)当 HashMap 中存储的元素个数超过了 threshold 阀值时,则会进行 reseize 扩容操作,扩容后的数组容量为之前的两倍;但扩容是个特别消耗性能的操作,所以当我们在使用 HashMap 的时候,可以估算下 Map 的大小,在初始化时指定一个大致的数值,这样可以减少 Map 频繁扩容的次数。

4)modCount 是用来记录 HashMap 内部结构发生变化的次数,put 方法覆盖 HashMap 中的某个 key 对应的 value 不属于结构变化。

5)HashMap 哈希桶的大小必须为 2 的幂次方

6)JDK1.8 引入红黑树操作,大幅度优化了 HashMap 的性能。

7)HashMap 是非线程安全的,在并发环境中同时操作 HashMap 时最好使用线程安全的 ConcurrentHashMap。
8)重写 equals 方法需同时重写 hashCode 方法。