关于java容器的问题
1.HashMap 和 Hashtable 有什么区别?
HashMap和HashTable区别主要有以下几点:
1.线程的安全性。HashTable的大部分方法都使用了Synchronize同步锁,使得HashTable在多线程环境下变得安全,而HashMap则不是。
2.效率。HashTable因为加了同步锁导致效率变低。
3.存值。HashMap可以存储k-v,null-null,k-null,null-k(只能有一个k值为null),而HashTable只能存储k-v。
4.扩容方式。HashMap初始容量为16,扩容方式为2n,HashTable初始容量为11,扩容方式为2n+1。
2.ArrayList 和 LinkedList 的区别是什么?
ArrayList 和 LinkedList 的区别主要有以下几点:
1.底层数据结构不同。ArrayList底层为动态数组,LinkedList底层为双向链表。
2.针对场景不同。ArrayList适用于查询多,增删少的场景,LinkedList相反。
3.Iterator 怎么使用?有什么特点?
Collection下的子类都实现了这个iterator()方法,可以用对象直接调用。
它的特点主要有两个,1.使用Iterator遍历集合时,不允许对通过集合对象修改集合内元素,否则抛出异常;2.使用Iterator 遍历集合时,可以用迭代器的remove方法移除元素。
4.Iterator 和 ListIterator 有什么区别?
主要有以下几点:
1.ListIterator继承了Iterator 接口,额外添加了一些方法,比如添加一个元素,替换一个元素,获取前面或后面元素的索引。
2.Iterator 可以遍历List和Set两种集合,ListIterator 只能遍历List。
3.Iterator 只能单向遍历,ListIterator 可以双向遍历。
5.说一下 HashMap 的实现原理?
HashMap底层的数据结构是数组和链表/红黑树,其中每个元素都是链表,通过.put()和.get()进行存取值,
首先,当把key值传入时,会进行.hashcode()计算出key的哈希值,然后根据数组长度(容量)n计算出数组下标,index = (n - 1) & hash,然后将元素放入,这时会出现两种情况,第一种是数组的该位置没有存值,这时直接插入即可,第二种是出现哈希冲突,该位置有值,这时就要将传入的key和该位置的key值进行equals比较,比较的结果若相同,则将传入的value存入覆盖掉原先的值,比较的结果若不相同,则在该位置的链表上插入,1.7进行头插,1.8尾插;1.7会在插入前判断扩容,1.8会在插入后进行判断扩容;1.8会在链表大于8时将链表转化为红黑树。
用.get()取值时,先将传入的key计算哈希值,计算出对应的数组下标得到该key在数组中的位置,然后equals比较两key值是否相等,相等取出该value,不相等则取链表上后续元素的key进行比较,直到相等返回,或者没有相等的返回null。
6.你知道 hash 的实现吗?为什么要这样实现?
在 JDK 1.8 的实现中,是通过 hashCode() 的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)。
主要是从速度、功效、质量 来考虑的,这么做可以在桶的 n 比较小的时候,保证高低 bit 都参与到 hash 的计算中,同时位运算不会有太大的开销。(不是太理解,需要后续学习补充)
7.说说你对红黑树的见解?
8.HashMap 的 table 的容量如何确定?初始化容量的时候能随意指定容量大小吗,它实际是怎样的?loadFactor 是什么,它的值为什么定为这样?容量如何变化?这种变化会带来什么问题?
1.table的容量由capacity参数确定,默认的初始容量是16,最大值为1<<30。
2.可以随意指定容量大小,但是HashMap回对输入容量进行调整,2的n-1次方 < capacity < 2的n次方。(负载因子太小了浪费空间并且会发生更多次数的resize,太大了哈希冲突增加会导致性能不好,所以0.75只是一个折中的选择)
3.loadFactor代表负载因子,主要用于确定map扩容的阈值,比如,默认的初始容量为16,扩容阈值为12,当hashMap的实际存储元素数量size达到12时,进行扩容。
4.容量变化的方式为扩大为原来的两倍。
5.容量的变化会带来性能的影响,当性能要求比较高时,这种性能损失比较致命。
9.拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?为什么链表长度超过8时转化成红黑树?
1.因为二叉树可能会出现特殊的情况,即二叉树形成线性结构,和链表结构一样,所以不能用二叉树,而自平衡的红黑树不会出现这种情况。
2.空间上的浪费。这个问题在源码注释中有解释过,主要因为单个 TreeNode 需要占用的空间大约是普通 Node 的两倍。如果 hash 计算的结果离散好的话,红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。
3.体现了时间和空间上的平衡。在理想情况下,链表长度符合泊松分布,当长度为 8 的时候,概率仅为 0.00000006,小于千万分之一,小于等于8时链表和红黑树查询的时间相差不多,同时空间上不会有太多负担,所以把长度 8 作为转化的默认阈值。
10.数组扩容的过程?
在扩容时,会先新建一个新容量为原来容量两倍的数组,并重新计算旧数组节点的存储位置,节点在新数组中的位置只有两种,原下标或者原下标+旧数组大小。
11.jdk8中对HashMap做了哪些改变?
1.数据结构变了。1.7中数据结构为数组+链表,1.8为数组+链表+红黑树,当链表长度大于8时,会将链表转化为红黑树,小于6时,将红黑树转化成链表。
2.链表的插入方式变了。1.7中的插入方式为头插法,1.8为尾插法。
3.Entry被Node替代。
12.怎么确保一个集合不能被修改?
使用Collections的unmodifiableCollection方法将集合变为不可修改的。
13.Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?
java.util.concurrent并发包下的ConcurrentHashMap。同样的线程安全,ConcurrentHashMap1.7使用的是分段锁,1.8使用的是CAS(无锁算法)+ synchronized,HashTable使用的是Synchronized同步锁,前者将Map中的某个Segment(1.7)或者Node(1.8)锁住,后者将整个map锁住,效率上存在明显的差距。
14.针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?
- 1.7中使用的是分段锁,Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶,每个桶即为数组的一个元素,这个元素为链表,包含多个HashEntry(键值对),每次锁的时候是锁住某segment对象,即锁住了数组上若干个元素,颗粒度是Segment。
- 1.8中使用的CAS + Synchronized锁,取消了segment,上锁时锁住的是某个数组的元素,即链表中的首个Node,颗粒度是Node。
15.ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
1.粒度降低了;
2.JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
3.在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。
16.ConcurrentHashMap 的并发度是什么?
程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)。
17.HashMap 容量为什么总是为 2 的次幂?
1.取代%,&比%快很多。
2.HashMap将键值对放入索引时,采用的是&,hash&(n-1),如果容量n不是2的次幂,是奇数的话,计算结果末尾位都会是0,比如,hash值是 1111,容量n=16,即 10000,n-1= 01111,结果等于01111 ,15;将容量换成15,其他不变,n = 01111 ,n-1 = 01110,计算结果为 01110,末尾永远是0,这样会导致哈希分布不均匀,增加了碰撞几率,减慢了查询效率,还有空间的浪费。