HashMap学习笔记

HashMap学习笔记

  • 今天来学习面试以及平时工作学习中常用到的一个类HashMap。

概述:

  1. 什么是HashMap
  2. 常见操作(get和put)的工作原理
  3. Hash函数的实现
  4. Resize是什么

一、什么是HashMap

  • 官方描述:
    HashMap官方描述

    关键信息:基于Map、和HashTable类似但HashMap不能保证线程安全且允许键值对为null、不保证有序(个人觉得所谓的序是指“put”进来的顺序)、不保证顺序不随时间改变(个人认为是由于reSize的原因)

  • HashMap的结构:基于链表加数组

  • 值得一提的参数:

    • Initial Capacity 即table的大小,如果初始值大于允许的最大值N/0.75,那么reHash操作永远不会发生,默认值是16。

    • Load Factor:0.75,达到了良好的时间和空间之间的平衡开销的一个点。>0.75将会降低空间开销但会增加查找开销(在get和put操作中得以体现。

    • MIN_TREEIFY_CAPACITY:64,只有当able的大小达到64之后,链表转成树才可能实现,否则,当某节点的链长度大于8时只会发生table的resize操作。
    • UNTREEIFY_THRESHOLD:8,当某节点的链表长度大于该值时,发生链表到树的转换操作。
    • UNTREEIFY_THRESHOLD:6,当某节点的链表长度减少到该值时,发生树到链表的转换操作。
  • 值得一提的fail-fast:

    • 如果map在迭代器生成后被修改了(不包含迭代器自身的移除操作),迭代器将会抛出ConcurrentModificationException,当面对同步修改操作时,迭代器会快速失败。迭代器的快速失败原则应当尽量只在debug时被使用到。

二、HashMap的常见操作

  • put操作

    • 步骤

      1. 将key的hashCode()做hash运算,再计算index
      2. 碰撞了?链表长度小于等于8?链表形式放入buckets
      3. 碰撞了?链表长度大于8了?链表转成红黑树
      4. 没碰撞?直接放入buckets
      5. 节点存在了?替换!
      6. bucket大小大于阈值了?resize
    • 源码:
      public V put(K key, V value) {
      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;
      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 {
      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;
      if (++size > threshold)
      resize();
      afterNodeInsertion(evict);
      return null;
      }

  • get操作

    • 步骤:

      1. buckets中第一个节点的hash值是目标key的hash值:直接返回
      2. 有冲突时:是树吗?return ((TreeNode<K,V>)first).getTreeNode(hash, key);
      3. 不是树时:在链表中get
    • 代码:
      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) {
      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;
      }

三、Hash函数的实现

  • 代码:
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 生动描述:

四、Resize()是什么

  • 当进行put操作时,当前所占bucket的程度已达到了 LoadFactor*Capacity时,就会触发resize操作,也就是把bucket的容量扩大两倍,然后重新计算各个节点的index值重新分配到新的bucket中。注释如下:

  • Resize的实现


    当hash1新增的比特位(右边开始第五个)为0的时候,则index不变,如果是1的话,则为原位置加oldCapacity。这样就不需要经过复杂的index计算,是不是很精妙呢?

阅读更多

没有更多推荐了,返回首页