目录
14、HashMap和ConcurrentHashMap的区别?
1、底层数据结构:
1、JDK1.7及之前HashMap底层采用数组+链表的结构,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
2、JDK1.8把它设计成达到一个特定的阈值(默认为8)之后,就将链表转成红黑树。
3、不是用红黑树来管理hashmap,而是在hash值相同的情况下(而重复数量大于8),用红黑树来管理数据。红黑树相当于排序数据可以自动的使用二分法进行定位。
4、当个数不多的时候,直接链表遍历更方便,实现起来也简单。而红黑树的实现要复杂得多。因为红黑树需要进行左旋,右旋操作,而单链表不需要,以下都是单链表与红黑树结构对比。
2、常用变量
(1)HashMap的默认初始容量为16,容量必须是2的N次方。最大容量为2^30。(计算索引位置的公式:(n-1)&hash,当n为2的n次方时,n-1为低位全是1的值,此时任何值跟n-1进行&运算的结果为该值的低N位,达到了和取模同样的效果,实现了均匀分布。)
(2)默认加载因子0.75。扩容阈值 = 容量 * 负载因子
(3)当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树,当红黑树上的元素减少到6个时就会退化为链表。
3、哈希算法
把任意长度值(key)通过散列算法变换成固定的key(地址),通过这个地址进行访问的数据结构。
4、HashCode
通过字符串(key)算出它的ASCII码,进行mod(取模),算出哈希表的下标。
5、HashMap的原理
HashMap基于hashing原理,我们通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会存储在链表的下一个节点中。HashMap在每个链表节点中存储键值对对象。
当两个不同的输入值,根据同一散列值的现象,我们就把它叫做碰撞(哈希碰撞、哈希冲突)。
HashMap扩容原理:
(1)扩容:创建一个新的entry空数组,长度是原数组的2倍。
(2)Rehash:遍历原entry数组,把所有entry重新hash到新数组内。
6、为什么1.8中引入红黑树?
当我们的HashMap中存在大量数据时,加入我们mougebucket下对应的链表有n个元素,那么遍历时间复杂度就为0(n),为了针对这个问题,JDK1.8在HashMap中新增加了红黑树的数据结构,进一步使得遍历复杂度降低至0(logn);
总结一下HashMap是使用了哪些方法来有效解决哈希冲突:
1、使用链地址法(使用散列表)来链接拥有相同hash值的数据
2、使用二次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均
3、引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
7、为什么HashMap链表会形成死循环?
因为resize的赋值,单线程不会出现这种情况,多线程情况下resize会出现线程堵塞方式,线程1准备处理节点,线程2把HashMap扩容成功,此时链表已经逆向排序,那么线程1在处理节点时就可能会出现环形链表。
因为JDK1.7是采用头插法,在多线程环境下有可能会使链表形成环状,从而导致死循环。JDK1.8做了改进,用的是尾插法,不会产生死循环。
8、HashMap线程安全吗?
不安全,HashMap在并发下存在数据覆盖、遍历的同时进行修改会抛出ConcurrentModificationException异常等问题,JDK1.8之前还会出现死循环等问题。
9、JDK1.7,Put方法与Get方法
Put: 1、判断当前数组是否需要初始化
2、如果key为空,则put一个空值进去
3、根据key计算出hashcode
4、根据计算出的Hashcode定位出所在桶
5、如果同是一个链表则需要遍历判断里面的hashcode、key是否和传入key相等,如果相等则进行覆盖,并返回原来的值。
6、如果同时空的,说明当前位置没有数据存入;新增一个Entry对象写入当前位置
Get:1、首先根据key计算出hashcode,然后定位到具体的桶中
2、判断该位置是否为链表
3、不是链表就根据key、key的hashcode是否相等来返回值
4、为链表则需要遍历直到key及hashcode相等时返回值
5、啥都没取到就直接返回null
10、JDK1.8,Put方法与Get方法
Put:1、判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
2、根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可
3、如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
4、如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
5、如果是个链表,就需要将当前的key、value封装成一个新节点写入到当前桶的后面
6、接着判断当前链表的大小是否大于预设的阈值,大于就要转换成红黑树
7、如果在遍历过程中找到key相同时直接退出遍历
8、如果 e != null 就相当于存在相同的key,那就需要将值覆盖
9、最后判断是否需要进行扩容
Get:1、首先将key值hash之后取得所定位的桶。
2、如果桶为空则直接返回null
3、否则判断桶的第一个位置(有可能是链表、红黑树)的key是否为查询的key,时就直接返回value。
4、如果第一个不匹配,则判断它的下一个是红黑树还是链表
5、红黑树就按照树的查找方式返回值
6、不然就按照链表的方式遍历匹配返回值
11、HashMap的插入流程
12、HashMap与Hashtable的区别
相同点:都是实现Map接口(hashTable还实现了Dictionary抽象类)
不同点: 1、Hashtable继承Dictionary,HashMap继承的是Java1.2出现的Map接口
2、HashMap去掉了Hashtable的contains方法,但是加上了containsValue和containsKey方法。
3、HashMap允许空键值,而Hashtable不允许
4、HashTable是同步的,而HashMap是非同步的,效率上比HashTable要高。也就是说HashMap更适合于单线程环境,而HashTable适合于多线程环境。
5、HashMap的迭代器(Iterator)是fail-fast迭代器,HashTable的enumerator迭代器不是fail-fast的。
6、HashTable中数组默认大小是11,扩容方法是old * 2+1,HashMap默认大小是16,扩容每次为2的指数大小。
一般不建议使用HashTable。主要原因两点:
1、HashTable是遗留类,内部实现很多没优化和冗余。
2、即使在多线程环境下,现在也有同步的ConcurrentHashMap替代,没有必要因为是多线程而用Hashtable。
13、HashSet和HashMap区别
HashSet底层就是基于HashMap实现的。只不过HashSet里面的HashMap的value都是同一个Object而已,因此HashSet也是非线程安全的。
HashMap | HashSet |
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用sdd()方法向set中添加元素 |
HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
14、HashMap和ConcurrentHashMap的区别?
ConcurrentHashMap是线程安全的HashMap的实现。主要区别如下:
1、ConcurrentHashMap对整个桶数组进行分割分段,然后在每个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好。而HashMap没有锁机制,不是线程安全的。
2、HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
15、HashMap与TreeMap的区别:
1、HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
2、在Map中插入、删除和定位元素,HashMap是最好的选择。但如果您按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和equals()的实现。