常见面试题之JAVA集合
1. Java中的集合分为哪几大类?
参考答案:
Java中的集合主要分为三大类:List
、Set
、Map
,还有一种不常用的Queue
。
List
:有序集合,允许有重复元素,可以插入多个null元素。常用的实现类有ArrayList
、LinkedList
和Vector
。Set
:无序集合,不允许有重复元素,只允许有一个null元素。常用的实现类有HashSet
、LinkedHashSet
和TreeSet
。Map
:一种键值对的集合,键不允许重复,值可以重复。常用的实现类有HashMap
、LinkedHashMap
、TreeMap
、Hashtable
等。Queue
:一种先进先出(FIFO)的集合,常用于任务调度。常用的实现类有LinkedList
(它也实现了List
接口)、PriorityQueue
等。
2. ArrayList
和LinkedList
的区别是什么?
参考答案:
- 底层实现:
ArrayList
是基于数组实现的,LinkedList
是基于链表实现的。 - 性能:
ArrayList
在随机访问方面性能优越,因为数组支持快速随机访问;而LinkedList
在插入和删除操作方面性能较好,因为链表支持快速的插入和删除操作。但在迭代访问时,LinkedList
的性能会低于ArrayList
。 - 内存使用:
ArrayList
需要连续的内存空间来存储其元素,因此内存利用率较高,但扩容时需要复制整个数组,内存开销较大;LinkedList
的每个元素都需要额外的存储空间来存储指向下一个元素的指针,因此内存利用率较低,但扩容时只需改变指针的指向,内存开销较小。 - 线程安全:两者都不是线程安全的,如果需要在多线程环境下使用,需要手动同步。
3. HashSet
是如何保证不重复的?
参考答案:
HashSet
是基于HashMap
实现的,HashSet
中的元素实际上是存储在HashMap
的key中的。HashMap
的key是不允许重复的,当向HashSet
中添加一个元素时,HashSet
会调用该元素的hashCode()
方法来获取其哈希值,并根据哈希值来确定该元素在HashMap
中的存储位置。如果两个元素的哈希值相同,HashMap
会进一步通过equals()
方法来比较这两个元素是否相等,如果相等,则后添加的元素不会被存储,从而保证了HashSet
中的元素不重复。
4. HashMap
的工作原理是什么?
参考答案:
HashMap
是基于哈希表(也称为散列表)实现的。哈希表是一个数组,数组中的每个元素称为桶(bucket),每个桶可以存储一个或多个键值对(Entry)。当向HashMap
中添加一个键值对时,HashMap
会根据键的hashCode()
方法来计算该键的哈希值,并根据哈希值来确定该键值对应该存储在哪个桶中。如果两个键的哈希值相同(也称为哈希冲突),则这两个键值对会被存储在同一个桶中,并通过链表或红黑树(在JDK 8及以后)来连接。当需要查找一个键时,HashMap
会根据该键的哈希值找到对应的桶,并在桶中遍历链表或红黑树来查找该键对应的值。
5. HashMap
和Hashtable
的区别是什么?
参考答案:
- 线程安全:
HashMap
是非线程安全的,而Hashtable
是线程安全的。Hashtable
的所有方法都是同步的,因此在多线程环境下使用较为安全,但性能会较低。而HashMap
在单线程环境下性能较高,但在多线程环境下需要手动同步。 - 性能:由于
Hashtable
是线程安全的,因此在性能方面会比HashMap
差一些。在单线程环境下,HashMap
的性能要优于Hashtable
。 - 允许null:
HashMap
允许键和值都为null,而Hashtable
不允许键或值为null。如果向Hashtable
中插入null键或null值,会抛出NullPointerException
异常。 - 继承关系:
HashMap
是继承自AbstractMap
类,并实现了Map
接口;而Hashtable
是继承自Dictionary
类,并实现了Map
接口(但实际上Hashtable
是一个古老的类,在Java 2之前就存在了,而Map
接口是在Java 2中引入的)。
6. TreeMap
是如何实现排序的?
参考答案:
TreeMap
是基于红黑树(一种自平衡的二叉查找树)实现的。红黑树是一种有序的数据结构,它能够在O(log n)的时间复杂度内完成插入、删除和查找操作。当向TreeMap
中添加一个键值对时,TreeMap
会根据键的自然顺序(或者根据构造函数中指定的比较器)来将键值对插入到红黑树中。由于红黑树是有序的,因此TreeMap
中的键值对也是按照键的顺序排列的。当对TreeMap
进行迭代时,迭代器会按照键的顺序返回键值对。
7. LinkedHashMap
和HashMap
的区别是什么?
参考答案:
- 底层实现:
LinkedHashMap
和HashMap
都是基于哈希表实现的,但LinkedHashMap
在HashMap
的基础上增加了一个双向链表来维护键值对的插入顺序或访问顺序。 - 顺序性:
HashMap
是无序的,即不保证键值对的插入顺序和迭代顺序相同;而LinkedHashMap
是有序的,它可以根据插入顺序或访问顺序来迭代键值对。默认情况下,LinkedHashMap
是按照插入顺序来迭代键值对的。 - 性能:由于
LinkedHashMap
需要维护一个双向链表来保持顺序性,因此在性能方面会比HashMap
稍微差一些。但在大多数情况下,这种性能差异是可以忽略不计的。
8. ConcurrentHashMap
是如何实现线程安全的?
参考答案:
ConcurrentHashMap
是Java中的一个线程安全的哈希表实现。它采用了分段锁(Segment Lock)技术来提高并发性能。在ConcurrentHashMap
中,整个哈希表被划分为多个段(Segment),每个段都是一个独立的哈希表,并且每个段都有自己的锁。当多个线程同时访问ConcurrentHashMap
时,它们可以并发地访问不同的段,而不需要等待其他线程释放锁。这样,ConcurrentHashMap
就可以在多线程环境下实现高效的并发访问。在JDK 8及以后,ConcurrentHashMap
的实现发生了较大的变化,它不再使用分段锁技术,而是使用了更加细粒度的锁(如CAS操作和synchronized块)来实现线程安全。这种变化使得ConcurrentHashMap
在性能方面有了更大的提升,并且更加适用于高并发场景。
9. ArrayList
和Vector
的区别是什么?
参考答案:
- 线程安全:
Vector
是线程安全的,而ArrayList
是非线程安全的。Vector
的所有方法都是同步的,因此在多线程环境下使用较为安全;而ArrayList
在单线程环境下性能较高,但在多线程环境下需要手动同步。 - 性能:由于
Vector
是线程安全的,因此在性能方面会比ArrayList
差一些。在单线程环境下,ArrayList
的性能要优于Vector
。 - 扩容机制:
ArrayList
和Vector
在扩容时都会创建一个新的数组,并将旧数组中的元素复制到新数组中。但是,ArrayList
的扩容机制是动态的,当数组容量不足时,会自动扩容为原来的1.5倍(或指定的大小);而Vector
的扩容机制是固定的,当数组容量不足时,会按照指定的增量(默认为原容量的1倍)进行扩容。 - 历史原因:
Vector
是一个古老的类,在Java 2之前就存在了;而ArrayList
是在Java 2中引入的,作为Vector
的一个替代品。由于Vector
是线程安全的,因此在早期多线程环境下使用较多;但随着Java多线程技术的发展和普及,以及ArrayList
在单线程环境下性能的优势,现在ArrayList
的使用更加广泛。
10. HashSet
和TreeSet
的区别是什么?
参考答案:
- 底层实现:
HashSet
是基于HashMap
实现的,而TreeSet
是基于TreeMap
实现的。因此,HashSet
的性能和行为与HashMap
类似,而TreeSet
的性能和行为与TreeMap
类似。 - 排序性:
HashSet
是无序的集合,不保证元素的存储顺序和迭代顺序相同;而TreeSet
是有序的集合,它会按照元素的自然顺序(或者根据构造函数中指定的比较器)来对元素进行排序。因此,TreeSet
的迭代顺序是确定的,与元素的插入顺序无关。 - 性能:由于
TreeSet
需要维护元素的排序顺序,因此在性能方面会比HashSet
差一些。特别是在插入和删除操作时,TreeSet
需要调整红黑树的结构来保持排序顺序,而HashSet
则不需要。 - 允许null:
HashSet
允许包含一个null元素,而TreeSet
不允许包含null元素。如果向TreeSet
中插入null元素,会抛出NullPointerException
异常。
11. Collections
工具类中有哪些常用的方法?
参考答案:
Collections
工具类提供了许多用于操作集合的静态方法,以下是一些常用的方法:
sort(List<T> list)
:对指定的列表进行排序。reverse(List<?> list)
:反转指定列表的顺序。shuffle(List<?>list)
:使用默认随机源对指定列表进行置换,所有排列具有相同的出现可能性。fill(List<? super T> list, T obj)
:使用指定元素替换指定列表中的所有元素。copy(List<? super T> dest, List<? extends T> src)
:将指定源列表中的所有元素复制到目标列表中。max(Collection<? extends T> coll, Comparator<? super T> comp)
:根据指定比较器返回给定集合的最大元素。min(Collection<? extends T> coll, Comparator<? super T> comp)
:根据指定比较器返回给定集合的最小元素。addAll(Collection<? super T> collection, T... elements)
:将所有指定的元素添加到指定集合中。frequency(Collection<?> c, Object o)
:返回指定集合中指定元素的出现次数。disjoint(Collection<?> c1, Collection<?> c2)
:如果两个指定集合没有公共元素,则返回true
。emptyList()
:返回一个空的、不可变的列表。emptySet()
:返回一个空的、不可变的集合。emptyMap()
:返回一个空的、不可变的映射。singletonList(T o)
:返回一个只包含指定对象的不可变列表。singleton(T o)
:返回一个只包含指定对象的不可变集合。singletonMap(K key, V value)
:返回一个仅包含指定键和值的不可变映射。
12. List
接口中有哪些常用的方法?
参考答案:
List
接口继承自Collection
接口,并增加了许多用于操作列表的特定方法,以下是一些常用的方法:
add(E e)
:将指定的元素追加到此列表的末尾。add(int index, E element)
:在列表的指定位置插入指定的元素。get(int index)
:返回列表中指定位置的元素。remove(int index)
:移除列表中指定位置的元素。set(int index, E element)
:用指定的元素替换列表中指定位置的元素。indexOf(Object o)
:返回指定元素在列表中第一次出现的索引,如果元素不存在,则返回 -1。lastIndexOf(Object o)
:返回指定元素在列表中最后一次出现的索引,如果元素不存在,则返回 -1。subList(int fromIndex, int toIndex)
:返回列表中指定的fromIndex
(包括)和toIndex
(不包括)之间的部分视图。size()
:返回列表中的元素数。isEmpty()
:如果列表不包含元素,则返回true
。contains(Object o)
:如果列表包含指定的元素,则返回true
。iterator()
:返回列表中的元素的迭代器。listIterator()
:返回列表中元素的列表迭代器(可以从任意位置开始遍历列表)。
13. Set
接口中有哪些常用的方法?
参考答案:
Set
接口继承自Collection
接口,但由于其不允许包含重复元素,因此具有一些特定的行为。以下是一些常用的方法:
add(E e)
:如果集合中尚未存在指定的元素,则添加该元素。remove(Object o)
:从集合中移除指定的元素。contains(Object o)
:如果集合包含指定的元素,则返回true
。size()
:返回集合中的元素数。isEmpty()
:如果集合不包含元素,则返回true
。iterator()
:返回集合中的元素的迭代器。clear()
:从集合中移除所有元素。
由于Set
接口不允许包含重复元素,因此它没有像List
接口那样的根据索引访问元素的方法。
14. Map
接口中有哪些常用的方法?
参考答案:
Map
接口提供了用于操作键值对的许多方法,以下是一些常用的方法:
put(K key, V value)
:将指定的键与指定的值关联(或覆盖)。get(Object key)
:返回指定键所映射的值,如果映射不存在,则返回null
。remove(Object key)
:从映射中移除指定键的键-值对。containsKey(Object key)
:如果映射包含指定键的键-值对,则返回true
。containsValue(Object value)
:如果映射包含一个或多个键映射到指定值,则返回true
。size()
:返回映射中的键-值对的数目。isEmpty()
:如果映射不包含任何键-值对,则返回true
。keySet()
:返回映射中包含的键的视图。values()
:返回映射中包含的值的视图。entrySet()
:返回映射中包含的键-值对的视图。getOrDefault(Object key, V defaultValue)
:返回指定键映射的值,如果该映射不存在,则返回默认值。putIfAbsent(K key, V value)
:如果指定键不存在,或者其当前映射的值是null
,则将其与指定的值关联并返回null
,否则返回当前值。
15. HashMap
的初始容量和加载因子是什么?它们对性能有什么影响?
参考答案:
HashMap
的初始容量和加载因子是影响其性能的两个重要参数。
- 初始容量:
HashMap
的初始容量是创建时哈希表的桶的数量。默认初始容量是16。如果初始容量设置得太小,随着元素的增加,哈希表会频繁地进行扩容操作,这会影响性能。扩容操作需要重新分配内存和重新哈希所有元素,因此是一个昂贵的操作。 - 加载因子:加载因子是哈希表在其容量自动增加之前可以达到多满的一种度量。默认加载因子是0.75。当哈希表中的元素数量超过加载因子与当前容量的乘积时,哈希表会进行扩容操作。加载因子过高会导致哈希表中桶的链表过长,增加查找元素的时间复杂度;加载因子过低会导致哈希表频繁扩容,浪费内存。
合理选择初始容量和加载因子可以平衡哈希表的时间复杂度和空间复杂度,从而提高HashMap
的性能。
16. HashMap
中的哈希冲突是如何解决的?
参考答案:
在HashMap
中,哈希冲突是通过链表(或红黑树,在JDK 8及以后)来解决的。当两个或多个键的哈希值相同时,它们会被放在同一个桶中,并通过链表或红黑树来连接。具体来说:
- 链表:在JDK 7及之前,当发生哈希冲突时,
HashMap
会在桶中使用链表来存储多个键值对。当查找一个键时,HashMap
会根据键的哈希值找到对应的桶,并在桶中遍历链表来查找该键对应的值。 - 红黑树:在JDK 8及以后,当桶中的链表长度超过阈值(默认为8)时,
HashMap
会将链表转换为红黑树。红黑树是一种自平衡的二叉查找树,它能够在O(log n)的时间复杂度内完成插入、删除和查找操作。因此,当桶中的元素较多时,使用红黑树可以提高查找效率。
通过链表和红黑树的结合使用,HashMap
可以有效地解决哈希冲突问题,并在大多数情况下提供较好的性能。
17. LinkedHashMap
是如何维护插入顺序或访问顺序的?
参考答案:
LinkedHashMap
通过维护一个双向链表来保持元素的插入顺序或访问顺序。具体来说:
- 插入顺序:当使用默认构造函数创建
LinkedHashMap
时,它会按照元素的插入顺序来迭代键值对。这是通过在每个桶中维护一个双向链表来实现的。当向LinkedHashMap
中添加一个键值对时,新元素会被添加到链表的末尾。因此,迭代LinkedHashMap
时,会按照插入顺序返回键值对。 - 访问顺序:当使用带有
accessOrder
参数的构造函数创建LinkedHashMap
,并将accessOrder
设置为true
时,LinkedHashMap
会按照元素的访问顺序来迭代键值对。在这种情况下,每次访问(通过get
或put
方法)一个元素时,该元素都会被移动到链表的末尾。因此,迭代LinkedHashMap
时,会按照最近访问的顺序返回键值对。这种机制可以用于实现缓存等功能,其中最近访问的元素更有可能再次被访问。
18. ConcurrentHashMap
在JDK 8中的实现有哪些变化?
参考答案:
在JDK 8中,ConcurrentHashMap
的实现发生了较大的变化,主要包括以下几个方面:
- 数据结构:在JDK 7及之前,
ConcurrentHashMap
使用分段锁(Segment Lock)来提高并发性能。每个段都是一个独立的哈希表,并且有自己的锁。而在JDK 8中,ConcurrentHashMap
不再使用分段锁技术,而是使用了更加细粒度的锁(如CAS操作和synchronized块)来实现线程安全。这种变化使得ConcurrentHashMap
在性能方面有了更大的提升,并且更加适用于高并发场景。 - 节点结构:在JDK 8中,
ConcurrentHashMap
的节点结构也发生了变化。它引入了红黑树(在链表长度超过阈值时)来优化哈希冲突的处理。当桶中的链表长度过长时,ConcurrentHashMap
会将链表转换为红黑树,以提高查找效率。这种优化在高度冲突的哈希表中特别有效。 - 扩容机制:在JDK 8中,
ConcurrentHashMap
的扩容机制也进行了优化。与JDK 7中的一次性扩容不同,JDK 8中的ConcurrentHashMap
支持渐进式扩容。这意味着扩容操作不会一次性完成,而是分散在多个插入操作中逐渐进行。这种机制可以减少扩容时对其他操作的影响,提高并发性能。 - 并发控制:在JDK 8中,
ConcurrentHashMap
使用了更加细粒度的并发控制。它结合了CAS(Compare-And-Swap)操作和synchronized块来实现线程安全。对于大多数读操作,如get
,是不需要加锁的,这大大提高了读取性能。而对于写操作,如put
,则使用synchronized块或CAS操作来保证线程安全。这种细粒度的并发控制使得ConcurrentHashMap
在高并发场景下具有更好的性能。 - 弱一致性:与JDK 7及之前的版本相比,JDK 8中的
ConcurrentHashMap
在迭代时提供了弱一致性保证。这意味着在迭代过程中,即使其他线程对ConcurrentHashMap
进行了修改,迭代器也不会抛出ConcurrentModificationException
。相反,它会尽可能地反映出迭代过程中的修改。这种弱一致性保证使得ConcurrentHashMap
更加适用于并发迭代场景。 - 性能提升:由于上述优化,JDK 8中的
ConcurrentHashMap
在性能上有了显著提升。它不仅在单线程环境下表现优异,而且在高并发场景下也表现出色。这使得ConcurrentHashMap
成为处理高并发数据的首选数据结构之一。