Skip to content

java.util 包面试题汇总

目录

  1. 集合框架概述
  2. List 接口及实现
  3. Map 接口及实现
  4. Set 接口及实现
  5. Queue 和 Deque
  6. Stream API
  7. 并发集合
  8. 线程池
  9. 锁机制

一、集合框架概述

1.1 Java 集合框架体系

问题: 请简述 Java 集合框架的体系结构。

答案:

Java 集合框架主要分为两大接口体系:

Collection(单列集合)
├── List(有序、可重复)
│   ├── ArrayList
│   ├── LinkedList
│   └── Vector
│       └── Stack
├── Set(无序、不可重复)
│   ├── HashSet
│   ├── LinkedHashSet
│   └── TreeSet
└── Queue(队列)
    ├── LinkedList
    ├── PriorityQueue
    └── ArrayDeque

Map(双列集合)
├── HashMap
├── LinkedHashMap
├── TreeMap
├── Hashtable
└── WeakHashMap

核心接口:

接口特点实现类
List有序、可重复、有索引ArrayList, LinkedList
Set无序、不可重复HashSet, TreeSet
QueueFIFO 队列LinkedList, PriorityQueue
Deque双端队列ArrayDeque, LinkedList
Map键值对、key 唯一HashMap, TreeMap

1.2 集合与数组的区别

问题: 集合与数组有什么区别?

答案:

特性数组集合
长度固定可变
存储类型基本类型 + 对象只能存储对象(基本类型自动装箱)
功能简单存储丰富的操作方法
泛型支持不支持支持
线程安全不安全部分实现线程安全

选择建议:

  • 元素数量固定、类型一致 → 使用数组
  • 需要动态扩容、丰富操作 → 使用集合

二、List 接口及实现

2.1 ArrayList 和 LinkedList 的区别

问题: ArrayListLinkedList 有什么区别?

答案:

特性ArrayListLinkedList
底层结构动态数组双向链表
随机访问O(1)O(n)
插入/删除O(n),尾部 O(1)O(1)
内存占用较少较多(需要存储指针)
扩容需要(1.5倍)不需要
适用场景查询多、增删少增删多、查询少

代码示例:

java
// ArrayList - 适合随机访问
List<String> arrayList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    arrayList.add("item" + i);
}
String item = arrayList.get(5000); // O(1)

// LinkedList - 适合频繁插入删除
List<String> linkedList = new LinkedList<>();
linkedList.add("first");
((LinkedList<String>) linkedList).addFirst("newFirst"); // O(1)

2.2 ArrayList 的扩容机制

问题: ArrayList 的扩容机制是什么?

答案:

扩容过程:

  1. 初始容量为 10(默认)
  2. 当元素数量超过当前容量时触发扩容
  3. 扩容为原来的 1.5 倍oldCapacity + (oldCapacity >> 1)
  4. 使用 Arrays.copyOf() 复制元素到新数组

源码分析:

java
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 扩容 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

优化建议:

  • 预估数据量,使用构造函数指定初始容量
java
// 避免频繁扩容
List<String> list = new ArrayList<>(10000);

2.3 Vector 和 ArrayList 的区别

问题: VectorArrayList 有什么区别?

答案:

特性VectorArrayList
线程安全安全(synchronized)不安全
扩容倍数2 倍1.5 倍
性能较低较高
出现版本JDK 1.0JDK 1.2

使用建议:

  • 单线程环境 → 使用 ArrayList
  • 多线程环境 → 使用 Collections.synchronizedList()CopyOnWriteArrayList

三、Map 接口及实现

3.1 HashMap 的实现原理

问题: HashMap 的实现原理是什么?

答案:

核心结构:

  • 数组 + 链表 + 红黑树(JDK 8+)
  • 数组每个位置称为桶(Bucket)
  • 每个桶存储一个链表(或红黑树)

存储过程:

  1. 计算 key 的 hashCode
  2. 通过 (n - 1) & hash 计算桶位置
  3. 如果桶为空,直接插入
  4. 如果桶有元素,遍历链表/树,key 相同则覆盖,不同则追加

源码结构:

java
// 数组(桶数组)
transient Node<K,V>[] table;

