Java集合常见面试题
1.常用的集合有哪些?
- List接口的实现类:
- ArrayList:基于动态数组实现,支持随机访问,适合频繁读取的场景
- 优点:随机访问速度快,内存效率高。
- 缺点:在中间插入或删除元素时需要移动后续元素,性能较低。
- LinkedList:基于双向链表实现,适合频繁插入和删除的场景。
- 优点:在头部或尾部插入和删除元素性能优秀。
- 缺点:随机访问速度慢,因为需要从头或尾遍历到目标位置。
- Vector:类似于 ArrayList,但线程安全,适合多线程环境(由于其性能开销,通常不推荐使用)。
- 优点:同步访问,线程安全。
- 缺点:在多线程环境下开销较大,且通常比 ArrayList 性能低。
- ArrayList:基于动态数组实现,支持随机访问,适合频繁读取的场景
- Set接口的实现类:
- HashSet:基于哈希表实现,不保证元素的顺序,适合不需要重复元素的场景。
- LinkedHashSet:维护元素插入的顺序,基于哈希表和链表实现。
- TreeSet:基于红黑树实现,保证元素的有序性,适合需要排序的场景。
- Map接口的实现类:
- HashMap:基于哈希表实现,允许 null 值和 null 键,性能较高。
- ConcurrentHashMap : 允许多个线程同时读写而无需额外的同步机制,确保线程安全性。
- LinkedHashMap:维护元素的插入顺序,基于哈希表和链表实现。
- TreeMap:基于红黑树实现,保证键的有序性。
- Hashtable:线程安全的哈希表实现,不允许 null 键和 null 值,性能较低,通常不推荐使用。
- Queue接口的实现类:
- PriorityQueue:基于堆实现,支持元素的优先级排序。
- ArrayDeque:基于动态数组实现,作为双端队列使用,性能优于 LinkedList。
- 其他集合类
- Stack:线程安全的后进先出(LIFO)栈,继承自 Vector,但不推荐使用。可以使用 ArrayDeque 来代替。
- CopyOnWriteArrayList:线程安全的 ArrayList 实现,适合读多写少的场景。写操作时会复制数组,导致性能开销。
- ConcurrentSkipListMap:线程安全的有序 Map,支持高并发场景,基于跳表实现。
2.集合有哪些特点?
- List:有序集合,能够精确控制每个元素的插入位置,可以包含重复元素。
- Set:无序不可重复,元素的顺序是不可预测的
- Map:键值对集合,持有键(唯一)到值的映射,键不可重复,值可以重复。
- Queue:队列,先进先出
- Deque:双端队列,允许从两端添加或删除元素
3.Iterator怎么使用?有什么特点
- Iterator是一个接口,提供了遍历集合的标准方法,使用如下
1.获取迭代器实例:通过调用集合的 iterator() 方法获取 Iterator 实例。
2.检查是否有下一个元素:使用 hasNext() 方法检查集合中是否还有元素。
3.使访问下一个元素:使用 next() 方法访问集合中的下一个元素,如果没有更多元素而调用此方法,会抛出 NoSuchElementException。
4.可以使用 remove() 方法从集合中删除最后一个返回的元素。使用 remove() 时必须在 next() 方法之后调用,否则会抛出 IllegalStateException - 特点:
- 通用性:为不同类型的集合提供了同一的遍历方式
- 安全删除:通过迭代器的remove()方法可以安全删除集合中的元素,避免ConcurrentModificationException.(是在多线程环境下由于结构修改而导致的)
- 单向遍历:迭代器仅支持向前遍历集合,不支持反向遍历
- 无法访问索引:迭代器不提供获取当前元素索引的方法
4.ArrayList和LinkedList的区别是什么?
- 内部结构:
- ArrayList:基于动态数组实现,支持快速随机访问(通过索引访问),可以在 O(1) 时间复杂度内访问任意元素。
- LinkedList:基于双向链表实现,每个节点包含指向前后元素的引用,访问特定位置的元素需要 O(n) 的时间复杂度,因为需要遍历链表。
- 性能:
- ArrayList:对于随机访问操作速度快(O(1)),但在列表中间或开头插入和删除元素时需要移动大量元素,时间复杂度为 O(n)。如果数组容量不足,还需要进行扩容,这会导致性能下降
- LinkedList:插入和删除操作速度快(O(1)),特别是在链表的头部或尾部进行操作时很高效。然而,随机访问速度较慢(O(n)),需要从头节点或尾节点开始遍历,直到找到目标元素。
- 内存占用:
- ArrayList:由于内部使用数组存储元素,当数组容量不足时可能会进行扩容,导致额外的内存占用(可能会浪费内存空间)。此外,动态数组需要连续的内存空间。
- LinkedList:每个节点除了存储数据外,还需要额外的内存来存储指向前后节点的引用(两个指针),因此对于存储大量小对象时,内存开销相对较大,可能会导致较多的内存碎片。
- 使用场景:ArrayList适合频繁查找操作,LinkedList适用于需要在列表的两端进行频繁操作的场合。
5.说一下HashSet的实现原理
- HashSet是基于HashMap实现的,它使用HashMap的键来存储元素,每个元素都映射到一个固定的虚拟值(HashMap的值部分)。HashSet利用HashMap键的唯一性来确保其元素的唯一性,并且通过哈希表实现,因此具有很高的查找和插入效率。当向HashSet添加元素时,实际上是将元素作为HashMap的键添加,而对应的值则是一个默认为Object对象。
6.HashMap与HashTable有什么区别?
- 线程安全:HashTable是线程安全的,所有方法都是同步的,而HashMap是非线程安全的。
- 性能:因为HashTable的方法是同步的,所以在单线程环境下比HashMap性能低。
- 空值:HashMap允许键和值为null(可以有多个null值,但只能有一个null键),HashTable不允许键或值为null。
- 迭代器:HashMap的迭代器是快速失败的(fail-fast:是指在使用迭代器遍历集合时,如果在遍历过程中检测到集合被修改(例如添加或删除元素),迭代器会立即抛出一个 ConcurrentModificationException,以避免遍历时数据不一致的情况。),而HashTable的枚举器不是。
7.Collection和Collecions有什么区别?
- Collection:是一个接口,它是各种集合结构的根接口,提供了对集合对象进行基本操作的通用接口方法。
- Collections:是一个包含有关集合操作的静态方法的工具类,如排序、搜索等。
8.comparable和comparator的区别?
- Comparable:是一个用于自然排序的接口,通常用在实体类中。
- Comparable 接口包含一个方法 compareTo(Object o),该方法返回一个整数,表示当前对象与参数对象的比较结果。
- 返回负数:当前对象小于参数对象。
- 返回零:当前对象等于参数对象。
- 返回正数:当前对象大于参数对象
- 使用场景:当需要为对象定义默认排序规则时,使用 Comparable。例如,实现 Comparable 的 String 类会根据字母顺序进行排序。
- Comparable 接口包含一个方法 compareTo(Object o),该方法返回一个整数,表示当前对象与参数对象的比较结果。
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
return this.age - other.age; // 按照年龄升序排序
}
}
- Comparator 接口:是一个用于自定义排序的接口,通常用于创建单独的比较器类。
- 方法:Comparator 接口包含一个方法 compare(Object o1, Object o2),用于比较两个对象。
- 返回负数:o1 小于 o2
- 返回零:o1 等于 o2
- 返回正数:o1 大于 o2
- 使用场景:当需要为对象定义多种排序规则时,使用 Comparator。比如一个类可以按年龄排序,也可以按姓名排序,这时就可以使用不同的 Comparator 实现。
- 方法:Comparator 接口包含一个方法 compare(Object o1, Object o2),用于比较两个对象。
下面展示一些 内联代码片
。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter methods...
}
// 按照姓名排序的 Comparator
class NameComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
}
// 按照年龄排序的 Comparator
class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
}
9.Collections工具类中的sort()方法如何比较元素?
- Collections.sort()方法比较元素的方式取决于传入的集合元素类型
- 如果集合元素实现了Comparable接口,sort()方法会使用这些元素的compareTo()进行自然排序。
- 可以重载sort()方法,传入一个自定义的Comparator对象,这时候会使用该Comparator的compare()方法来比较元素,实现自定义排序。
10.如何实现数组和List之间的转换?
- 数组转list,可以使用jdk自动的一个工具类Arrars,里面有一个asList方法
可以转换为数组 - List 转数组,可以直接调用list中的toArray方法,需要给一个参数,指定数
组的类型,需要指定数组的长度
11.HashMap的jdk1.7和jdk1.8有什么区别?
- JDK1.8之前采用的拉链法,数组+链表
- JDK1.8之后采用数组+链表+红黑树,当链表的长度超过8,并且当前的数组长度至少为64时,会将链表转换为红黑树
12.HashMap的扩容机制
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化
数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度 * 0.75) - 每次扩容的时候,都是扩容之前容量的2倍
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
13.为何HashMap的数组长度一定是2的次幂?
- hashmap这么设计主要有两个原因
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高:在进行扩容是会进行判断 hash值按位与运算旧数组长租是否 == 0,如果等于0,则把元素留在原来位置 ,否则新位置是等于旧位置的下标+旧数
组长度
14.ArrayList的扩容机制
- ArrayList 默认的初始容量是 10,当添加新元素时,如果当前的数组长度已经满了(即容量已满),ArrayList 会自动调用 ensureCapacity() 方法进行扩容。新数组的容量为原数组容量的 1.5 倍。具体计算方式是:newCapacity = oldCapacity + (oldCapacity >> 1),这意味着新容量是原容量加上原容量的一半。新数组创建后,ArrayList 会将旧数组中的元素复制到新数组中。