JDK8 易懂的HashMap原理

JDK1.8 易懂的HashMap原理

数组是什么?

它是存取有序的容器,在内存空间是一块连续的空间,正因为连续所以长度固定,每个分量固定大小,只能存储同类型,可以以索引进行查找,增删慢,查找快

链表是什么?

它是存取有序的容器,在内存空间并不是一块连续的空间,长度可变,可存储不同类型。每个节点都有指向下一个节点的指针,所以增删快,查找慢。

HashMap是什么?

它由数组加链表组成。所以同时拥有数组和链表的优点:查找又快,增删又快,就是有点耗内存,大道理先别讲这么多,不然容易懵逼。
首先看看且记记实例化HashMap的几个字段:

public class HashMap<K,V> extends AbstractMap<K,V>  implements Map<K,V>, Cloneable, Serializable{
	//序列化ID
    private static final long serialVersionUID = 362498820763181265L;
    //默认初始容量,也就是你自己实例化不设置的话,人家就给你这个大小
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认加载因子,就是说如果容器的容量*加载因子>=临界值时将会进行自动扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //链表节点数大于这个值时将会自动转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    //红黑树节点数小于这个只是将会自动转成链表
    static final int UNTREEIFY_THRESHOLD = 6;
    //桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存储Node的数组
    transient Node<K,V>[] table;
    //存储键值对的集合
    transient Set<Map.Entry<K,V>> entrySet;
    //已经存放元素的个数
    transient int size;
    //每次扩容和更改map结构的计数器
    transient int modCount;
    //临界值
    int threshold;
    //加载因子
    final float loadFactor;
 	...
}
Node是什么?

它实现了Entry接口
Node的field:

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...

其中有个Node类型的next,field,next为Node,Node的next又为Node,没错这就是链表怎么构成的原因。

如果你认真看了上面一大坨字段的话,那下面这个图估计一看就明白,看看HashMap是这么将数组和链表组合起来的:在这里插入图片描述
这个红黑树节点总数肯定是大于6的,我不画了,因为在画,图片就看不清了。

为什么TreeNode能放到Node数组里的呢?因为TreeNode是最终还是继承Node的呀,看下结构吧:

static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<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;
...

先不讲为什么需要链表,

我们都知道,hashMap是以key来进行存取的,key不能重复,也就是说你再存一个重复key的Node,将会覆盖相同key的Node。

hashMap是通过key查找值,最理想的时间复杂度为O(1),那哪个容器查找时时间复杂度能达到O(1)呢?没错就是数组,也就是说我们时用key时对应数组的索引的。下面我们看一下它是这么进行对应的:
hashCode():

先不直接贴这个方法源码,咱们应该从头追踪进去,这样才能更好验证我们所说的

我们先看存数据时的逻辑:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

里边调用了hash(key),

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

key为空时直接返回0即对应数组的下标0位置,否则进行hash运算,这里只需知道通过hash算法我们的到了对应数组的下标位置,hash算法越好,元素再数组的分布越均匀,但是也难免会有冲突(即算出来的下标位置上已经有元素了),至于HashMap里的hash算法的详细逻辑就需要另起一篇幅讲了,不然文章就很长了。

那现在已经拿到了下标位置我们看看咱们put进去的逻辑:

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就是Node数组
    //如果为空或者长度为0,就执行resize(),就是对
    //Node数组的创建或者扩容
    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;
        //如果发现key相等。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //将元素赋值给e
            e = p;
        //key不相等
        //如果该位置的元素是TreeNode,说明是个红黑树,那就得用红黑树的插入方法进行插入
        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;
                }
                //如果发现key值相等,则退出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果中找到key值、hash值与插入元素相等的结点,则进行替换value
        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;
}

if-else很多,但了解后就很简单很简单,就是插入元素时找到下标位置(也就是算出来的hash值),
没有元素则直接插入
有元素(这就是冲突,hashMap对冲突的解决方法就是链表法,所以hashMap是以数组加链表实现的)就对比key,
 key相等  则直接进行value替换后结束,
 不相等
   且只有一个元素,直接往后插入,
   有多个元素的话,
     如果是链表就插入到末尾,
     如果是红黑树就按红黑树插入逻辑进行插入
     (插入链表和红黑数的循环过程也会判断key是否相等,相等则进行value替换后结束),
最后判断是否要扩容。


当然冲突的解决方法也有其他方法,hashMap用的是链表法,
其他注意的方法有:开放定址法、线性探查法、随机探测、线性补偿探测法。
当然你高水平的重写hashCode(),也能尽量避免冲突产生。

扩容resize()