// 链表节点
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

// 树节点(JDK 8+)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;
    boolean red;
}

3.2 HashMap 的扩容机制

问题: HashMap 什么时候扩容?扩容过程是怎样的?

答案:

触发条件:

  • 元素数量 > 容量 × 负载因子(默认 0.75)
  • 单个链表长度 > 8,但桶数量 < 64

扩容过程:

  1. 创建新数组,容量为原来的 2 倍
  2. 重新计算每个元素的 hash 位置
  3. 将元素移动到新数组

JDK 8 优化:

  • 扩容时不需要重新计算 hash
  • 只需要判断 hash 新增的高位是 0 还是 1
  • 0:留在原位置;1:移到原位置 + 旧容量
java
// 扩容源码(简化)
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap = oldCap << 1; // 2倍
    
    // 迁移元素
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;
            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e;
            // ...
        }
    }
}

3.3 HashMap 链表转红黑树

问题: HashMap 什么时候将链表转为红黑树?为什么要转?

答案:

转换条件:

  • 链表长度 ≥ 8
  • 桶数量 ≥ 64

转换原因:

  • 链表查询复杂度 O(n)
  • 红黑树查询复杂度 O(log n)
  • 提高大数据量下的查询性能

为什么不直接转树:

  • 树节点占用内存是链表节点的 2 倍
  • 数据量小的时候,链表效率更高

阈值说明:

java
// 链表转树阈值
static final int TREEIFY_THRESHOLD = 8;

// 树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;

// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;

3.4 HashMap 和 Hashtable 的区别

问题: HashMapHashtable 有什么区别?

答案:

特性HashMapHashtable
线程安全不安全安全(synchronized)
null 支持key/value 可为 null都不可为 null
扩容2 倍2 倍 + 1
性能
出现版本JDK 1.2JDK 1.0
遍历方式IteratorIterator + Enumeration

线程安全替代方案:

java
// 方式1: Collections.synchronizedMap
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

// 方式2: ConcurrentHashMap(推荐)
Map<String, String> concurrentMap = new ConcurrentHashMap<>();

3.5 ConcurrentHashMap 的实现原理

问题: ConcurrentHashMap 是如何实现线程安全的?

答案:

JDK 7 - 分段锁(Segment):

  • 将数据分成多个 Segment
  • 每个 Segment 是一个独立的 HashMap
  • 每个 Segment 独立加锁
  • 默认 16 个 Segment,最多支持 16 个并发

JDK 8 - CAS + synchronized:

  • 取消 Segment,使用 Node 数组
  • 插入使用 CAS 操作
  • 冲突使用 synchronized 锁住链表头节点
  • 读操作无锁(volatile 保证可见性)

核心源码:

