视频
数组大于64,或者链表大于8,才会变为红黑树
在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。“头插法”
在JDK1.8 中,由“数组+链表+红黑树”组成。尾插法。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
JDK1.8 后采用数组+链表+红黑树的数据结构。通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算,来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当
HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。
而引入红黑树,就是为了提高查找效率的;因为红黑树是一种自平衡的二叉查找树;
这是HashMap解决查询效率低下的一种方式,还有一种方式是通过负载因子控制的扩容,每次Hash碰撞达到负载因子的时候,就会触发扩容,扩容为之前的两倍,从而大幅提高查找的效率;理论上扩容最高可以提升2倍的效率,但实际很难出现;
因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。
当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。
当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
HashMap用链地址法解决hash冲突,则当链表里的长度太长就会严重影响HashMap的性能。于是在jdk1.8里,对数据结构做了进一步优化,引入了红黑树,当链表长度大于8的时候,链表就会转成红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
简要流程如下:
1.为了数据的均匀分布,减少哈希碰撞。
因为(2的次幂数 - 1)的二进制形式表示都是1,这样在和经过异或运算的h进行按位与运算的时候才可以最多地保留其特性,减少产生哈希碰撞的概率,让数组空间均匀分配。
2.输入数据若不是2的幂,HashMap通过一通位移运算和或运算,得到的肯定是2的幂次数,并且是离那个数最近的数字。
如果两个不同对象的hashCode相同,这种现象称为hash冲突。有以下的方式可以解决哈希冲突:
开放定址法 :就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
链地址法: 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
再哈希法 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
建立公共溢出区 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
而HashMap就是采用链地址法进行解决hash冲突的。
hashMap初始长度就是16,负载因子是0.75。
HashMap所容纳的最大数据量为:长度*负载因子。即当长度达到这个值的时候就会发生扩容。
loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。
扩容(resize)就是重新计算容量,
向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。
当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
底层是resize方法中的transfer方法
,将原有的Entry数组的元素拷贝到新的Entry数组里,扩容都是以2的N次幂进行扩容 一般是2倍。
HashMap是线程不安全的,多个线程同时写HashMap可能会导致数据的不一致。
如果需要满足线程安全可以用ConcurrentHashMap
,还有一个HashTable
。
由于HashMap线程不安全的,至于为何不安全,什么时候会出现问题,这里来讨论一下:
当有多个线程共同操作hashMap的put方法时,这个时候hashMap容量不够了,两个线程都去扩容执行resize方法,在这个时候cpu切换资源的话,会造成链表成环问题,死循环问题。
对key的hashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点。
相同点:都是存储key-value键值对的
不同点:
选择Integer,String这种不可变的类型,像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的,