任意长度的输入->固定长度的输出
抽屉原理 无法避免
要尽可能的分散,因为在table中slot大部分都处于空闲状态时,要尽可能降低hash冲突
jdk8为例 数组+链表+红黑树
每个数据单元都是一个node结构
node结构中有key、value、hash、next
next字段就是发生hash冲突的时候,当前桶位中的node与冲突的node连成一个链表要用的字段
16
jdk7
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
jdk8
默认为0.75,负载因子作用是计算扩容阈值用的,比如说使用无参构造方法创建的hashmap对象,默认情况下,扩容阈值就是16*0.75 =12
有两个指标,一个是链表长度达到8,还有一个是当前散列表数组长度已经达到64,否则的话就算slot内部链表长度已经达到8,它也不会转树,它仅仅会发生一次resize,散列表扩容
不是的,是h=key.hashcode() 而成加工得到的
hash=h^(h>>>16)
第一点,散列表的长度必须是2的次方数,比如16,32,64....
寻址算法是hash&(table.length-1)
(n-1) 与hash 会更加散列
如果用n与hash 会只有两种结果
采用高低位异或,主要是优化hash算法
因为hashmap散列表大多数情况下,它不会特别大
也就是说table.length-1得到的二进制数,实际有效位很有限
一般都在低16位内,这样的话,hash值高16位就等于完全浪费了,没起到作用
一共四种情况把
第一种 slot==null
把当前put方法传进来的key和value包装成一个node对象,放入slot就可以
第二种 slot!=null 并且它引用的node还没有链化
需要先对比node对象的key和当前put对象的key是否完全相等,如果完全相等,这个操作就是替换操作。否则的话就是hash冲突了,然后在slot->node后面追加一个node就可以了,采用尾插法。
第三种 slot内的node已经链化
迭代查找node,需要先对比node对象的key和当前put对象的key是否完全相等,如果完全相等,这个操作就是替换操作。否则的话就是hash冲突了,然后在slot->node后面追加一个node就可以了,采用尾插法。还需要检查一下链表长度,有没有达到树化阈值。达到的话调用一个树化方法
第四种 冲突很严重的情况下,链已经转化为红黑树了
先从treenode说起,它继承了node结构,在node基础上增加了几个字段,指向父节点parent,指向左子节点、指向右子节点的right.红黑树的插入操作,首先是找到一个合适的插入点,就是找到插入节点的父节点,红黑树满足二叉树排序树的所有特性,这个找父节点的操作和二叉排序树完全一致,每次向下查找一层就可以排除一半的数据,左子树或者右子树为null,说明整个树中,它没有发现node-key与当前put-key一致的treenode,此时探测节点就是插入父节点所在了,然后当前插入节点插入到父节点的左子树或者右子树,然后根据这个插入节点的hash值和父节点hash值大小决定左右的,插入会打破平衡,还需要一个红黑树的平衡算法。第二种情况是向下探测过程中 发现node-key与当前put-key一致的treenode,这届repalce
构成树的节点有一个颜色属性,要么黑,要么红
第二个 根节点必须是黑色的
第三条 从叶节点到根节点的路径上,它每条路径黑色节点它一定一致,俗称黑高
第四条 不能有两个红色节点相连,也可以推导出两个子节点一定是黑色节点
叶子节点一定是黑色的 (叶子节点=NIL节点)
还有一点,一定是红插,就是新的节点一定是红色的,红插的话有优势,碰到父节点是黑色的话,树不会失衡,如果是黑插,就一定会失衡(树违法红黑树规则)
主要解决hash冲突导致链化严重的问题
jdk7的时候,当这个slot链化非常严重的时候,影响get查询效率,本身散列表最理想的查询效率为
o(1),链化严重会导致查询退化为o(n),所以才引入红黑树,一颗特殊的二叉排序树,
因为链表毕竟不是数组,它从内存角度来看,它没有连续着,查询的时候,如果在某位,必须一个一个next 过去,非常耗费性能
写数据之后,可能会触发扩容,hashmap结构内,有个记录当前数据量的字段。数据量字段达到扩容阈值的话,它就会触发扩容
扩容规则是这样的,因为table数组长度必须是2的次方数,扩容其实是,每次都是按照上一次tablesize位移运算得到的,做一次左移运算16<<1=32
主要因为性能吧,因为cpu毕竟它不支持乘法运算,所有的乘法运算最终都是在指令层面为了加法实现的,效率很低,如果用位运算的话对cpu来说就非常的简单高效,效率很高。
迁移其实就是,挨个桶位推进迁移,一个桶位一个桶位的处理,主要还是看当前处理桶位的数据状态吧
大概分了四种状态:
一共四种情况把
第一种 slot==null
不用处理
第二种 slot!=null 并且它引用的node还没有链化
当迁移的时候发现slot中存储的这个node节点,它的next是null的时候,,说明这个slot还没有发生hash冲突,直接迁移就好了,根据新表的tablesize计算出它在新表的位置,然后存放过去就ok
第三种 slot内的node已经链化
迁移发现它这个next字段,这个node->next字段它不是null,说明这个slot发生过hash冲突,
这个时候需要把当前slot中保存的这个链表,拆成两个链表,分别是高位链和低位链,
所有的node->hash字段转化为二进制后,低位都是相同的,低位指的就是老表的tablesize-1 转化出来的二进制的有效位,如15就是1111,目前这个链表的低位一定是相同的,但是高位不一定,如下key.hashcode&newtable.length-1
元素放到index或index+oldtab.length
第四种 冲突很严重的情况下,链已经转化为红黑树了
红黑树节点对象,这个treenode结构,依然保留了next字段,红黑树结构内部维护链表,查询的时候不用,但是新增或者删除的时候需要维护,这个链表就是说,方便split拆分这个红黑树时去用的,处理和普通的node链表一样,也就是根据高低位拆分成高位链和低位链,高位链要存放到新表,不同的是拆分出来的链表要看一下长度,若小于等于6,直接将treenode转化为普通的node链表,然后放到扩容后的新表就可以了,如果拆分出来的链表>6,还是需要把链表升级成红黑树的(其实就是重建红黑树)