转自:微信公众号-Java3y 2021-03-02
讲讲Map,JDK1.8的就好
Map在Java里是一个接口,常见的实现类包括HashMap,LinkedHashMap,TreeMap
和ConcurrentHashMap。
在Java里边,哈希表的实现由数组+链表组成,
HashMap底层数据结构是数组+链表/红黑树;
LinkedHashMap底层数据结构是数组+链表+双向链表;
TreeMap底层数据结构是红黑树;
而ConcurrentHashMap底层数据结构是数组+链表/红黑树。
嗯…我们从HashMap开始讲起吧,你能讲讲当new一个HashMap的时候,会发生什么吗?
HashMap有几个构造方法,但最主要的就是指定初始值的和负载因子的的大小。
如果我们不指定,默认的HashMap的大小为16,负载因子大小为0.75。
还有就是:HashMap的大小只能是2次幂的,假设你传一个10进去,实际上HashMap的大小
是16,你传一个7进去,HashMap的大小是8,具体的实现在tableSizeFor可以看到。
我们把元素放进HashMap的时候,需要算出这个元素所在的位置(hash),在HashMap里
用的是位运算来代替取模,能够更加高速地算出该元素所在的位置。
为什么HashMap的大小只能是2次幂,因为只有大小为2次幂时,才能合理的运用位运算
代替取模,而负载因子的大小决定着哈希表的扩容和哈希冲突。
比如现在我默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放12个
元素,一旦超出12个元素就说明需要扩容。
怎么算出是12呢,简单说就是16*0.75,每次put进元素的时候,都会检查HashMap的大小
有没有超过这个阈值,如果有,则需要扩容。
鉴于上面的说法,HashMap的大小只能是2次幂,所以扩容默认是原来的2倍。还有就是
扩容这个操作肯定是耗时的,那能不能把负载因子调的高一点,比如调为1,
那我的HashMap就等到16个元素的时候才开始扩容呢。
当然是可以的,但是不推荐,负载因子调高了,这意味着哈希冲突的概率会增高,
哈希冲突的概率增高,同样会耗时,因为查找的速度变慢了。
我继续问下,put元素的时候,传递的key是怎么计算哈希值的?
实现就在hash方法上,可以发现的是,他是先算出正常的哈希值,
然后与高16位做异或运算,产生最终的哈希值。
这样的好处可以增加了随机性,减少了碰撞冲突的可能性。
你再简单说下put方法和get方法的实现吧
put的时候首先对key做hash运算,计算出该key所在的index,
如果没碰撞,直接放到数组中,
如果碰撞了,要判断当前的数据结构是链表还是红黑树,根据不同的情况来进行插入。
假设key是相同的,则替换原来的值,最后判断哈希表是否满了
(当前哈希表大小*负载因子),如果满了,则扩容。
在get的时候,还是对key做hash运算,计算出该key所在的index,
然后判断是否有hash冲突,假设没有hash冲突直接返回,
假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。
那在HashMap中是怎么判断一个元素是否相同的呢
这么简单都问。首先比较hash值,如果hash值相同说明该元素哈希冲突了,
随后用==运算符和equals()判断该元素是否相同,
如果都相同说明是同一个元素。
你说HashMap的数据结构是数组+链表/红黑树,那什么情况才会用到红黑树呢
当数组的大小大于64且链表大小大于8的时候才会将链表改为红黑树,
当红黑树大小为6时,会退化为链表。
这里红黑树转为链表的操作主要是由于查询和插入时对性能的考量。
链表查询时间复杂度为O(n),插入时间复杂度O(1),
红黑树的查询和插入时间复杂度O(logN)。
你在日常工作中使用LinkedHashMap用的多吗
不多,在前边提到了,LinkedHashMap底层结构是数组+链表+双向链表,
实际上它继承了HashMap,在HashMap的基础上维护了一个双向链表。
有了这个双向链表,我们的插入可以说是有序的,
这里的有序不是指大小有序,而是插入有序。
LinkedHashMap在遍历的时候实际上使用的是双向链表来遍历的,
所以LinkedHashMap的大小不会影响到遍历的性能。
那TreeMap呢
TreeMap在实际开发中使用的也不多,TreeMap的底层结构是红黑树,
TreeMap的key不能为null,如果为null,那还怎么排序呢,
TreeMap的有序是通过Comparator来进行比较的,
如果comparator为null,那就使用自然顺序。
再来讲讲线程安全的Map吧,HashMap是线程安全的吗
HashMap不是线程安全的,在多线程环境下,HashMap有可能
有数据丢失和获取不了最新数据的问题,
比如说:线程A put进去了,现场B get不出来,我们想要线程安全,
一般使用ConcurrentHashMap。
ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。
线程安全的Map实现类除了ConcurrentHashMap还有一个HashTable。
当然了,也可以使用Collections来包装出一个线程安全的Map。
但无论是HashTable还是Collections包装的都比较低效,
因为他们是直接在外层套synchronize,
所以我们一般有线程安全问题都考虑ConcurrentHashMap。
ConcurrentHashMap的底层结构是数组+链表/红黑树,
他能支持高并发的访问和更新,是线程安全的。
ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步,
在get的时候没有加锁,Node都用了volatile。
在扩容时,会给每个线程分配对应的区间,并且为了防止putVal导致数据不一致,
会给线程所负责的区间加锁。
能给我讲讲JDK7和JDK8中的HashMap和ConcurrentHashMap的区别吗
不能,我不会,你不是说好的是JDK8吗,
其实学习的时候看过JDK7的这两个,还有有一些不一样的地方,
比如JDK7的HashMap是头插法,JDK8变成了尾插法,在JDK7还没引入红黑树。。。
你这很危险,这都不会
说什么呢,危险个锤子
拜拜