42 道Java集合经典面试题,陪伴学习,共同优秀
本篇文章是Java集合经典面试题。
1、Java中常用的集合有哪些?
Java集合框架为不同类型的集合定义了大量接口。
Java类库中的集合:
- ArrayList,可以动态增长和缩减的一个索引序列。
- LinkedList,可以在任意位置高效插入和删除的一个有序序列。
- ArrayDeque,实现为循环数组的一个双端队列。
- HashSet,没有重复元素的一个无序集合。
- TreeSet,有序集。
- EnumSet,包含枚举类型值的集合。
- LinkedHashSet,可以记住元素插入次序的集合。
- PriorityQueue,允许高效删除最小元素的集合。
- HashMap,存储键值对的数据结构。
- TreeMap,键有序的键值对集合。
- EnumMap,键是枚举类型的键值对集合。
- LinkedHashMap,可以记住添加次序的键值对集合。
- WeakHashMap,这个集合中的键如果不在使用,就会被垃圾回收。
2、Collection 和 Collections 有什么区别?
(1)Collection是最基本的集合接口,Collection派生了两个子接口list和set,分别定义了两种不同的存储方式。
(2)Collections是一个包装类,它包含各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等)。
此类不能实例化,就像一个工具类,服务于Collection框架。
3、为什么集合类没有实现 Cloneable 和 Serializable 接口?
集合类通常包含多个元素,这些元素的类型和数量可能会非常复杂。实现Cloneable和Serializable接口需要对这些元素的状态进行精确的控制和管理,这可能会导致代码变得复杂且容易出错。
序列化和克隆操作可能会消耗大量的计算资源,尤其是在处理大型集合时。不实现这些接口可以避免不必要的性能开销。
序列化和克隆操作可能会引发安全问题,因为它们允许对对象的深拷贝。如果不小心使用,可能会导致敏感信息的泄露或不当访问。
4、数组和集合有什么本质区别?
数组的大小是固定的,一旦创建后就不能改变,而集合的大小是动态的,可以根据需要扩展和缩减。
数组可以存储基本数据类型和引用数据类型,而集合只能存储引用数据类型(对象)。
数组通常用于存储同一类型的数据,而集合可以存储不同类型的数据。
数组的长度是不可变的,而集合的长度是可变的,这意味着可以在运行时向集合中添加或删除元素。
5、数组和集合如何选择?
如果数据的大小是固定的,那么数组可能是一个更好的选择,因为它提供了固定大小的存储空间。相反,如果数据的大小可能会发生变化,那么集合可能更合适。
如果需要存储基本数据类型,那么只能使用数组,如果需要存储不同类型的数据,集合可能更适合。
数组在访问速度上通常比集合更快,因为它们可以通过索引直接访问元素。
集合提供了许多有用的方法,如add、remove、contains等,这些方法使得数据的操作更加方便。如果需要使用这些方法,那么集合可能是更好的选择。
6、list与Set区别
(1)List简介
实际上有两种List:一种是基本的ArrayList,其优点在于随机访问元素,另一种是LinkedList,它并不是为快速随机访问设计的,而是快速的插入或删除。
- ArrayList:由数组实现的List。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。
- LinkedList :对顺序访问进行了优化,向List中间插入与删除的开销并不大。随机访问则相对较慢。
还具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 这些方法 (没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。
(2)Set简介
Set具有与Collection完全一样的接口,因此没有任何额外的功能。实际上Set就是Collection,只是行为不同。这是继承与多态思想的典型应用:表现不同的行为。Set不保存重复的元素(至于如何判断元素相同则较为负责)
- Set : 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
- HashSet:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
- TreeSet:保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
(3)list与Set区别
List,Set都是继承自Collection接口。
List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
Set和List对比:
- Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
- List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
7、HashMap 和 Hashtable 有什么区别?
- Hashtable不允许键或值为null,否则会抛出NullPointerException异常。而HashMap可以存储key和value为null的元素;
- Hashtable继承自Dictionary类,HashMap继承自AbstractMap类并实现了Map接口;
- Hashtable在创建时必须指定容量大小,且默认大小为11。而HashMap可以在创建时不指定容量大小,系统会自动分配初始容量,并采用2倍扩容机制;
- 迭代器 Iterator 对 Hashtable 是安全的,而 Iterator 对 HashMap 不是安全的,因为迭代器被设计为工作于一个快照上,如果在迭代过程中其他线程修改了 HashMap,则会抛出并发修改异常;
- Hashtable是线程安全的,而HashMap是非线程安全的。Hashtable通过在每个方法前加上synchronized关键字来保证线程安全性,而HashMap则没有实现这种机制。
8、concurrentHashMap和HashTable有什么区别
concurrentHashMap融合了hashmap和hashtable的优势,hashmap是不同步的,但是单线程情况下效率高,hashtable是同步的同步情况下保证程序执行的正确性。
concurrentHashMap锁的方式是细粒度的。concurrentHashMap将hash分为16个桶(默认值),诸如get、put、remove等常用操作只锁住当前需要用到的桶。
concurrentHashMap的读取并发,因为读取的大多数时候都没有锁定,所以读取操作几乎是完全的并发操作,只是在求size时才需要锁定整个hash。
而且在迭代时,concurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,弱一致迭代器。在这种方式中,当iterator被创建后集合再发生改变就不会抛出ConcurrentModificationException,取而代之的是在改变时new新的数据而不是影响原来的数据,iterator完成后再讲头指针替代为新的数据,这样iterator时使用的是原来的数据。
9、HashMap 的工作原理是什么?
(1)存储
当向HashMap中添加一个键值对时,首先会计算键(Key)的哈希值,这个哈希值将决定该键值对在内部数组中的索引位置。然后,该键值对会被存储在对应索引的链表中。如果两个不同的键拥有相同的哈希值,它们会被存储在同一个索引位置,这种现象称为哈希冲突。为了解决冲突,HashMap会在链表中维护这些具有相同哈希值的键值对。
(2)查找
当需要获取某个特定键对应的值时,HashMap会再次计算该键的哈希值,并沿着对应索引的链表查找匹配的键。一旦找到,就返回对应的值。
(3)扩容
随着HashMap中元素的增加,为了防止性能下降,当链表的长度超过一定阈值时,HashMap会进行自动扩容。这个过程涉及到创建一个新的、更大的数组,并将旧数组中的所有元素重新映射到新数组的索引上。这个过程也被称为rehashing。
(4)数据结构
HashMap的内部结构是一个Entry数组,每个Entry包含一个key-value键值对。这样设计的目的是为了高效地存储和检索数据。
10、Hashmap什么时候进行扩容?
在初始化HashMap时,需要指定其初始容量和负载因子。负载因子是一个介于0到1之间的浮点数,默认值为0.75。
当HashMap中的元素数量达到当前容量乘以负载因子时,即满足capacity * loadFactor条件时,就会触发扩容操作。
在扩容过程中,HashMap会创建一个新的数组,这个新数组的容量是原来容量的两倍。然后,它会重新计算每个键值对的哈希值,并将这些键值对重新映射到新数组的对应位置上。这个过程可能会涉及到一些性能开销,因为它需要重新计算哈希值和重新分配元素。
由于每次扩容都需要重新计算哈希值并重新分配元素,这会带来一定的性能开销。因此,我们应该尽量避免让HashMap频繁地进行扩容,以提高性能。
11、说一下 HashMap 的实现原理?
HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null。HashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。
HashMap在JDK1.7中采用数组+链表的存储结构。
HashMap采取Entry数组来存储key-value,每一个键值对组成了一个Entry实体,Entry类时机上是一个单向的链表结构,它具有next指针,指向下一个Entry实体,以此来解决Hash冲突的问题。
HashMap实现一个内部类Entry,重要的属性有hash、key、value、next。
JDK1.8中采用数据+链表+红黑树的存储形式。当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
12、为什么HashMap使用红黑树而不使用AVL树
- AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树;
- 红黑树更适合于插入修改密集型任务,即更适合HashMap;
- 通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。
深入理解红黑树与AVL树:
- AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。
- 两种实现都缩放为a O(lg N),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。
- 在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。
- 两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。
13、Java中的ConcurrentHashMap中为什么不能存储null?
与HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种更细的加锁机制来实现更大程度的共享,这种机制成为分段锁。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。ConcurrentHashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。
ConcurrentHashMap返回的迭代器具有弱一致性,而并非“及时失败”。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。ConcurrentHashMap返回的迭代器具有弱一致性,而并非“及时失败”。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。
14、Java8开始ConcurrentHashMap,为什么舍弃分段锁?
ConcurrentHashMap的原理是引用了内部的 Segment ( ReentrantLock ) 分段锁,保证在操作不同段 map 的时候, 可以并发执行, 操作同段 map 的时候,进行锁的竞争和等待。从而达到线程安全, 且效率大于 synchronized。
但是在 Java 8 之后, JDK 却弃用了这个策略,重新使用了 synchronized+CAS。
Java 8 中的 ConcurrentHashMap 放弃了分段锁,而是引入了 CAS 操作,即 Compare and Swap,利用原子性的操作和无锁编程的思想,来实现并发写入:采用一种乐观锁的方式,通过比较当前值与期望值是否相等,来决定是否更新。这种方式避免了对整个数据结构加锁,提高了并发写入时的性能和效率。
15、ConcurrentHashMap(JDK1.8)为什么要使用synchronized而不是如ReentranLock这样的可重入锁?
我想从下面几个角度讨论这个问题:
(1)锁的粒度
首先锁的粒度并没有变粗,甚至变得更细了。每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。
(2)Hash冲突
JDK1.7中,ConcurrentHashMap从过二次hash的方式(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。
16、set有哪些实现类?
其实面试官想听到的不只是有哪些实现类,而是触类旁通,比如它们的原理、对比、使用场景等。
(1)HashSet
- 基于散列表实现的Set集合,内部的存储结构是哈希表,是线程不安全的
- 它的底层数据结构是HashMap,因此它拥有快速的存取速度,是用的最多的实现类;
- HashSet不保证元素的迭代顺序,也不保证该顺序会随着时间的推移保持不变;
- 允许使用null元素;
(2)TreeSet
- 基于红黑树(Red-Black tree)或者AVL树等自平衡二叉查找树实现的Set集合;
- TreeSet可以确保集合元素处于排序状态;
- 不允许插入null元素
TreeSet对元素进行排序的方式:
- 元素自身具备比较功能,需要实现Comparable接口,并覆盖compareTo方法;
- 元素自身不具备比较功能,需要实现Comparator接口,并覆盖compare方法。
(3)链接散列集LinkedHashSet
- LinkedHashSet结合了哈希表和链表两种数据结构,具有可预知的迭代顺序;
- 它继承自HashSet,但是又添加了一个双向链表来维持插入的顺序;
- LinkedHashSet的元素迭代顺序是它们被插入的顺序,或者最近访问的顺序。
HashSet和LinkedHashSet内部使用哈希表来存储元素,当多个元素经过哈希函数计算后产生同一个索引位置时,就会产生哈希冲突。为了解决哈希冲突,HashSet和LinkedHashSet使用链式散列技术,即在哈希表每个索引位置上维护一个链表,将所有哈希值相同的元素存放在同一个链表中,从而实现快速查找和添加元素。
LinkedHashMap的常用方法包括:
- put(K key, V value):将一个键值对添加到链接散列集中;
- get(K key):返回一个键值对,如果键不存在则返回null;
- remove(K key):从链接散列集中删除一个键值对;
- containsKey(K key):检查一个键是否存在于链接散列集中;
- size():返回链接散列集中键值对的数量;
这些方法都是基于链表实现的,因此它们的时间复杂度都是O(1),其中n是Map中元素的数量。
(4)AbstractSet
这是一个抽象类,它为创建新的set实现提供了一个框架。它本身不能直接实例化,但可以通过继承并实现其抽象方法来创建自定义的set实现类。
面试中能说出AbstractSet的肯定不多,面试加分项。
17、说一下HashSet的实现原理
HashSet底层使用的是数组加链表或者红黑树的数据结构。在JDK1.8之前,主要是数组加链表的方式,而在JDK1.8及以后的版本中,为了优化性能,引入了红黑树。
当我们向HashSet中添加一个元素时,首先会计算该元素的哈希值,然后根据哈希值确定元素在内部HashMap中的存储位置。如果该位置为空,则直接存储;如果不为空,则需要通过链表或红黑树来处理冲突。
在查找元素时,也是通过计算哈希值来确定位置,然后在对应的链表或红黑树中进行搜索。
HashSet的性能可以通过合理设置初始容量和负载因子来提高。一个较大的初始容量可以减少扩容操作的频率,而合适的负载因子可以平衡空间利用率和查找效率。
18、Set是如何保证元素不重复的?
HashSet内部实际上是通过HashMap来实现的。当向HashSet中添加一个元素时,它会调用该元素的hashCode()方法来计算其哈希值,然后根据这个哈希值决定元素在HashMap中的存储位置。
如果有两个元素具有相同的哈希值,那么它们会被视为同一个位置的候选者。为了区分这些具有相同哈希值的元素,HashSet还会使用equals()方法来比较它们是否相等。只有当元素在HashMap中不存在时,它才会被添加到集合中。如果已经存在,则不会重复添加,从而保证了Set集合中元素的唯一性。
19、HashMap和HashSet的区别
(1)先了解一下HashCode
Java中的集合有两类,一类是List,一类是Set。
List:元素有序,可以重复。
Set:元素无序,不可重复。
要想保证元素的不重复,拿什么来判断呢?这就是Object.equals方法了。如果元素有很多,增加一个元素,就要判断n次吗?
显然不现实,于是,Java采用了哈希表的原理。哈希算法也称为散列算法,是将数据依特定算法直接指定到一根地址上,初学者可以简单的理解为,HashCode方法返回的就是对象存储的物理位置(实际上并不是)。
这样一来,当集合添加新的元素时,先调用这个元素的hashcode()方法,就一下子能定位到他应该放置的物理位置上。如果这个位置上没有元素,他就可以直接存储在这个位置上,不用再进行任何比较了。如果这个位置上有元素,就调用它的equals方法与新元素进行比较,想同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际上调用equals方法的次数就大大降低了,几乎只需要一两次。
简而言之,在集合查找时,hashcode能大大降低对象比较次数,提高查找效率。
Java对象的equals方法和hashCode方法时这样规定的:
相等的对象就必须具有相等的hashcode。
- 如果两个对象的hashcode相同,他们并不一定相同。
- 如果两个对象的hashcode相同,他们并不一定相同。
如果两个Java对象A和B,A和B不相等,但是A和B的哈希码相等,将A和B都存入HashMap时会发生哈希冲突,也就是A和B存放在HashMap内部数组的位置索引相同,这时HashMap会在该位置建立一个链接表,将A和B串起来放在该位置,显然,该情况不违反HashMap的使用规则,是允许的。当然,哈希冲突越少越好,尽量采用好的哈希算法避免哈希冲突。
equals()相等的两个对象,hashcode()一定相等;equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。
(2)HashMap和HashSet的区别
20、TreeSet常用方法有哪些?
- add(Object obj):将一个对象添加到TreeSet中。
- remove(Object obj):从TreeSet中移除一个对象。
- pollFirst():返回TreeSet中的第一个对象,如果TreeSet为空则返回null。
- pollLast():返回TreeSet中的最后一个对象,如果TreeSet为空则返回null。
- size():返回TreeSet中元素的个数。
- isEmpty():判断TreeSet是否为空。
- contains(Object obj):判断一个对象是否在TreeSet中。
- addAll(Collection