Java面试---集合篇
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Java⾯试---集合篇
1.什么是迭代器(Iterator)?
Iterator接⼝提供了很多对集合元素进⾏迭代的⽅法。
每⼀个集合类都包含了可以返回迭代器实例的迭代⽅法。
迭代器可以在迭代的过程中删除底层集合的元素,但是不可以直接调⽤集合的 remove(Object Obj)删除,可以通过迭代器的remove()⽅法删除。
2.Iterator和ListIterator的区别是什么?
Iterator可⽤来遍历Set和List集合,但是ListIterator只能⽤来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接⼝,并包含其他的功能,⽐如:增加元素,替换元素,获取前⼀个和后⼀个元素的索引,等等。
3.快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
⼀:快速失败(fail—fast)
在⽤迭代器遍历⼀个集合对象时,如果遍历过程中对集合对象的结构进⾏了修改(增加、删除),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使⽤⼀个 modCount 变量。
集合在被遍历期间如果结构发⽣变化,就会改变modCount的值。
每当迭代器使⽤hashNext()/next()遍历下⼀个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终⽌遍历。
注意:这⾥异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。
如果集合发⽣变化时修改modCount值刚好⼜设置为了expectedmodCount值,则异常不会抛出。
因此,不能依赖于这个异常是否抛出⽽进⾏并发操作的编程,这个异常只建议⽤于检测并发修改的bug。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发⽣并发修改(迭代过程中被修改)。
⼆:安全失败(fail—safe)
采⽤安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,⽽是先复制原有集合内容,在拷贝的集合上进⾏遍历。
原理:由于迭代时是对原集合的拷贝进⾏遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那⼀刻拿到的集合拷贝,在遍历期间原集合发⽣的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使⽤,并发修改。
4.HashMap的底层结构?
4.1HashMap的扩容机制?什么时候扩容?
4.2扩容时要重新计算hashcode值吗?
HashMap数据结构图:entry对象的数组+链表/红⿊树(entry对象即键值对对象)
HashMap数据插⼊原理图:
1.判断数组是否为空,为空进⾏初始化;
2.不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
3.查看 table[index] 是否存在数据,没有数据就构造⼀个Node节点存放在 table[index] 中;
4.存在数据,说明发⽣了hash冲突(存在⼆个节点key的hash值⼀样), 继续判断key是否相等,相等,⽤新的value替换原数据(onlyIfAbsent为false);
5.如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插⼊红⿊树中;(如果当前节点是树型节点证明当前已经是红⿊树了)
6.如果不是树型节点,创建普通Node加⼊链表中;判断链表长度是否⼤于 8并且数组长度⼤于64,⼤于的话链表转换为红⿊树;
7.插⼊完成之后判断当前节点数是否⼤于阈值,如果⼤于开始扩容为原数组的⼆倍。
4.1HashMap是怎么初始化容量的?
⼀般如果new HashMap() 不传值,默认⼤⼩是16,负载因⼦是0.75,如果⾃⼰传⼊初始⼤⼩k,初始化⼤⼩为⼤于k的 2的整数次⽅,例如如果传10,⼤⼩为16
4.2HashMap的哈希函数怎么设计的?为什么?
hash函数是先拿到 key 的hashcode,是⼀个32位的int值,然后让hashcode的⾼16位和低16位进⾏异或操作。
这个也叫扰动函数,这么设计有⼆点原因:
⼀定要尽可能降低hash碰撞,越分散越好;
算法⼀定要尽可能⾼效,因为这是⾼频操作, 因此采⽤位运算;
5.HashMap和Hashtable有什么区别?HashTable是如何保证线程安全的?
HashMap和Hashtable都实现了Map接⼝,因此很多特性⾮常相似。
他们有以下不同点:
HashMap允许键和值是null,⽽Hashtable不允许键或者值是null。
Hashtable是同步的,⽽HashMap不是。
因此,HashMap更适合于单线程环境,⽽Hashtable适合于多线程环境。
HashMap提供了可供应⽤迭代的键的集合,因此,HashMap是快速失败的。
另⼀⽅⾯,Hashtable提供了对键的列举(Enumeration)。
⼀般认为Hashtable是⼀个遗留的类。
6.数组(Array)和列表(ArrayList)有什么区别?什么时候应该使⽤Array⽽不是ArrayList?
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
Array⼤⼩是固定的,ArrayList的⼤⼩是动态变化的。
ArrayList提供了更多的⽅法和特性,⽐如:addAll(),removeAll(),iterator()等等。
对于基本类型数据,集合使⽤⾃动装箱来减少编码⼯作量。
但是,当处理固定⼤⼩的基本数据类型的时候,这种⽅式相对⽐较慢。
7.ArrayList和LinkedList有什么区别?
ArrayList和LinkedList都实现了List接⼝
他们有以下的不同点:
ArrayList是基于索引的数据接⼝,它的底层是数组。
它可以以O(1)时间复杂度对元素进⾏随机访问。
LinkedList是以元素列表的形式存储它的数据,每⼀个元素都和它的前⼀个和后⼀个元素链接在⼀起,在这种情况下,查找某个元素的时间复杂度是O(n)。
相对于ArrayList,LinkedList的插⼊,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算⼤⼩或者是更新索引。
LinkedList⽐ArrayList更占内存,因为LinkedList为每⼀个节点存储了两个引⽤,⼀个指向前⼀个元素,⼀个指向下⼀个元素。
8.你了解⼤O符号(big-O notation)么?你能给出不同数据结构的例⼦么?
⼤O符号描述了当数据结构⾥⾯的元素增加的时候,算法的规模或者是⼀个渐进上界。
⼤O符号也可⽤来描述其他的⾏为,⽐如:内存消耗。
因为集合类实际上是数据结构,我们⼀般使⽤⼤O符号基于时间,内存和性能来选择最好的实现。
⼤O符号可以对⼤量数据的性能给出⼀个很好的说明。
9.如何权衡是使⽤⽆序的数组还是有序的数组?
有序数组最⼤的好处在于查找的时间复杂度是O(log n),⽽⽆序数组是O(n)。
有序数组的缺点是插⼊操作的时间复杂度是O(n),因为值⼤的元素需要往后移动来给新元素腾位置。
相反,⽆序数组的插⼊时间复杂度是常量O(1)。
10.Enumeration接⼝和Iterator接⼝的区别有哪些?
Enumeration速度是Iterator的2倍,同时占⽤更少的内存。
但是,Iterator远远⽐Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合⾥⾯的对象。
同时,Iterator允许调⽤者删除底层集合⾥⾯的元素,这对Enumeration来说是不可能的。
11.HashSet和TreeSet有什么区别?
HashSet是由⼀个hash表来实现的,因此,它的元素是⽆序的。
add(),remove(),contains()⽅法的时间复杂度是O(1)。
TreeSet是由⼀个树形的结构来实现的,它⾥⾯的元素是有序的。
因此,add(),remove(),contains()⽅法的时间复杂度是O(logn)。
12.你知道哪些线程安全的集合实现类?
Vector:
Vector和ArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它给⼏乎所有的public⽅法都加上了synchronized 关键字。
由于加锁导致性能降低,在不需要并发访问同⼀对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃⽤HashTable:
HashTable和HashMap类似,不同点是HashTable是线程安全的,它给⼏乎所有public⽅法都加上了synchronized关键字,还有⼀个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃⽤了
java.util.concurrent包中的集合:
ConcurrentHashMap:
ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。
HashTable的加锁⽅法是给每个⽅法加上synchronized关键字,这样锁住的是整个Table对象。
⽽ConcurrentHashMap是更细粒度的加锁在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的⼀部分,这样不同分段之间的并发操作就互不影响 JDK1.8对此做了进⼀步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每⼀⾏进⾏加锁,进⼀步减⼩了并发冲突的概率CopyOnWriteArrayList和CopyOnWriteArraySet:
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执⾏
除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,⾄于为什么没有ConcurrentArrayList,原因是⽆法设计⼀个通⽤的⽽且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这⽤Collections⾥的包装类就能办到
13.ConcurrentHashMap和HashTable的区别?
ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。
HashTable的加锁⽅法是给每个⽅法加上synchronized关键字,这样锁住的是整个Table对象。
⽽ConcurrentHashMap是更细粒度的加锁在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的⼀部分,这样不同分段之间的并发操作就互不影响 JDK1.8对此做了进⼀步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每⼀⾏进⾏加锁,进⼀步减⼩了并发冲突的概率
14.LinkedHashMap的使⽤场景?实现原理?
LinkedHashMap内部维护了⼀个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和after⽤于标识前置节点和后置节点。
可以实现按插⼊的顺序或访问顺序排序。
15.TreeMap怎么实现有序的?
TreeMap是按照Key的⾃然顺序或者Comprator的顺序进⾏排序,内部是通过红⿊树来实现。
所以要么key所属的类实现Comparable接⼝,或者⾃定义⼀个实现了Comparator接⼝的⽐较器,传给TreeMap⽤于key的⽐较。
16.JDK1.8对于HashMap的优化?为什么?
数组+链表改成了数组+链表或红⿊树;
链表的插⼊⽅式从头插法改成了尾插法,简单说就是插⼊时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
扩容的时候1.7需要对原数组中的元素进⾏重新hash定位在新数组的位置,1.8采⽤更简单的判断逻辑,位置不变或索引+旧容量⼤⼩;
在插⼊时,1.7先判断是否需要扩容,再插⼊,1.8先进⾏插⼊,插⼊完成再判断是否需要扩容;。