标题
描述一下Map put的过程
1、首次扩容:
先判断数组是否为空,若数组为空则进行第一次扩容(resize);
2、计算索引:
根据键值key计算hash,得到键值对在数组中的索引;
3、插入数据:
如果当前位置元素为空,则直接插入数据;
如果当前位置元素非空,且key已存在,则直接覆盖其value;
如果当前位置元素非空,且key不存在,则将数据链到链表末端;
若链表长度>8且当前数组长度>64时,则将链表转换成红黑树,并将数据插入树中;
4、再次扩容
插入成功后,如果数组中元素个数(size)超过threshold(数组长度*0.75),则再次进行扩容操作(扩容为原来的两倍)
链表长度>=8且当前数组长度小于64时,也会进行扩容。
介绍一下HashMap的扩容机制
补充:
如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置;
a:统计每个下标位置的元素个数
b:如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置
c:如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
hashMap的寻址算法
为何HashMap的数组长度一定是2的次幂
1、计算索引时效率更高:如果是2的n次幂可以使用位与运算代替取模
97%16=1 (16-1)&97=1
99%32=3 (32-1)&99=3
2、扩容时重新计算索引效率更高:hash&oldCap == 0的元素留在原来的位置,否则新位置=旧位置 + oldCap
HashMap为什么用红黑树而不用B树?
B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。
HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。
说一说TreeSet和HashSet的区别
HashSet、TreeSet中的元素都是不能重复的,并且它们都是线程不安全的,二者的区别是:
-
HashSet中的元素可以是null,但TreeSet中的元素不能是null;
-
HashSet不能保证元素的排列顺序,而TreeSet支持自然排序、定制排序两种排序的方式;
-
HashSet底层是采用哈希表实现的,而TreeSet底层是采用红黑树实现的。
注意:HashSet是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
HashMap 是怎么解决哈希冲突的?
P63
要了解Hash冲突,那首先我们要先了解Hash算法和Hash表。
Hash 算法,就是把任意长度的输入,通过散列算法,变成固定长度的输出,这
个输出结果是散列值。
Hash 表又叫做“散列表”,它是通过key直接访问在内存存储位置的数据结构,
在具体实现上,我们通过hash函数把key映射到表中的某个位置,来获取这个
位置的数据,从而加快查找速度。
所谓hash冲突,是由于哈希算法被计算的数据是无限的,而计算后的结果范围
有限,所以总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。
①开放定址法,也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的
次序从hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲
位置中。ThreadLocal就用到了线性探测法来解决hash冲突的。
②链式寻址法,这是一种非常常见的方法,简单理解就是把存在hash冲突的key,以单向链表的方式来存储,比如HashMap就是采用链式寻址法来实现的。像这样一种情况,存在冲突的key直接以单向链表的方式进行存储。
③再hash法,就是当通过某个hash函数计算的key存在冲突时,再用另外一个hash 函数对这个key做hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。
HashMap在JDK1.8版本中,通过链式寻址法+红黑树的方式来解决hash冲突问题,其中红黑树是为了优化Hash表链表过长导致时间复杂度增加的问题。当链表长度大于8并且hash表的容量大于64的时候,再向链表中添加元素就会触发转化。
HashMap补充:
hash(key)方法计算hash值
计算方法是键的hashCode()方法与高位16进行异或运算得到hash值
将元素存储在i = (n - 1) & hash的下标链表中
ArrayList和LinkedList有什么区别?
1、ArrayList的实现是基于数组,LinkedList的实现是基于双向链表;
2、对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N);
3、对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引;
4、LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
ArrayList的实现原理
1、ArrayList底层是用动态数组实现的
2、ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
3、ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组(Arrays.copyOf 方法)
4、ArrayList在添加数据的时候
- 确保数组已使用长度(size)+1后足够存下下一个数据
- 计算数组容量,如果当前数组已经使用长度+1后大于当前数组长度,则调用grow方法进行扩容。
- 确保新增的数据有地方存储之后,则将新元素添加到size位置上
- 返回添加成功布尔值
ArrayList list = new ArrayList(10)扩容几次
声明和实例了一个ArrayList,指定了容量为10,未扩容。
如何实现数组和List之间的转换
说一说你对LinkedHashMap的理解
LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。
如何得到一个线程安全的Map?
- 使用Collections工具类,将线程不安全的Map包装成线程安全的Map;
- 使用java.util.concurrent包下的Map,如ConcurrentHashMap;
HashMap与HashTable区别
- 从功能特性的角度来说HashTable是线程安全的,而HashMap不是。
- HashMap的性能要比HashTable更好,因为,HashTable采用了全局同步锁来保证安全性,对性能影响较大
- 从内部实现的角度来说HashTable使用数组加链表、HashMap采用了数组+链表+红黑树。
- HashMap初始容量是16、HashTable初始容量是11
- HashMap 可以使用null作为key,HashMap会把null转化为0进行存储,而Hashtable 不允许。
- 最后,他们两个的key的散列算法不同,HashTable直接是使用key的hashcode对数组长度做取模。而HashMap对key的hashcode做了二次散列,从而避免key的分布不均匀问题影响到查询性能。
说一说HashSet的底层结构
HashSet是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。