我整合了多个技术平台上的相关内容,从常见问题入手,结合应用实例,为你梳理出这篇Java集合面试题总结,希望能助你学习一臂之力。
常见JAVA集合面试题(自用整理,持续更新)
一、ArrayList和LinkedList的区别
1. 底层数据结构
- ArrayList:基于动态数组实现。在创建ArrayList时,如果没有指定初始容量,会默认创建一个容量为10的数组。当数组中的元素数量超过数组容量时,会进行扩容操作,新的容量一般是原容量的1.5倍。例如:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
// 当继续添加元素时,如果当前容量不足,会自动扩容
- LinkedList:基于双向链表实现。每个节点不仅存储了元素本身,还包含了指向前一个节点和后一个节点的引用。比如:
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("a");
linkedList.add("b");
// 链表节点之间通过引用相互连接
2. 随机访问性能
- ArrayList:支持快速随机访问,时间复杂度为O(1)。因为可以通过数组索引直接定位到元素的位置。例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
int element = list.get(1); // 可以快速获取索引为1的元素,即20
- LinkedList:随机访问性能较差,时间复杂度为O(n)。因为需要从链表的头节点或尾节点开始,逐个遍历节点,直到找到目标位置的节点。例如:
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(10);
linkedList.add(20);
linkedList.add(30);
// 获取索引为1的元素,需要从链表头开始遍历
int element = linkedList.get(1);
3. 插入和删除性能
- ArrayList:在数组中间位置插入或删除元素时,需要移动大量元素,时间复杂度为O(n)。但在数组尾部插入或删除元素时,时间复杂度为O(1)(不考虑扩容情况)。例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 在索引为1的位置插入元素4,后续元素都要向后移动
list.add(1, 4);
- LinkedList:在链表的任意位置插入或删除元素,只需要修改相邻节点的引用,时间复杂度为O(1)。但如果要先定位到指定位置的节点,时间复杂度则为O(n)。例如:
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
// 在索引为1的位置插入元素4,只需修改相邻节点引用
linkedList.add(1, 4);
4. 内存占用
- ArrayList:内存占用相对较小,主要存储元素本身。
- LinkedList:每个节点除了存储元素,还需要存储两个引用,内存占用较大。
二、HashMap和HashTable的区别
1. 线程安全性
- HashMap:非线程安全,在多线程环境下并发访问可能会出现数据不一致问题,甚至引发死循环(在JDK 1.7及之前的版本中,多线程扩容时可能会出现)。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
// 多线程同时进行put操作,可能会出现问题
new Thread(() -> {
hashMap.put(1, "a");
}).start();
new Thread(() -> {
hashMap.put(2, "b");
}).start();
- HashTable:线程安全,它的大部分方法(如put、get等)都使用了synchronized关键字进行同步。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
// 多线程环境下使用相对安全
new Thread(() -> {
hashtable.put(1, "a");
}).start();
new Thread(() -> {
hashtable.put(2, "b");
}).start();
2. Null键和Null值
- HashMap:允许null键和null值。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(null, "nullValue");
hashMap.put(1, null);
- HashTable:不允许null键和null值,否则会抛出NullPointerException。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
// 以下代码会抛出NullPointerException
// hashtable.put(null, "value");
// hashtable.put(1, null);
3. 迭代器
- HashMap:迭代器是fail - fast的,即当迭代过程中集合结构被修改(除了通过迭代器自身的remove方法),会立即抛出ConcurrentModificationException。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
Iterator<Map.Entry<Integer, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
// 这里如果直接调用hashMap.put(3, "c");会抛出异常
iterator.remove();
}
- HashTable:迭代器不是fail - fast的,在迭代过程中集合结构被修改,不会立即抛出异常,但可能导致迭代结果不准确。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "a");
hashtable.put(2, "b");
Enumeration<Map.Entry<Integer, String>> enumeration = Collections.enumeration(hashtable.entrySet());
while (enumeration.hasMoreElements()) {
Map.Entry<Integer, String> entry = enumeration.nextElement();
// 这里如果调用hashtable.put(3, "c");不会立即抛出异常
}
4. 默认容量和扩容机制
- HashMap:默认初始容量为16,负载因子为0.75。当HashMap中的元素个数超过容量 负载因子(即16 0.75 = 12)时,会进行扩容,新容量为原容量的2倍。例如:
HashMap<Integer, String> hashMap = new HashMap<>();
for (int i = 0; i < 13; i++) {
hashMap.put(i, "value" + i);
// 当添加到第13个元素时,会触发扩容
}
- HashTable:默认初始容量为11,负载因子为0.75。当HashTable中的元素个数超过容量 负载因子(即11 0.75,取整后为8)时,会进行扩容,新容量为原容量的2倍 + 1(即11 * 2 + 1 = 23)。例如:
Hashtable<Integer, String> hashtable = new Hashtable<>();
for (int i = 0; i < 9; i++) {
hashtable.put(i, "value" + i);
// 当添加到第9个元素时,会触发扩容
}
三、HashSet和TreeSet的区别
1. 底层数据结构
- HashSet:基于HashMap实现,内部使用HashMap来存储元素,HashSet的元素实际上是作为HashMap的键来存储的,值是一个固定的Object对象。例如:
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(1);
hashSet.add(2);
// 内部通过HashMap存储,键为1和2,值为固定对象
- TreeSet:基于红黑树实现,红黑树是一种自平衡的二叉搜索树,能保证元素的有序性。例如:
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(3);
treeSet.add(1);
treeSet.add(2);
// 内部通过红黑树存储,元素会按照从小到大的顺序排列
2. 元素顺序
- HashSet:元素无序,元素的插入顺序和存储顺序不一定相同。例如:
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(3);
hashSet.add(1);
hashSet.add(2);
// 遍历hashSet时,元素顺序可能不是3, 1, 2
for (int i : hashSet) {
System.out.print(i + " ");
}
- TreeSet:元素有序,可以按照自然顺序(如数字从小到大、字符串按字典序)或通过实现Comparator接口来定义自定义顺序。例如:
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(3);
treeSet.add(1);
treeSet.add(2);
// 遍历treeSet时,元素顺序是1, 2, 3
for (int i : treeSet) {
System.out.print(i + " ");
}
3. 添加和查询性能
- HashSet:添加和查询操作平均时间复杂度为O(1),因为使用哈希表进行快速查找。例如:
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(1);
boolean contains = hashSet.contains(1); // 快速判断是否包含元素1,时间复杂度O(1)
- TreeSet:添加和查询操作时间复杂度为O(log n),因为红黑树的高度是对数级别的。例如:
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
boolean contains = treeSet.contains(1); // 判断是否包含元素1,时间复杂度O(log n)
4. 唯一性保证
- HashSet:通过hashCode()和equals()方法来保证元素的唯一性。当向HashSet中添加元素时,首先会计算元素的hashCode值,根据hashCode值确定元素在哈希表中的存储位置。如果该位置已经有元素存在,再通过equals()方法比较两个元素是否相等,如果相等则不添加。例如:
class CustomObject {
private int id;
public CustomObject(int id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
CustomObject other = (CustomObject) obj;
return id == other.id;
}
}
HashSet<CustomObject> hashSet = new HashSet<>();
hashSet.add(new CustomObject(1));
hashSet.add(new CustomObject(1)); // 由于hashCode和equals方法判断为相同元素,不会重复添加
- TreeSet:通过比较元素的顺序来保证唯一性,如果两个元素比较相等(通过自然顺序或自定义比较器判断),则不会同时存在于TreeSet中。例如:
class CustomComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
}
TreeSet<Integer> treeSet = new TreeSet<>(new CustomComparator());
treeSet.add(1);
treeSet.add(1); // 由于比较器判断为相同元素,不会重复添加
四、ConcurrentHashMap的实现原理
1. JDK 1.7版本
- 分段锁机制:ConcurrentHashMap将数据分成多个段(Segment),每个段相当于一个小的HashTable,都有自己独立的锁。在JDK 1.7中,默认有16个Segment。当一个线程对某个Segment进行写操作时,只会锁住这个Segment,其他线程仍然可以访问其他Segment,从而提高并发性能。例如:
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
// 多个线程可以同时对不同的Segment进行操作
new Thread(() -> {
concurrentHashMap.put(1, "a");
}).start();
new Thread(() -> {
concurrentHashMap.put(17, "b"); // 17和1可能位于不同的Segment,可并发操作
}).start();
- 数据结构:每个Segment内部是一个数组 + 链表的结构,和HashMap类似。当发生哈希冲突时,通过链表来解决。例如:
// 假设某个Segment的数组长度为4
Segment<K, V> segment = new Segment<>(16, 0.75f);
// 当有元素put进来时,如果哈希冲突,会在链表上进行存储
2. JDK 1.8版本
- CAS + Synchronized:摒弃了分段锁机制,采用CAS操作和Synchronized关键字相结合的方式。在JDK 1.8中,当多个线程进行读操作时,不需要加锁,可以直接并发进行。当进行写操作(如put)时,首先会尝试使用CAS操作来插入元素,如果CAS操作失败,再使用Synchronized关键字对当前节点进行加锁。例如:
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
// 多个线程读操作可并发
new Thread(() -> {
String value = concurrentHashMap.get(1);
}).start();
new Thread(() -> {
String value = concurrentHashMap.get(2);
}).start();
// 写操作先尝试CAS,失败后加锁
new Thread(() -> {
concurrentHashMap.put(3, "c");
}).start();
- 数据结构:和HashMap类似,采用数组 + 链表 + 红黑树的结构。当链表长度超过8时,会将链表转换为红黑树,以提高查找效率。例如:
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
// 当某个桶中的链表长度超过8时,会转换为红黑树
for (int i = 0; i < 10; i++) {
concurrentHashMap.put(i, "value" + i);
// 假设这些元素哈希冲突,都在同一个桶中,当添加到第9个元素时,链表可能转换为红黑树
}
五、如何遍历HashMap和HashTable
1. 遍历HashMap
(1)使用entrySet()遍历键值对
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
for (Map.Entry<Integer, String> entry : hashMap.entrySet()) {
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
(2)使用keySet()遍历键,再通过键获取值
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
for (Integer key : hashMap.keySet()) {
String value = hashMap.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}
(3)使用values()遍历值(只能获取值,无法获取键)
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
for (String value : hashMap.values()) {
System.out.println("Value: " + value);
}
(4)使用迭代器遍历
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "a");
hashMap.put(2, "b");
Iterator<Map.Entry<Integer, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
2. 遍历HashTable
(1)使用entrySet()遍历键值对
Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "a");
hashtable.put(2, "b");
for (Map.Entry<Integer, String> entry : hashtable.entrySet()) {
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
(2)使用keySet()遍历键,再通过键获取值
Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "a");
hashtable.put(2, "b");
for (Integer key : hashtable.keySet()) {
String value = hashtable.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}
Java 集合,ArrayList,HashMap,LinkedList,HashSet,ConcurrentHashMap, 集合框架,List,Set,Map,Collection,Vector,TreeMap,LinkedHashSet,Queue
代码获取方式
https://pan.quark.cn/s/14fcf913bae6