java
// 插入操作(JDK 8)
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CAS 插入
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            synchronized (f) { // 锁住链表头
                // 链表/树插入
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

四、Set 接口及实现

4.1 HashSet 的实现原理

问题: HashSet 是如何实现的?如何保证元素不重复?

答案:

实现原理:

  • 底层使用 HashMap 实现
  • 元素作为 HashMap 的 key
  • value 是一个固定的 Object 占位符

源码:

java
public class HashSet<E> extends AbstractSet<E> {
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
    
    public boolean add(E e) {
        return map.put(e, PRESENT) == null;
    }
}

去重原理:

  1. 计算元素的 hashCode
  2. 如果 hashCode 相同,调用 equals 比较
  3. equals 返回 true,则认为是重复元素,不添加

使用建议:

  • 自定义对象放入 HashSet,必须重写 hashCode()equals()

4.2 HashSet、LinkedHashSet、TreeSet 的区别

问题: HashSetLinkedHashSetTreeSet 有什么区别?

答案:

特性HashSetLinkedHashSetTreeSet
底层实现HashMapLinkedHashMapTreeMap
有序性无序插入顺序自然/定制排序
排序方式插入顺序自然顺序或 Comparator
性能O(1)O(1)O(log n)
null 支持支持支持不支持

代码示例:

java
// HashSet - 无序
Set<String> hashSet = new HashSet<>();
hashSet.add("C"); hashSet.add("A"); hashSet.add("B");
System.out.println(hashSet); // [A, B, C] 或乱序

// LinkedHashSet - 插入顺序
Set<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("C"); linkedSet.add("A"); linkedSet.add("B");
System.out.println(linkedSet); // [C, A, B]

// TreeSet - 自然排序
Set<String> treeSet = new TreeSet<>();
treeSet.add("C"); treeSet.add("A"); treeSet.add("B");
System.out.println(treeSet); // [A, B, C]

五、Queue 和 Deque

5.1 Queue 接口及实现

问题: Queue 接口有哪些实现类?它们的特点是什么?

答案:

Queue 实现类:

实现类特点使用场景
LinkedList双向链表,可作队列/栈通用队列
PriorityQueue优先队列,堆实现任务调度、Top K
ArrayDeque数组双端队列高性能队列/栈
ConcurrentLinkedQueue并发无界队列高并发场景
BlockingQueue阻塞队列生产者-消费者

Queue 方法对比:

操作抛出异常返回特殊值
插入add(e)offer(e)
移除remove()poll()
检查element()peek()

5.2 PriorityQueue 的实现原理

问题: PriorityQueue 是如何实现的?

答案:

实现原理:

  • 底层使用小顶堆(完全二叉树)
  • 数组存储堆结构
  • 父节点索引 = (子节点索引 - 1) / 2
  • 左子节点 = 父节点 × 2 + 1
  • 右子节点 = 父节点 × 2 + 2

核心操作:

  • offer(): 插入元素,向上调整(siftUp)
  • poll(): 移除堆顶,向下调整(siftDown)

时间复杂度:

  • 插入: O(log n)
  • 删除: O(log n)
  • 查询堆顶: O(1)

代码示例:

java
// 自然排序(小顶堆)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(3); minHeap.offer(1); minHeap.offer(2);
System.out.println(minHeap.poll()); // 1

// 大顶堆(使用 Comparator)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(
    (a, b) -> b - a
);

六、Stream API

6.1 Stream 是什么?

问题: 什么是 Stream?有什么特点?

答案:

Stream:

  • JDK 8 引入的函数式数据处理 API
  • 对集合进行声明式操作
  • 支持链式调用并行处理

特点:

特点说明
声明式描述做什么,而非怎么做
链式调用多个操作串联
惰性求值终止操作才执行
可并行自动多线程处理
不修改源数据返回新结果

代码示例:

java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 传统方式
List<Integer> result = new ArrayList<>();
for (Integer n : numbers) {
    if (n % 2 == 0) {
        result.add(n * n);
    }
}
Collections.sort(result);

// Stream 方式
List<Integer> result = numbers.stream()
    .filter(n -> n % 2 == 0)    // 过滤偶数
    .map(n -> n * n)             // 平方
    .sorted()                    // 排序
    .collect(Collectors.toList());

6.2 中间操作和终止操作

问题: Stream 的中间操作和终止操作有什么区别?

答案:

中间操作(Intermediate):

  • 返回新的 Stream
  • 惰性求值,不会立即执行
  • 可以链式调用

终止操作(Terminal):

  • 返回非 Stream 结果
  • 触发执行所有中间操作
  • 只能有一个

常见操作:

类型操作说明
中间filter过滤
中间map映射转换
中间flatMap扁平化映射
中间sorted排序
中间distinct去重
中间limit限制数量
终止collect收集结果
终止forEach遍历
终止reduce归约
终止count计数

代码示例:

java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 中间操作不会执行
Stream<String> stream = names.stream()
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.length() > 3;
    });
// 无输出

// 终止操作触发执行
List<String> result = stream.collect(Collectors.toList());
// 输出: filter: Alice, filter: Bob, ...

6.3 并行流的注意事项

问题: 使用并行流(Parallel Stream)需要注意什么?

答案:

适用场景:

  • 数据量大(通常 > 10,000)
  • 复杂计算操作
  • 无状态、无副作用

注意事项:

问题说明
线程安全避免修改共享变量
顺序问题forEachOrdered 性能差
拆箱装箱使用 IntStream 等原始流
错误数据源Stream.iterateStream.of 不适合

代码示例:

java
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    numbers.add(i);
}

