面试必备Java集合知识
Java集合框架是面试的重中之重,掌握这20道题,让你的面试如虎添翼!下面我将通过详细的解析、图表和代码示例带你深入理解Java集合框架的核心知识点。
📚 想了解更多Java集合框架知识和面试技巧?欢迎访问以下资源:
- 🌐 网站: 绘问IT学习平台
- 📱 微信公众号: 搜索"绘问",获取Java进阶干货和最新面试题库
graph TD
A[Java集合框架] --> B[Collection接口]
A --> C[Map接口]
B --> D[List接口]
B --> E[Set接口]
B --> F[Queue接口]
D --> G[ArrayList]
D --> H[LinkedList]
D --> I[Vector]
E --> J[HashSet]
E --> K[LinkedHashSet]
E --> L[TreeSet]
F --> M[PriorityQueue]
F --> N[ArrayDeque]
C --> O[HashMap]
C --> P[LinkedHashMap]
C --> Q[TreeMap]
C --> R[Hashtable]
C --> S[ConcurrentHashMap]
20道经典Java集合面试题详解
1. ArrayList和LinkedList的区别?
答案:
ArrayList和LinkedList是Java集合框架中最常用的两种List实现,它们有以下关键区别:
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层数据结构 | 动态数组 | 双向链表 |
| 随机访问效率 | O(1),支持高效随机访问 | O(n),需要遍历链表 |
| 插入/删除效率 | 尾部O(1),中间O(n)需要移动元素 | O(1),只需修改指针 |
| 内存占用 | 较少 | 较多,需额外存储前后引用 |
| 线程安全 | 非线程安全 | 非线程安全 |
// ArrayList示例
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Java"); // O(1) 尾部添加
arrayList.add(0, "Python"); // O(n) 需要移动元素
// LinkedList示例
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Java"); // O(1)
linkedList.addFirst("Python"); // O(1) 链表特有方法
实际应用选择:
- 频繁随机访问,较少插入删除操作:选择ArrayList
- 频繁在任意位置插入删除:选择LinkedList
- 频繁在两端添加删除:可以考虑LinkedList或ArrayDeque
2. HashMap的底层实现原理?
答案:
HashMap是Java中使用最广泛的Map实现,基于哈希表实现。
graph TD
A[HashMap结构] --> B[数组 + 链表 + 红黑树]
B --> C[数组: Entry/Node类型]
C --> D[链表: 解决哈希冲突]
D --> E[红黑树: 当链表长度>8时转换]
实现原理:
-
数据结构:Java 8前使用数组+链表,Java 8后使用数组+链表+红黑树
-
哈希计算:
- 计算key的hashCode
- 对hashCode进行扰动处理:
hash = key.hashCode() ^ (key.hashCode() >>> 16) - 计算数组下标:
index = hash & (capacity - 1)
-
解决哈希冲突:
- 链表法:相同哈希值的元素形成链表
- 当链表长度超过8且数组长度超过64时,链表转为红黑树
- 当红黑树节点数小于6时,红黑树转回链表
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 重要常量
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子
static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的阈值
// 节点定义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希值
final K key;
V value;
Node<K,V> next; // 链表下一个节点
// 构造方法和其他方法...
}
// 哈希表,在第一次使用时初始化
transient Node<K,V>[] table;
// 键值对数量
transient int size;
// 扩容阈值 = 容量 * 负载因子
int threshold;
// 负载因子
final float loadFactor;
// 其他实现...
}
扩容过程:
- 当键值对数量超过阈值(capacity * loadFactor)时触发扩容
- 创建新数组,容量为原来的2倍
- 重新计算每个元素在新数组中的位置
- Java 8优化:元素要么在原位置,要么在原位置+oldCap
3. ConcurrentHashMap如何实现线程安全?
答案:
ConcurrentHashMap是HashMap的线程安全版本,其实现方式随Java版本变化:
Java 7实现:分段锁(Segment)
- 将哈希表分成多个段(Segment),每个段拥有独立的锁
- 每个Segment相当于一个小型的HashMap
- 并发度默认为16,即支持16个线程并发写
- 读操作无锁,写操作仅锁定对应段
Java 8实现:CAS + synchronized
- 移除了Segment设计,直接使用Node数组
- 写操作:
- 使用CAS无锁操作尝试更新
- 失败则使用synchronized锁定当前桶
- 读操作无锁
- 红黑树同样用于优化冲突链表
// Java 8 ConcurrentHashMap简化示例
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V> {
// 节点定义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
// 构造方法和其他方法...
}
// 存储桶数组,使用volatile保证可见性
transient volatile Node<K,V>[] table;
// 当前大小计数器,使用LongAdder替代AtomicLong
private transient CounterCell[] counterCells;
private transient volatile long baseCount;
// 其他实现...
// put操作核心方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 计算哈希值
// 2. 如果桶为空,使用CAS创建新桶
// 3. 如果桶不为空,使用synchronized锁定当前桶
// 4. 在加锁情况下进行更新操作
// 5. 必要时检查是否需要转为红黑树
// 6. 必要时检查是否需要扩容
}
}
相比Hashtable优势:
- 粒度更细的锁,支持更高的并发度
- 读操作无锁,性能更好
- 扩容时支持并发操作
4. HashSet如何保证元素唯一性?
答案:
HashSet内部使用HashMap实现,保证元素唯一性的机制如下:
- 底层实现:HashSet内部包含一个HashMap对象
- 元素存储方式:将添加的元素作为HashMap的key,使用一个固定的Object对象作为value
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
private transient HashMap<E,Object> map;
// 所有HashSet共享的同一个value对象
private static final Object PRESENT = new Object();
// 构造方法
public HashSet() {
map = new HashMap<>();
}
// 添加元素
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
// 判断是否包含元素
public boolean contains(Object o) {
return map.containsKey(o);
}
// 其他方法...
}
唯一性保证机制:
-
添加元素时:
- 计算元素的hashCode()
- 检查是否有hashCode冲突
- 如有冲突,使用equals()方法比较
-
自定义对象作为元素时:
- 必须正确重写hashCode()和equals()方法
- 相等的对象必须返回相同的hashCode
- equals()返回true的两个对象,hashCode()必须相同
// 自定义类正确实现hashCode和equals
public class Person {
private String name;
private int age;
// 构造函数和getter/setter略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
5. TreeMap和HashMap有什么区别?
答案:
TreeMap和HashMap都实现了Map接口,但有以下关键区别:
| 特性 | HashMap | TreeMap |
|---|---|---|
| 数据结构 | 哈希表(数组+链表+红黑树) | 红黑树 |
| 有序性 | 无序 | 根据键的自然顺序或比较器排序 |
| 性能 | 平均O(1)查找/插入/删除 | O(log n)查找/插入/删除 |
| Null键 | 允许一个null键 | 不允许null键 |
| 使用场景 | 不关心顺序,追求性能 | 需要按键排序 |
| 内存占用 | 相对较少 | 相对较多 |
// HashMap示例 - 无序
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("banana", 2);
hashMap.put("apple", 5);
hashMap.put("orange", 3);
// 输出可能是任意顺序
System.out.println(hashMap); // 可能是 {apple=5, orange=3, banana=2} 或其他顺序
// TreeMap示例 - 有序
TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put("banana", 2);
treeMap.put("apple", 5);
treeMap.put("orange", 3);
// 输出一定是按键的字母顺序
System.out.println(treeMap); // 一定是 {apple=5, banana=2, orange=3}
// TreeMap的特殊方法示例
System.out.println(treeMap.firstKey()); // apple
System.out.println(treeMap.lastKey()); // orange
System.out.println(treeMap.floorKey("cherry")); // banana
System.out.println(treeMap.ceilingKey("cherry")); // orange
TreeMap的额外特性:
- 提供了按范围操作的方法:subMap(), headMap(), tailMap()
- 提供了查找最近键的方法:floorKey(), ceilingKey(), lowerKey(), higherKey()
- 可以使用自定义Comparator指定排序规则
// 自定义排序规则的TreeMap
TreeMap<String, Integer> customTreeMap = new TreeMap<>((s1, s2) -> s2.compareTo(s1)); // 逆序
customTreeMap.put("banana", 2);
customTreeMap.put("apple", 5);
customTreeMap.put("orange", 3);
// 输出将按逆字母顺序
System.out.println(customTreeMap); // {orange=3, banana=2, apple=5}
6. ArrayList的扩容机制是怎样的?
答案:
ArrayList基于动态数组实现,当容量不足时会触发扩容机制:
扩容过程:
- 默认初始容量为10(懒加载,首次添加元素时创建)
- 当元素数量达到容量上限时触发扩容
- 扩容策略:
- Java 6:新容量 = 旧容量 * 3/2 + 1
- Java 7+:新容量 = 旧容量 + 旧容量 >> 1(即1.5倍)
- 创建更大的新数组,复制原数组元素
- 如果指定的最小容量大于上述计算结果,使用指定的最小容量
// ArrayList源码中的扩容核心逻辑(简化版)
private void grow(int minCapacity) {
// 旧容量
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量/2 (即1.5倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量仍小于最小需求容量,则使用最小需求容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超过最大数组大小,则使用Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 创建新数组并复制数据
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList扩容性能影响:
- 扩容操作开销较大,需要创建新数组并复制元素
- 频繁扩容会导致性能下降
- 当预知集合大小时,建议使用带初始容量的构造函数
// 预设容量,避免扩容
ArrayList<String> list = new ArrayList<>(10000);
// 性能对比图示
gantt
title ArrayList扩容性能对比
dateFormat s
axisFormat %S
section 无预设容量
添加10000个元素 :a1, 0, 3s
其中扩容消耗 :a2, 0, 1s
section 预设容量
添加10000个元素 :b1, 0, 2s
7. 如何实现线程安全的List?
答案:
Java提供了多种方式实现线程安全的List:
-
Vector:
- 早期的线程安全List实现
- 所有方法都使用synchronized同步
- 性能较差,不推荐使用
-
Collections.synchronizedList:
- 将普通List包装成线程安全的List
- 所有方法都使用同一个锁对象同步
- 比Vector稍好,但并发性能仍然有限
-
CopyOnWriteArrayList:
- 写时复制策略,适合读多写少场景
- 写操作创建新数组,不影响读操作
- 读操作无锁,性能好
- 写操作开销大,不适合频繁写入
-
自定义加锁:
- 根据需求对ArrayList的操作加锁
- 可以使用ReentrantLock等实现细粒度锁
// Vector示例
Vector<String> vector = new Vector<>();
// Collections.synchronizedList示例
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// CopyOnWriteArrayList示例
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
// 自定义锁示例
public class CustomThreadSafeList<E> {
private final ArrayList<E> list = new ArrayList<>();
private final ReentrantLock lock = new ReentrantLock();
public void add(E element) {
lock.lock();
try {
list.add(element);
} finally {
lock.unlock();
}
}
public E get(int index) {
lock.lock();
try {
return list.get(index);
} finally {
lock.unlock();
}
}
// 其他方法...
}
性能对比:
bar
title 不同线程安全List实现读写性能对比
"读操作" : 90, 85, 98, 88
"写操作" : 60, 65, 40, 70
数据从左到右依次为:Vector, SynchronizedList, CopyOnWriteArrayList, 自定义锁
选择建议:
- 读多写少:首选CopyOnWriteArrayList
- 读写均衡:考虑Collections.synchronizedList或自定义加锁
- 高并发场景:考虑并发集合框架中其他实现或自定义解决方案
8. HashMap和Hashtable的区别?
答案:
HashMap和Hashtable都实现了Map接口,但有几个关键区别:
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全,方法使用synchronized修饰 |
| null值支持 | 允许null键和null值 | 不允许null键和null值 |
| 性能 | 较好 | 较差(同步开销) |
| 迭代器类型 | fail-fast | fail-fast |
| 扩容方式 | 默认容量16,扩容为2倍 | 默认容量11,扩容为2n+1 |
| 继承关系 | 继承AbstractMap | 继承Dictionary类 |
| 推荐使用 | 单线程环境 | 几乎不推荐使用,建议用ConcurrentHashMap |
// HashMap示例
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put(null, 100); // 允许null键
hashMap.put("key", null); // 允许null值
// Hashtable示例
Hashtable<String, Integer> hashtable = new Hashtable<>();
// hashtable.put(null, 100); // 抛出NullPointerException
// hashtable.put("key", null); // 抛出NullPointerException
历史和演进:
- Hashtable是Java 1.0引入的遗留类
- HashMap是Java 1.2引入,设计更现代
- Java现代程序应避免使用Hashtable
- 需要线程安全时,应使用ConcurrentHashMap而非Hashtable
// 现代线程安全Map的推荐用法
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
9. fail-fast和fail-safe迭代器的区别?
答案:
fail-fast迭代器:
- 在迭代过程中,如果集合结构被修改,立即抛出ConcurrentModificationException
- 通过modCount计数器实现
- 大多数集合(如ArrayList、HashMap等)采用这种机制
- 目的是立即检测并发修改问题
fail-safe迭代器:
- 在迭代过程中,即使集合结构被修改,也不抛出异常
- 通常通过操作底层集合的副本实现
- 常见于并发集合(如CopyOnWriteArrayList、ConcurrentHashMap)
- 缺点是可能看不到最新修改的数据
// fail-fast示例
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 错误使用方式,将抛出ConcurrentModificationException
try {
for (String item : list) {
if ("Java".equals(item)) {
list.remove(item); // 直接修改集合
}
}
} catch (ConcurrentModificationException e) {
System.out.println("发生并发修改异常");
}
// 正确使用方式1:使用迭代器的remove方法
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("Python".equals(item)) {
iterator.remove(); // 安全移除
}
}
// 正确使用方式2:使用removeIf
list.removeIf(item -> "C++".equals(item));
// fail-safe示例
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("Java");
cowList.add("Python");
// 不会抛出异常,但迭代器看不到新添加的元素
for (String item : cowList) {
cowList.add("C++"); // 不会影响当前迭代
System.out.println(item); // 只会打印"Java"和"Python"
}
工作原理对比:
graph TB
A[迭代器创建] --> B{迭代过程中集合被修改?}
B -->|是| C{迭代器类型?}
C -->|fail-fast| D[抛出ConcurrentModificationException]
C -->|fail-safe| E[继续迭代,但可能看不到修改]
B -->|否| F[正常完成迭代]
10. LinkedHashMap和HashMap的区别?
答案:
LinkedHashMap是HashMap的子类,它在HashMap基础上维护了一个双向链表,用于保持插入顺序或访问顺序。
主要区别:
| 特性 | HashMap | LinkedHashMap |
|---|---|---|
| 有序性 | 无序 | 有序(插入顺序或访问顺序) |
| 实现方式 | 哈希表 | 哈希表 + 双向链表 |
| 性能 | 插入/查找O(1) | 插入/查找O(1),但有额外开销 |
| 内存消耗 | 较少 | 较多(需存储链表指针) |
| 迭代性能 | 较差 | 较好(直接遍历链表) |
| LRU缓存实现 | 不支持 | 支持(访问顺序模式) |
LinkedHashMap的底层实现:
- 继承HashMap,复用其哈希表结构
- 增加了双向链表维护Entry顺序
- 每个Entry多了before和after引用
- 维护head和tail引用指向链表首尾
// LinkedHashMap简化结构
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
// 链表头节点
transient LinkedHashMap.Entry<K,V> head;
// 链表尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 是否按访问顺序排序
final boolean accessOrder;
// 节点类,继承自HashMap.Node
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 双向链表指针
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// 其他实现...
}
LinkedHashMap的两种排序模式:
- 插入顺序(默认):元素的顺序与插入顺序一致
- 访问顺序:最近访问的元素会移到链表末尾,可用于实现LRU缓存
// 插入顺序示例
LinkedHashMap<String, Integer> insertOrderMap = new LinkedHashMap<>();
insertOrderMap.put("A", 1);
insertOrderMap.put("B", 2);
insertOrderMap.put("C", 3);
System.out.println(insertOrderMap); // 输出顺序: A=1, B=2, C=3
// 访问顺序示例
LinkedHashMap<String, Integer> accessOrderMap =
new LinkedHashMap<>(16, 0.75f, true); // 最后参数true表示按访问顺序
accessOrderMap.put("A", 1);
accessOrderMap.put("B", 2);
accessOrderMap.put("C", 3);
accessOrderMap.get("A"); // 访问A
System.out.println(accessOrderMap); // 输出顺序: B=2, C=3, A=1 (A被移到最后)
// LRU缓存实现
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
11. Java集合中的equals和hashCode有什么关系?
答案:
在Java集合框架中,equals()和hashCode()方法密切相关,尤其是在使用HashMap、HashSet等基于哈希的集合时:
两者的关系:
- 如果两个对象相等(equals()返回true),则它们的hashCode()必须相同
- 如果两个对象的hashCode()相同,它们不一定相等
- 重写equals()方法时,必须同时重写hashCode()方法
违反上述规则的后果:
- 如果只重写equals()不重写hashCode(),对象在基于哈希的集合中可能表现不正确
- 可能导致相等对象无法找到、重复存储等问题
在集合中的应用过程:
- HashMap/HashSet添加元素:
- 计算元素的hashCode()
- 确定在哈希表中的位置
- 如有哈希冲突,使用equals()比较
- HashMap/HashSet查找元素:
- 计算查找对象的hashCode()
- 找到哈希表中对应的桶
- 遍历桶中元素,使用equals()比较
// 不正确实现的示例
class BadPerson {
private String name;
private int age;
// 构造函数略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BadPerson that = (BadPerson) o;
return age == that.age && Objects.equals(name, that.name);
}
// 错误:没有重写hashCode()
}
// 正确实现的示例
class GoodPerson {
private String name;
private int age;
// 构造函数略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GoodPerson that = (GoodPerson) o;
return age == that.age && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
// 测试代码
public void testHashCodeEquals() {
// 使用不正确实现的类
BadPerson p1 = new BadPerson("John", 20);
BadPerson p2 = new BadPerson("John", 20);
HashSet<BadPerson> badSet = new HashSet<>();
badSet.add(p1);
System.out.println(badSet.contains(p2)); // 可能返回false,尽管equals认为它们相等
// 使用正确实现的类
GoodPerson g1 = new GoodPerson("John", 20);
GoodPerson g2 = new GoodPerson("John", 20);
HashSet<GoodPerson> goodSet = new HashSet<>();
goodSet.add(g1);
System.out.println(goodSet.contains(g2)); // 返回true
}
hashCode()的一般规则:
- 相同状态的对象必须产生相同的哈希码
- hashCode()应该尽量分散,减少冲突
- 计算应高效,不应有太大开销
- 使用所有关键字段,但可以排除派生字段
12. 什么是PriorityQueue,它是如何实现的?
答案:
PriorityQueue是一个基于优先级堆的队列实现,它可以根据元素的自然顺序或Comparator来确定优先级。
PriorityQueue特性:
- 非FIFO队列,元素按优先级出队
- 默认是最小堆,堆顶是最小元素
- 底层实现是二叉堆(完全二叉树)
- 通过数组表示二叉堆
- 不允许null元素
- 不是线程安全的
核心操作时间复杂度:
- 入队(offer/add):O(log n)
- 出队(poll):O(log n)
- 查看堆顶(peek):O(1)
底层数组表示法:
- 对于索引为i的元素:
- 父节点索引:(i-1)/2
- 左子节点索引:2*i + 1
- 右子节点索引:2*i + 2
public class PriorityQueue<E> extends AbstractQueue<E> {
// 存储元素的数组
transient Object[] queue;
// 队列中元素的数量
private int size;
// 比较器,如果为null则使用元素的自然顺序
private final Comparator<? super E> comparator;
// 默认初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 构造函数、添加/删除元素的方法等
}
上移和下移操作:
- 上移(siftUp):添加元素时,将元素放在数组末尾,然后上移到正确位置
- 下移(siftDown):移除堆顶元素时,将最后一个元素移到堆顶,然后下移到正确位置
// PriorityQueue使用示例
PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // 默认最小堆
minHeap.offer(5);
minHeap.offer(3);
minHeap.offer(8);
minHeap.offer(1);
while (!minHeap.isEmpty()) {
System.out.print(minHeap.poll() + " "); // 输出: 1 3 5 8
}
// 最大堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
maxHeap.offer(5);
maxHeap.offer(3);
maxHeap.offer(8);
maxHeap.offer(1);
while (!maxHeap.isEmpty()) {
System.out.print(maxHeap.poll() + " "); // 输出: 8 5 3 1
}
堆结构可视化:
graph TD
A[1] --> B[3]
A --> C[5]
B --> D[8]
上图展示了包含元素1,3,5,8的最小堆结构
13. 集合中的Iterator和ListIterator有什么区别?
答案:
Iterator和ListIterator都是Java集合框架中的迭代器接口,但ListIterator提供了更多功能:
| 特性 | Iterator | ListIterator |
|---|---|---|
| 适用集合 | 所有Collection | 仅List类型集合 |
| 遍历方向 | 只能向前 | 支持向前和向后 |
| 元素位置 | 不支持获取位置 | 支持获取索引位置 |
| 添加元素 | 不支持 | 支持在遍历过程中添加 |
| 修改元素 | 不支持 | 支持在遍历过程中修改 |
| 获取方法 | 只有next() | 有next()和previous() |
Iterator接口:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
// 实现略
}
}
ListIterator接口:
public interface ListIterator<E> extends Iterator<E> {
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void remove();
void set(E e);
void add(E e);
}
使用示例:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// Iterator使用示例
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
// iterator.add("X"); // 编译错误,Iterator不支持添加
}
// ListIterator使用示例
ListIterator<String> listIterator = list.listIterator();
// 向前遍历
while (listIterator.hasNext()) {
int index = listIterator.nextIndex();
String item = listIterator.next();
System.out.println("索引" + index + ": " + item);
if ("B".equals(item)) {
listIterator.set("修改后的B"); // 修改当前元素
listIterator.add("B之后"); // 在当前位置之后添加
}
}
// 向后遍历
while (listIterator.hasPrevious()) {
String item = listIterator.previous();
System.out.println("反向: " + item);
}
适用场景:
- 只需要向前遍历时,使用Iterator
- 需要双向遍历或修改列表时,使用ListIterator
- 需要知道元素位置时,使用ListIterator
14. 什么是BlockingQueue,Java中有哪些实现?
答案:
BlockingQueue是Java并发包(java.util.concurrent)中的一个接口,它扩展了Queue接口,提供了阻塞操作:当队列为空时获取元素的线程会等待队列变为非空;当队列满时添加元素的线程会等待队列有空间。
BlockingQueue核心方法:
| 操作类型 | 抛出异常 | 返回特殊值 | 阻塞 | 超时 |
|---|---|---|---|---|
| 插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 移除 | remove() | poll() | take() | poll(time, unit) |
| 检查 | element() | peek() | 不适用 | 不适用 |
Java中BlockingQueue的主要实现:
-
ArrayBlockingQueue
- 基于数组的有界队列
- 构造时需指定容量
- 支持公平/非公平策略
- 适用于固定大小的场景
-
LinkedBlockingQueue
- 基于链表的可选有界队列
- 不指定容量时为Integer.MAX_VALUE
- 通常性能优于ArrayBlockingQueue
- 适用于消费者生产者速率不匹配场景
-
PriorityBlockingQueue
- 带优先级的无界队列
- 元素按优先级出队
- 不保证同优先级元素顺序
- 适用于按优先级处理任务场景
-
DelayQueue
- 延迟队列,元素在指定延迟后才能取出
- 元素需实现Delayed接口
- 适用于定时任务调度
-
SynchronousQueue
- 容量为0的队列
- 每个put操作必须等待take操作
- 适用于"手递手"场景
-
LinkedTransferQueue
- 基于链表的无界队列
- 支持转移语义,生产者可能等待消费者接收元素
- Java 7引入
// ArrayBlockingQueue示例
BlockingQueue<String> arrayQueue = new ArrayBlockingQueue<>(3); // 容量为3的队列
// 生产者线程
new Thread(() -> {
try {
arrayQueue.put("任务1"); // 阻塞式添加
arrayQueue.put("任务2");
arrayQueue.put("任务3");
System.out.println("队列已满,等待空间...");
arrayQueue.put("任务4"); // 队列已满,将阻塞
System.out.println("任务4已添加");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
Thread.sleep(2000); // 延迟2秒
System.out.println("消费: " + arrayQueue.take()); // 消费任务1
System.out.println("消费: " + arrayQueue.take()); // 消费任务2
System.out.println("生产者线程应该解除阻塞");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
BlockingQueue的应用场景:
- 生产者-消费者模式
- 线程池任务队列
- 消息缓冲区
- 数据交换
graph LR
A[生产者线程] -->|put| B[BlockingQueue]
B -->|take| C[消费者线程]
B -->|队列满: 阻塞生产者| A
B -->|队列空: 阻塞消费者| C
15. WeakHashMap和HashMap有什么区别?
答案:
WeakHashMap是HashMap的一种特殊变体,它使用弱引用(weak references)保存键,这意味着当键对象不再被其他对象引用时,相应的键值对会被自动从映射中移除。
主要区别:
| 特性 | HashMap | WeakHashMap |
|---|---|---|
| 引用类型 | 强引用 | 键是弱引用 |
| 垃圾回收 | 不受GC影响 | 键在仅被WeakHashMap引用时会被回收 |
| 内部实现 | 普通Entry | WeakReference包装的Entry |
| 应用场景 | 一般映射 | 缓存、解决内存泄漏问题 |
| 性能 | 更好 | 略差(需要处理已被回收的键) |
工作原理:
- WeakHashMap中的键被包装为WeakReference
- 当键对象仅被WeakReference引用时,下次GC会回收此对象
- ReferenceQueue用于跟踪已被回收的键
- 在每次操作(如put/get)前,先清理已失效的Entry
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
// 引用队列,用于跟踪已被回收的键
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// Entry继承自WeakReference
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
// 构造函数和其他方法...
}
// 在大多数操作前清理已失效的Entry
private void expungeStaleEntries() {
// 实现略
}
// 其他方法...
}
使用示例:
// 强引用的HashMap
HashMap<Key, String> hashMap = new HashMap<>();
Key key1 = new Key("hashMap");
hashMap.put(key1, "HashMap值");
key1 = null; // 将key1设为null
System.gc(); // 触发垃圾回收
System.out.println("HashMap大小: " + hashMap.size()); // 输出1,键值对仍存在
// 弱引用的WeakHashMap
WeakHashMap<Key, String> weakMap = new WeakHashMap<>();
Key key2 = new Key("weakMap");
weakMap.put(key2, "WeakHashMap值");
key2 = null; // 将key2设为null
System.gc(); // 触发垃圾回收
System.out.println("WeakHashMap大小: " + weakMap.size()); // 输出0,键值对已被回收
// 测试用Key类
static class Key {
private String id;
public Key(String id) { this.id = id; }
// equals, hashCode方法略
}
适用场景:
- 缓存实现:不想缓存阻止对象被垃圾回收
- 注册回调:当客户端不再可访问时自动移除回调
- 防止内存泄漏:在引用关系复杂的场景下避免强引用导致的内存泄漏
注意事项:
- 仅键是弱引用,值仍是强引用
- 如果值引用键,键不会被回收
- 回收行为依赖于垃圾收集器运行时间
- 不适用于生命周期可预测的对象
16. EnumMap和HashMap有什么区别?
答案:
EnumMap是一种专门用于枚举类型键的Map实现,相比于HashMap有很多特殊优化:
主要区别:
| 特性 | EnumMap | HashMap |
|---|---|---|
| 键类型 | 仅限枚举类型 | 任意Object(实现hashCode/equals) |
| 内部实现 | 数组 | 哈希表(数组+链表+红黑树) |
| 性能 | 非常高效,O(1)操作 | 一般高效,但有哈希冲突开销 |
| 内存占用 | 非常紧凑 | 较高 |
| 迭代顺序 | 保证枚举常量的定义顺序 | 无序 |
| 允许null键 | 不允许 | 允许一个null键 |
EnumMap工作原理:
- 内部使用数组实现,数组大小为枚举类型常量数
- 通过枚举的ordinal()值作为数组索引
- 不需要计算hashCode或处理冲突
- 所有操作基本都是O(1)时间复杂度
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements Serializable, Cloneable {
// 枚举类
private final Class<K> keyType;
// 值数组,大小等于枚举常量数
private transient Object[] vals;
// 实际大小
private transient int size = 0;
// 其他实现...
}
使用示例:
// 枚举定义
enum DayOfWeek {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
// EnumMap示例
EnumMap<DayOfWeek, String> daySchedule = new EnumMap<>(DayOfWeek.class);
daySchedule.put(DayOfWeek.MONDAY, "工作日");
daySchedule.put(DayOfWeek.TUESDAY, "工作日");
daySchedule.put(DayOfWeek.SATURDAY, "周末");
daySchedule.put(DayOfWeek.SUNDAY, "周末");
// 迭代顺序与枚举定义顺序一致
for (Map.Entry<DayOfWeek, String> entry : daySchedule.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 高效判断
System.out.println("周一安排: " + daySchedule.get(DayOfWeek.MONDAY));
// HashMap对比
HashMap<DayOfWeek, String> dayHashMap = new HashMap<>();
// 使用相同,但性能和内存占用不如EnumMap
dayHashMap.put(DayOfWeek.MONDAY, "工作日");
EnumMap的优势:
- 性能优势:直接数组访问,无需哈希计算
- 内存效率:内存占用低,没有哈希表开销
- 有序性:保证枚举定义顺序
- 类型安全:编译时检查键类型
- 不需要关注hashCode/equals:枚举已正确实现
适用场景:
- 需要使用枚举作为键的任何场景
- 高性能要求的应用
- 内存敏感的应用
- 需要保持枚举顺序的场景
17. 如何理解Java中的Collections工具类?
答案:
Collections是Java集合框架中的工具类,提供了丰富的静态方法来操作和创建集合,类似于数组的Arrays工具类。
主要功能分类:
-
集合包装
- 同步包装:将普通集合包装为线程安全版本
- 不可修改包装:防止集合被修改
- 类型安全包装:强制集合只能包含特定类型的对象
-
集合算法
- 排序
- 查找(二分查找)
- 洗牌(随机排序)
- 反转
- 旋转
- 交换
-
集合工厂方法
- 空集合
- 单例集合
- Java 9后提供的不可变集合工厂方法
-
其他工具方法
- 最大/最小值查找
- 频率统计
- 集合间操作(并集、交集等)
// 1. 同步包装
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// 2. 不可修改包装
List<String> readOnlyList = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
// readOnlyList.add("D"); // 抛出UnsupportedOperationException
// 3. 排序
List<Integer> numbers = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
Collections.sort(numbers); // 自然顺序排序
System.out.println(numbers); // [1, 1, 3, 4, 5, 9]
Collections.sort(numbers, Collections.reverseOrder()); // 逆序排序
System.out.println(numbers); // [9, 5, 4, 3, 1, 1]
// 4. 二分查找(要求已排序)
Collections.sort(numbers);
int index = Collections.binarySearch(numbers, 4);
System.out.println("4的索引: " + index);
// 5. 洗牌(随机排序)
Collections.shuffle(numbers);
System.out.println("洗牌后: " + numbers);
// 6. 频率统计
int frequency = Collections.frequency(numbers, 1);
System.out.println("1出现的次数: " + frequency);
// 7. 交换元素
Collections.swap(numbers, 0, 1);
// 8. 填充
Collections.fill(numbers, 0); // 所有元素设为0
// 9. 最值查找
List<Integer> data = Arrays.asList(3, 1, 4, 1, 5, 9);
int max = Collections.max(data);
int min = Collections.min(data);
System.out.println("最大值: " + max + ", 最小值: " + min);
// 10. 单例和空集合
Set<String> singleton = Collections.singleton("唯一元素");
List<Object> emptyList = Collections.emptyList();
Map<String, Integer> emptyMap = Collections.emptyMap();
// 11. Java 9+的不可变集合工厂方法
List<String> list = List.of("A", "B", "C");
Set<String> set = Set.of("X", "Y", "Z");
Map<String, Integer> map = Map.of("one", 1, "two", 2);
使用建议:
- 优先使用Collections工具类中的算法,而非自己实现
- 需要线程安全时,考虑使用同步包装或并发集合类
- 返回集合给客户端时,考虑使用不可修改包装
- Java 9+项目中,优先使用新的工厂方法创建不可变集合
- 不要在循环中重复创建空集合,使用Collections.empty*方法
18. 什么是Comparable和Comparator接口?
答案:
Comparable和Comparator是Java中用于对象排序的两个接口,它们有不同的用途和使用场景:
Comparable接口:
- 位于java.lang包中
- 一个类实现该接口表示"该类对象可以与自身比较"
- 只有一个方法:
compareTo(T o) - 提供类的默认排序方式
- 使用
Collections.sort(List)或Arrays.sort(Object[])时使用此排序
Comparator接口:
- 位于java.util包中
- 独立的比较器类
- 核心方法:
compare(T o1, T o2) - 提供额外的排序逻辑
- 使用
Collections.sort(List, Comparator)或Arrays.sort(Object[], Comparator)时使用
| 特性 | Comparable | Comparator |
|---|---|---|
| 接口定义 | interface Comparable<T> | interface Comparator<T> |
| 核心方法 | int compareTo(T o) | int compare(T o1, T o2) |
| 实现者 | 要比较的类自身 | 独立的比较器类 |
| 修改类 | 需要修改源代码 | 不需要修改被比较类 |
| 实现数量 | 一个类只能有一种实现 | 一个类可以有多个比较器 |
| 使用时机 | 类有明确的自然顺序 | 需要多种排序方式或无法修改类 |
Comparable示例:
// 实现Comparable接口的类
class Person implements Comparable<Person> {
private String name;
private int age;
// 构造函数略
// 按年龄升序排序
@Override
public int compareTo(Person other) {
return this.age - other.age;
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
// 使用Comparable进行排序
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 25));
people.add(new Person("Bob", 20));
people.add(new Person("Charlie", 30));
Collections.sort(people); // 使用Person类的自然顺序(年龄)
System.out.println(people); // 按年龄排序: Bob(20), Alice(25), Charlie(30)
Comparator示例:
// 使用Comparator创建不同的排序规则
// 1. 按姓名排序的比较器
Comparator<Person> byName = new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
};
// 2. 按年龄降序排序的比较器(Lambda表达式)
Comparator<Person> byAgeDesc = (p1, p2) -> p2.getAge() - p1.getAge();
// 使用比较器排序
Collections.sort(people, byName); // 按姓名排序
System.out.println("按姓名: " + people);
Collections.sort(people, byAgeDesc); // 按年龄降序
System.out.println("按年龄降序: " + people);
// 3. 组合比较器
Comparator<Person> byAgeAscThenNameDesc = Comparator
.comparing(Person::getAge) // 首先按年龄升序
.thenComparing(Person::getName, Comparator.reverseOrder()); // 然后按姓名降序
Collections.sort(people, byAgeAscThenNameDesc);
Java 8后的Comparator增强:
- 静态工厂方法:
comparing(),comparingInt()等 - 默认方法:
reversed(),thenComparing()等 - 更易读的链式API
// Java 8 Comparator方法
Comparator<Person> modern = Comparator
.comparingInt(Person::getAge) // 基本类型优化
.reversed() // 反转排序
.thenComparing(Person::getName) // 多级排序
.thenComparing(p -> p.getAddress().getCity()); // 嵌套属性
使用建议:
- 当一个类有明确的自然排序时实现Comparable
- 需要多种排序方式时使用Comparator
- 无法修改源代码时使用Comparator
- Java 8+项目中使用Comparator接口的新方法简化代码
19. 如何正确使用HashMap的自定义对象键?
答案:
在HashMap中使用自定义对象作为键时,必须正确实现hashCode()和equals()方法,否则可能导致意外的行为,如键查找失败、重复键等问题。
正确实现自定义键的步骤:
-
正确实现equals()方法:
- 遵循自反性、对称性、传递性、一致性原则
- 包含所有影响对象相等性的字段
- 考虑null值处理
- 考虑继承关系
-
正确实现hashCode()方法:
- 相等对象必须返回相同哈希码
- 使用相同的字段计算哈希码
- 尽量分散哈希值以减少冲突
- 考虑性能因素
-
保持不可变性(推荐):
- 键对象最好是不可变的
- 可变对象在修改后哈希码可能变化
错误示例:
// 错误实现:只重写了equals而未重写hashCode
class BadKey {
private int id;
private String name;
// 构造函数略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BadKey badKey = (BadKey) o;
return id == badKey.id && Objects.equals(name, badKey.name);
}
// 错误:没有重写hashCode()
}
// 使用错误的键
BadKey key1 = new BadKey(1, "test");
BadKey key2 = new BadKey(1, "test"); // 与key1逻辑相等
HashMap<BadKey, String> map = new HashMap<>();
map.put(key1, "值1");
System.out.println(map.get(key2)); // 可能返回null,尽管key2与key1相等
正确示例:
// 正确实现:同时重写equals和hashCode
class GoodKey {
private final int id; // 使用final使字段不可变
private final String name;
public GoodKey(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GoodKey goodKey = (GoodKey) o;
return id == goodKey.id && Objects.equals(name, goodKey.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
// 使用正确的键
GoodKey key1 = new GoodKey(1, "test");
GoodKey key2 = new GoodKey(1, "test"); // 与key1逻辑相等
HashMap<GoodKey, String> map = new HashMap<>();
map.put(key1, "值1");
System.out.println(map.get(key2)); // 正确返回"值1"
可变键的危险:
// 可变对象作为键的问题
class MutableKey {
private int id;
private String name;
// 构造函数略
public void setId(int id) { this.id = id; }
public void setName(String name) { this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MutableKey that = (MutableKey) o;
return id == that.id && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
// 演示可变键的问题
MutableKey key = new MutableKey(1, "original");
HashMap<MutableKey, String> map = new HashMap<>();
map.put(key, "value");
// 修改键
key.setName("modified");
// 尝试获取
System.out.println(map.get(key)); // 返回null!无法找到键
最佳实践:
- 优先使用不可变类作为键(如String, Integer, UUID等)
- 自定义键类使用final字段并私有化
- 确保equals和hashCode使用相同字段集
- 考虑使用IDE或Lombok生成这些方法
- 如果必须使用可变对象作为键,确保在Map使用期间不修改其状态
20. 如何理解Java中的Deque双端队列?
答案:
Deque(Double-Ended Queue,读作"deck")是Java集合框架中的一个接口,表示双端队列,允许在队列的两端进行插入和删除操作。
Deque特性:
- 继承自Queue接口
- 支持在两端添加和移除元素
- 可以用作FIFO队列(先进先出)
- 可以用作LIFO栈(后进先出)
- 可以用作双向队列
核心方法分组:
| 操作类型 | 队列头部(First) | 队列尾部(Last) | 抛出异常? |
|---|---|---|---|
| 插入 | addFirst(e) | addLast(e) | 是 |
| 插入 | offerFirst(e) | offerLast(e) | 否 |
| 删除 | removeFirst() | removeLast() | 是 |
| 删除 | pollFirst() | pollLast() | 否 |
| 查看 | getFirst() | getLast() | 是 |
| 查看 | peekFirst() | peekLast() | 否 |
Deque的常用实现类:
- ArrayDeque:基于可调整大小的数组实现
- LinkedList:基于双向链表实现
- ConcurrentLinkedDeque:线程安全的基于链表的实现
// 创建Deque
Deque<String> deque = new ArrayDeque<>();
// 作为队列使用(FIFO)
deque.offer("First"); // 添加到队尾
deque.offer("Second");
deque.offer("Third");
System.out.println(deque.poll()); // 从队头移除:"First"
System.out.println(deque.poll()); // "Second"
// 作为栈使用(LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("First"); // 添加到队头
stack.push("Second");
stack.push("Third");
System.out.println(stack.pop()); // 从队头移除:"Third"
System.out.println(stack.pop()); // "Second"
// 作为双端队列使用
Deque<String> dualQueue = new ArrayDeque<>();
dualQueue.offerFirst("First"); // 添加到队头
dualQueue.offerLast("Last"); // 添加到队尾
dualQueue.offerFirst("NewFirst"); // 添加到队头
System.out.println(dualQueue.peekFirst()); // 查看队头:"NewFirst"
System.out.println(dualQueue.peekLast()); // 查看队尾:"Last"
System.out.println(dualQueue.pollFirst()); // 移除队头:"NewFirst"
System.out.println(dualQueue.pollLast()); // 移除队尾:"Last"
ArrayDeque vs LinkedList性能对比:
| 操作 | ArrayDeque | LinkedList |
|---|---|---|
| 添加/删除(两端) | O(1) | O(1) |
| 随机访问 | O(n) | O(n) |
| 内存占用 | 较低 | 较高 |
| 移除中间元素 | 不支持 | 支持 |
| 空间利用 | 更高效 | 较低效 |
Deque应用场景:
- 实现栈:比Stack类更推荐使用Deque作为栈
- 实现队列:可以方便地实现FIFO队列
- 滑动窗口:维护固定大小的元素序列
- 工作窃取算法:允许线程从其他队列两端窃取工作
- 回滚操作:使用双端队列跟踪操作历史
// 1. 使用Deque实现固定大小的滑动窗口
Deque<Integer> slidingWindow = new ArrayDeque<>(3); // 大小为3的窗口
// 添加元素至窗口
for (int i = 1; i <= 5; i++) {
// 如果窗口已满,从头部移除元素
if (slidingWindow.size() == 3) {
slidingWindow.pollFirst();
}
slidingWindow.offerLast(i);
System.out.println("当前窗口: " + slidingWindow);
}
最佳实践:
- 需要栈时,优先使用
Deque而非Stack类 - 一般情况下,推荐使用
ArrayDeque而非LinkedList - 需要在两端操作且需要线程安全时,使用
ConcurrentLinkedDeque - 根据应用场景选择合适的方法(抛异常还是返回特殊值)
更多学习资源
📚 想了解更多Java集合框架知识和面试技巧?欢迎访问以下资源:
- 🌐 网站: 绘问IT学习平台
- 📱 微信公众号: 搜索"绘问",获取Java进阶干货和最新面试题库
这20道Java集合面试题已为您全面解析!如果您想获取更多Java面试技巧和学习资源,记得访问我们的网站和关注微信公众号!祝您面试成功!