java学习_part01_java核心卷_day06_HashMap

参考:

https://zhuanlan.zhihu.com/p/79219960

https://zhuanlan.zhihu.com/p/21673805

1. 数据结构内部属性

1.1 存储数据结构

  • jdk1.8以前:数组+链表(增删快,查询慢)

  • jdk1.8开始:数组+链表+红黑树(增删快,查询也快)

注:

  • 只有在链表长度大于等于8,数组长度大于等于64,才会把链表转化为红黑树,红黑树节点数小于6时,又变回链表
  • 频繁扩容会导致红黑树进行拆分和重组,比较耗时,因此只在链表长度较长时候转化成红黑树才会显著提高效率

1.2 静态内部类Node

哈希桶数组Node[] table,实现了Map.Entry<K, V>接口,本质是一个键值对

属性:

  • final int hash:存储key的hashcode经过扰动函数以后的hash值
  • final K key
  • V value
  • Node<K, V> next:存储下一个节点的索引

方法:

  • hashCode():键的哈希值与值的哈希值进行异或运算的结果

1.3 属性

  • 序列化值
  • 默认数组容量:16,初始化时机为第一次调用put方法,进一步调用resize方法时候执行
  • 最大容量:1 <<30
  • 默认负载因子:0.75
  • 树化阈值:8,即链表添加元素后,该条链表长度如果大于等于8,且数组容量大于等于64,则树化,如果数组容量小于64,则扩容
  • 反树化阈值:6,即红黑树节点数小于6,则转变成链表
  • Node<K, V> table:初始化的数组用于存放Node
  • int size:数组中实际键值对映射数目(即添加一个Node节点就+1)
  • int threshold:扩容阈值,默认为数组容量*负载因子,哈希表中size大于该值就会扩容

2. 方法

2.1 构造方法

  • public HashMap(int initialCapacity)
  • pblic HashMap()
  • public HashMap(Map<? extends K, ? extends V> m)
  • pblic HashMap(int initialCapacity, float loadFactor):
    1. 如果初始化容量小于0,抛出异常
    2. 如果初始化容量大于最大值,则设置为最大值1<<30
    3. 如果负载因子不大于0或不是浮点数,抛出异常
    4. 否则,设置负载因子,设置初始化容量(大于等于传入参数的最小2次幂数)

2.2 tableSizeFor方法理解

参考:

https://www.cnblogs.com/loading4/p/6239441.html

求不小于传入参数的最小二次幂

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

假定n为:

0b01xx xxxx xxxx xxxx...,右移1位,异或,得到

0b011x xxxx xxxx xxxx...,右移2位,异或,得到

0b0111 1xxx xxxx xxxx...,右移4位,异或,得到

0b0111 1111 1xxx xxxx...,右移一位,异或,得到

最后可以把最高位以后的数全变为1

2.2 put

  1. 调用put传入键值对,实际调用putVal(hash(key), key, value, false, true)

  2. 调用hash方法(扰动函数),对键的hashcode进行二次操作(经过高低位异或操作)得到hash值,构建Node对象{hash=hash值, k, v, next=null},对(数组长度-1)进行取余数操作,得到在数组中的位置,

    • hash方法(扰动函数)代码:

      static final int hash(Object key){
          int h;
          return(key == null ? :0 :(h=key.hashCode() ^ (h >>> 16)));
      }
      
    • 结果取余数运算,就是与数组长度-1进行与运算,位运算效率高:

      在数组中位置 = node.hash & (table.length() - 1)
      
  3. 如果未发生冲突,直接存放

  4. 如果发生了冲突,判断当前位置数据key是否相等,相等则覆盖,否则判断当前节点数据结构:

    • 红黑树:存入红黑树节点

    • 链表:判断当前键在链表中是否存在,存在就覆盖,否则判断插入之后链表大小是否大于8:

      • 否:直接插入在链表末尾
  • 是:判断数组大小是否大于64:

    • 是:转化为红黑树
      • 否:扩容
  1. 补充

    • 为什么不直接用key的hashcode,而要用hash求hash值:

      ​ key的hashcode从-20亿到+20亿有40多亿的映射空间,只要哈希函数映射的比较均匀,一般很难出现碰撞.但是40长度数组,内存 放不下.,且需要进行对数组长度-1取模操作,余数才能访问下标

    • 为什么要采用高16位与低16位进行异或操作:

      ​ 扰动函数,高位与低位做异或是为了加大低位的随机性.且保留了高位的特征信息

    • jdk1.8相对于jdk1.7对HashMap的优化:

      • 添加了红黑树结构
      • 头插法变为尾插法
      • 1.7扩容时候需要对原数组中元素进行重新hash定位在新数组位置,1.8采用简单的判断逻辑,位置不变或新位置下标=旧位置+旧数组大小
      • 1.7为先判断是否需要扩容,再插入,1.8为先插入,再判断是否需要扩容

3. 扩容

3.1 扩容时机

  • 空参数的构造方法,实例化的HashMap默认内部数组为null,没有实例化,第一次调用put时开始第一次初始化扩容,长度为16
  • 有参数构造方法:用于指定容量.找到不小于传入参数的最小2次幂数,作为容量,确定扩容阈值=容量*负载因子
  • 其他时候扩容:容量,阈值变为原来两倍

3.2 元素迁移

  • 如果只有一个元素,以(新数组的长度-1)进行求余数确定新位置,实际上会在原地或移动旧数组个位置,与链表元素移动同理

  • 如果当前为链表,举例:

    ​ 假定旧的当前位置为1111,则其节点分为高位节点11111,低位节点01111,低位节点在新数组中的位置不变为01111,高位节点在新数 组中的位置变为11111

  • 如果当前为红黑树:待添加...

4. 其他Map实现类(待补充)

4.1 HashTable(不建议使用,建议使用ConcurrentHashMap)

  • 线程安全
  • 不允许null作为键
  • 初始容量为11,扩容时候为容量翻倍+1
  • 计算hash时候为key的hashcode直接对数组长度取模运算

4.2 ConcurrentHashMap

4.3 TreeMap

posted @ 2020-12-30 18:50  OLeeO97  阅读(52)  评论(0编辑  收藏  举报