// 正确使用并行流
long sum = numbers.parallelStream()
    .mapToLong(n -> n * n)  // 使用原始流避免装箱
    .sum();

// 错误:修改共享变量
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(result::add); // 线程不安全

// 正确:使用 collect 合并结果
List<Integer> result = numbers.parallelStream()
    .collect(Collectors.toList());

七、并发集合

7.1 CopyOnWriteArrayList

问题: CopyOnWriteArrayList 的实现原理是什么?适用场景?

答案:

实现原理:

  • 写时复制(Copy-On-Write)
  • 读操作:直接读取数组(无锁)
  • 写操作:复制新数组,修改后替换引用

源码分析:

java
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

适用场景:

  • 读多写少
  • 数据量小
  • 实时性要求不高

缺点:

  • 写操作开销大(需要复制数组)
  • 内存占用高(同时存在两个数组)
  • 数据最终一致性

7.2 并发集合对比

问题: Java 中有哪些并发集合?它们的特点是什么?

答案:

集合特点实现机制
ConcurrentHashMap线程安全 MapCAS + synchronized
CopyOnWriteArrayList线程安全 List写时复制
CopyOnWriteArraySet线程安全 Set写时复制
ConcurrentLinkedQueue无界并发队列CAS
BlockingQueue阻塞队列锁 + Condition
ConcurrentSkipListMap有序并发 Map跳表

选择建议:

  • 高并发 Map → ConcurrentHashMap
  • 读多写少 List → CopyOnWriteArrayList
  • 生产者-消费者 → BlockingQueue

八、线程池

8.1 ThreadPoolExecutor 参数

问题: ThreadPoolExecutor 有哪些核心参数?

答案:

核心参数:

参数说明
corePoolSize核心线程数,即使空闲也保留
maximumPoolSize最大线程数
keepAliveTime非核心线程空闲存活时间
workQueue任务等待队列
threadFactory线程工厂
rejectedExecutionHandler拒绝策略

执行流程:

  1. 当前线程数 < corePoolSize:创建新线程执行任务
  2. 当前线程数 ≥ corePoolSize:任务进入队列
  3. 队列满且线程数 < maximumPoolSize:创建非核心线程
  4. 队列满且线程数 ≥ maximumPoolSize:执行拒绝策略

拒绝策略:

策略说明
AbortPolicy抛出异常(默认)
CallerRunsPolicy调用者线程执行
DiscardPolicy静默丢弃
DiscardOldestPolicy丢弃队列最老任务

8.2 线程池的使用

问题: 如何正确使用线程池?

答案:

创建线程池:

java
// 方式1: 手动创建(推荐)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                      // 核心线程数
    10,                     // 最大线程数
    60L,                    // 空闲存活时间
    TimeUnit.SECONDS,       // 时间单位
    new LinkedBlockingQueue<>(100), // 任务队列
    new ThreadFactory() {   // 线程工厂
        private AtomicInteger count = new AtomicInteger(0);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "pool-thread-" + count.incrementAndGet());
        }
    },
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

// 方式2: Executors 工厂方法(不推荐)
ExecutorService executor = Executors.newFixedThreadPool(10);

提交任务:

java
// execute - 无返回值
executor.execute(() -> System.out.println("Task 1"));

// submit - 有返回值
Future<Integer> future = executor.submit(() -> {
    return 42;
});
Integer result = future.get();

// invokeAll - 批量执行
List<Callable<Integer>> tasks = Arrays.asList(() -> 1, () -> 2);
List<Future<Integer>> futures = executor.invokeAll(tasks);

关闭线程池:

java
// 优雅关闭
executor.shutdown();           // 不再接受新任务,等待执行完
executor.awaitTermination(60, TimeUnit.SECONDS); // 等待60秒

// 强制关闭
executor.shutdownNow();        // 立即关闭,返回未执行的任务

8.3 线程池大小设置

问题: 如何设置线程池的大小?

答案:

CPU 密集型任务:

线程数 = CPU 核心数 + 1

IO 密集型任务:

线程数 = CPU 核心数 × (1 + 等待时间 / 计算时间)
// 或简单设置:CPU 核心数 × 2