HashMap的容量,默认是16
HashMap的加载因子,默认是0.75
当HashMap中元素数超过容量*加载因子时,HashMap会进行扩容
HashMap扩容的时候,长度会翻倍
hashMap的扩容,需要进行hash的重新分配,耗时耗力尽量避免

final Node<K,V>[] resize() {
      Node<K,V>[] oldTab = table;
      int oldCap = (oldTab == null) ? 0 : oldTab.length;
      int oldThr = threshold;
      int newCap, newThr = 0;
      if (oldCap > 0) {
      	  //如果数组长度达到MAXIMUM_CAPACITY,就不在进行扩容	
          if (oldCap >= MAXIMUM_CAPACITY) {
              threshold = Integer.MAX_VALUE;
              return oldTab;
          }
          //否则如果数组长度大于16小于MAXIMUM_CAPACITY,
          //则阈值扩大一倍,注意再判断时也进行了容器的扩大一倍
          else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                   oldCap >= DEFAULT_INITIAL_CAPACITY)
              newThr = oldThr << 1; // double threshold
      }
      else if (oldThr > 0) // initial capacity was placed in threshold
          newCap = oldThr;
      else {               // zero initial threshold signifies using defaults
          newCap = DEFAULT_INITIAL_CAPACITY;
          newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
      }
      //如果新阈值为零,重新计算阈值
      if (newThr == 0) {
          float ft = (float)newCap * loadFactor;
          newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
      }
      //赋值
      threshold = newThr;
      @SuppressWarnings({"rawtypes","unchecked"})
      //创建新数组
          Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
      table = newTab;
      //循环的把老数组搬到新数组
      if (oldTab != null) {
          for (int j = 0; j < oldCap; ++j) {
              Node<K,V> e;
              if ((e = oldTab[j]) != null) {
                  oldTab[j] = null;
                  //如果该索引位置只有一个元素,进行重新hash,计算位置,然后插入
                  if (e.next == null)
                      newTab[e.hash & (newCap - 1)] = e;
                  //如果有多个元素且是红黑数
                  //那就转换成链表后重新循环
                  else if (e instanceof TreeNode)
                      ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);		  
                  //如果遇到了链表
                  else { // preserve order
                      Node<K,V> loHead = null, loTail = null;
                      Node<K,V> hiHead = null, hiTail = null;
                      Node<K,V> next;
                      //内部进行链表的循环,重新hash、放置
                      do {
                          next = e.next;
                          //运算后的hash值进行和oldCap与运算
                          if ((e.hash & oldCap) == 0) {
                          //把节点放到原位置的链表里
                              if (loTail == null)
                                  loHead = e;
                              else
                                  loTail.next = e;
                              loTail = e;
                          }
                          else {
                          //把节点放到原位置+原数组大小的索引位置的链表里
                              if (hiTail == null)					
                                  hiHead = e;
                              else
                                  hiTail.next = e;
                              hiTail = e;
                          }
                      } while ((e = next) != null);				
                      //把链表放到原位置
                      if (loTail != null) {
                          loTail.next = null;
                          newTab[j] = loHead;
                      }
                      //把链表放到原位置+原数组大小的索引位置
                      if (hiTail != null) {
                          hiTail.next = null;
                          newTab[j + oldCap] = hiHead;
                      }
                  }
              }
          }
      }
      return newTab;
  }

看注释应该能理解差不多,我讲讲这个搬运是个神马逻辑:
首先我们看到每次进行搬运都会进行与运算,干嘛用的呢?
其实就是搬运时,会进行hash值与老容量的对比,hash值比老容量小则位置不变,hash值比老容量大,那就得换位置了,新位置=老位置+老容量;

get(key)就不分析了

那我们看看remove(key)是怎么移除的

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

同样进行了hash计算索引位置

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        //如果第一个元素就是要删的
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        //如果第一个不是要删的而且该元素有下一个节点
        else if ((e = p.next) != null) {
        	//判断是否为红黑树,是就跑到用红黑树逻辑查找
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
            	//查链表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //以上都是查找然后node=找到的元素
        //下面开删,链表删除逻辑咱就不讲了
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

咱们就重点看看((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);这句

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
          	...
            if (root.parent != null)
            	//找到根节点
                root = root.root();
            //只看这句,因为用根节点来计算红黑树节点数是否少于6
            if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
            ...
        }

里面做了很多事比如删除,平衡红黑树等,咱就不讲了。我们只看贴出来的一段代码。
因为红黑树是平衡的,所以只需用根节点就可以知道节点总数是否小于6,小于6就执行untreeify(),把树转为链表。
详细关于hashMap的红黑树操作可看
年轻的叔叔:
https://blog.csdn.net/sun112233445/article/details/103350026

到这儿,估计大伙对HashMap有一定理解了把,谢谢大家阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值