获取 CPU 核心数:

java
int cpuCores = Runtime.getRuntime().availableProcessors();

示例:

java
// CPU 密集型
ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(
    cpuCores + 1,
    cpuCores + 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>()
);

// IO 密集型
ThreadPoolExecutor ioExecutor = new ThreadPoolExecutor(
    cpuCores * 2,
    cpuCores * 4,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)
);

九、锁机制

9.1 synchronized 和 ReentrantLock 的区别

问题: synchronizedReentrantLock 有什么区别?

答案:

特性synchronizedReentrantLock
实现方式JVM 层面(字节码)API 层面(Java 代码)
锁获取自动获取/释放手动 lock/unlock
可中断不支持lockInterruptibly()
超时获取不支持tryLock(timeout)
公平锁非公平可配置公平/非公平
条件变量一个(wait/notify)多个(Condition)
性能JDK 6+ 优化后接近与 synchronized 接近

ReentrantLock 示例:

java
ReentrantLock lock = new ReentrantLock();

// 基本使用
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

// 可中断锁
lock.lockInterruptibly();
try {
    // 临界区
} finally {
    lock.unlock();
}

// 尝试获取锁(带超时)
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
}

9.2 读写锁(ReadWriteLock)

问题: 什么是读写锁?有什么优势?

答案:

ReadWriteLock:

  • 维护一对锁:读锁写锁
  • 读锁:多个线程可同时获取
  • 写锁:独占,与其他读写锁互斥

优势:

  • 读多写少场景下提高并发性能
  • 读操作不互斥,提高吞吐量

使用示例:

java
public class ReadWriteLockExample {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    private Map<String, Object> data = new HashMap<>();
    
    // 读操作
    public Object get(String key) {
        readLock.lock();
        try {
            return data.get(key);
        } finally {
            readLock.unlock();
        }
    }
    
    // 写操作
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            data.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

锁降级:

java
// 锁降级:写锁 → 读锁(支持)
writeLock.lock();
try {
    // 修改数据
    data.put("key", "value");
    
    // 获取读锁(锁降级)
    readLock.lock();
} finally {
    writeLock.unlock(); // 释放写锁,保留读锁
}

try {
    // 使用读锁读取数据
    Object value = data.get("key");
} finally {
    readLock.unlock();
}

// 锁升级:读锁 → 写锁(不支持,会导致死锁)

9.3 AQS(AbstractQueuedSynchronizer)

问题: 什么是 AQS?它在并发包中的作用是什么?

答案:

AQS(抽象队列同步器):

  • 并发包的基础框架
  • ReentrantLockCountDownLatchSemaphore 等都基于 AQS
  • 使用 CLH 队列管理等待线程

核心思想:

  • state 变量: 表示同步状态
  • FIFO 队列: 管理等待线程
  • 模板方法模式: 子类实现获取/释放逻辑

实现类:

用途
ReentrantLock可重入锁
ReentrantReadWriteLock读写锁
CountDownLatch倒计时门闩
Semaphore信号量
CyclicBarrier循环屏障

AQS 结构:

java
public abstract class AbstractQueuedSynchronizer {
    private volatile int state;           // 同步状态
    private transient volatile Node head; // 队列头
    private transient volatile Node tail; // 队列尾
    
    // 子类实现
    protected abstract boolean tryAcquire(int arg);
    protected abstract boolean tryRelease(int arg);
}

十、面试题速查表

问题核心要点
ArrayList vs LinkedList数组 vs 链表,查询快 vs 增删快
HashMap 扩容2 倍扩容,链表转红黑树(长度≥8)
ConcurrentHashMapJDK 8 使用 CAS + synchronized
HashSet 原理底层 HashMap,key 存元素
Stream 惰性求值中间操作不执行,终止操作触发
CopyOnWriteArrayList写时复制,读多写少场景
线程池参数core、max、队列、拒绝策略
ReentrantLock 优势可中断、可超时、多 Condition
读写锁读共享、写独占,适合读多写少
AQS并发包基础框架,state + CLH 队列

文档版本: 1.0.0
最后更新: 2026-03-26
适用 JDK: Java 8+

Released under the MIT License.