【史上最全】2025年 Java集合框架面试必杀技!20道高频题详解

557 阅读29分钟

面试必备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实现,它们有以下关键区别:

特性ArrayListLinkedList
底层数据结构动态数组双向链表
随机访问效率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时转换]

实现原理:

  1. 数据结构:Java 8前使用数组+链表,Java 8后使用数组+链表+红黑树

  2. 哈希计算

    • 计算key的hashCode
    • 对hashCode进行扰动处理:hash = key.hashCode() ^ (key.hashCode() >>> 16)
    • 计算数组下标:index = hash & (capacity - 1)
  3. 解决哈希冲突

    • 链表法:相同哈希值的元素形成链表
    • 当链表长度超过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;
    
    // 其他实现...
}

扩容过程:

  1. 当键值对数量超过阈值(capacity * loadFactor)时触发扩容
  2. 创建新数组,容量为原来的2倍
  3. 重新计算每个元素在新数组中的位置
  4. 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优势:

  1. 粒度更细的锁,支持更高的并发度
  2. 读操作无锁,性能更好
  3. 扩容时支持并发操作

4. HashSet如何保证元素唯一性?

答案:

HashSet内部使用HashMap实现,保证元素唯一性的机制如下:

  1. 底层实现:HashSet内部包含一个HashMap对象
  2. 元素存储方式:将添加的元素作为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);
    }
    
    // 其他方法...
}

唯一性保证机制:

  1. 添加元素时

    • 计算元素的hashCode()
    • 检查是否有hashCode冲突
    • 如有冲突,使用equals()方法比较
  2. 自定义对象作为元素时

    • 必须正确重写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接口,但有以下关键区别:

特性HashMapTreeMap
数据结构哈希表(数组+链表+红黑树)红黑树
有序性无序根据键的自然顺序或比较器排序
性能平均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的额外特性:

  1. 提供了按范围操作的方法:subMap(), headMap(), tailMap()
  2. 提供了查找最近键的方法:floorKey(), ceilingKey(), lowerKey(), higherKey()
  3. 可以使用自定义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基于动态数组实现,当容量不足时会触发扩容机制:

扩容过程:

  1. 默认初始容量为10(懒加载,首次添加元素时创建)
  2. 当元素数量达到容量上限时触发扩容
  3. 扩容策略:
    • Java 6:新容量 = 旧容量 * 3/2 + 1
    • Java 7+:新容量 = 旧容量 + 旧容量 >> 1(即1.5倍)
  4. 创建更大的新数组,复制原数组元素
  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:

  1. Vector

    • 早期的线程安全List实现
    • 所有方法都使用synchronized同步
    • 性能较差,不推荐使用
  2. Collections.synchronizedList

    • 将普通List包装成线程安全的List
    • 所有方法都使用同一个锁对象同步
    • 比Vector稍好,但并发性能仍然有限
  3. CopyOnWriteArrayList

    • 写时复制策略,适合读多写少场景
    • 写操作创建新数组,不影响读操作
    • 读操作无锁,性能好
    • 写操作开销大,不适合频繁写入
  4. 自定义加锁

    • 根据需求对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接口,但有几个关键区别:

特性HashMapHashtable
线程安全非线程安全线程安全,方法使用synchronized修饰
null值支持允许null键和null值不允许null键和null值
性能较好较差(同步开销)
迭代器类型fail-fastfail-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基础上维护了一个双向链表,用于保持插入顺序或访问顺序。

主要区别:

特性HashMapLinkedHashMap
有序性无序有序(插入顺序或访问顺序)
实现方式哈希表哈希表 + 双向链表
性能插入/查找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的两种排序模式:

  1. 插入顺序(默认):元素的顺序与插入顺序一致
  2. 访问顺序:最近访问的元素会移到链表末尾,可用于实现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等基于哈希的集合时:

两者的关系

  1. 如果两个对象相等(equals()返回true),则它们的hashCode()必须相同
  2. 如果两个对象的hashCode()相同,它们不一定相等
  3. 重写equals()方法时,必须同时重写hashCode()方法

违反上述规则的后果

  • 如果只重写equals()不重写hashCode(),对象在基于哈希的集合中可能表现不正确
  • 可能导致相等对象无法找到、重复存储等问题

在集合中的应用过程

  1. HashMap/HashSet添加元素
    • 计算元素的hashCode()
    • 确定在哈希表中的位置
    • 如有哈希冲突,使用equals()比较
  2. 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()的一般规则

  1. 相同状态的对象必须产生相同的哈希码
  2. hashCode()应该尽量分散,减少冲突
  3. 计算应高效,不应有太大开销
  4. 使用所有关键字段,但可以排除派生字段

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提供了更多功能:

特性IteratorListIterator
适用集合所有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的主要实现

  1. ArrayBlockingQueue

    • 基于数组的有界队列
    • 构造时需指定容量
    • 支持公平/非公平策略
    • 适用于固定大小的场景
  2. LinkedBlockingQueue

    • 基于链表的可选有界队列
    • 不指定容量时为Integer.MAX_VALUE
    • 通常性能优于ArrayBlockingQueue
    • 适用于消费者生产者速率不匹配场景
  3. PriorityBlockingQueue

    • 带优先级的无界队列
    • 元素按优先级出队
    • 不保证同优先级元素顺序
    • 适用于按优先级处理任务场景
  4. DelayQueue

    • 延迟队列,元素在指定延迟后才能取出
    • 元素需实现Delayed接口
    • 适用于定时任务调度
  5. SynchronousQueue

    • 容量为0的队列
    • 每个put操作必须等待take操作
    • 适用于"手递手"场景
  6. 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的应用场景

  1. 生产者-消费者模式
  2. 线程池任务队列
  3. 消息缓冲区
  4. 数据交换
graph LR
    A[生产者线程] -->|put| B[BlockingQueue]
    B -->|take| C[消费者线程]
    B -->|队列满: 阻塞生产者| A
    B -->|队列空: 阻塞消费者| C

15. WeakHashMap和HashMap有什么区别?

答案:

WeakHashMap是HashMap的一种特殊变体,它使用弱引用(weak references)保存键,这意味着当键对象不再被其他对象引用时,相应的键值对会被自动从映射中移除。

主要区别

特性HashMapWeakHashMap
引用类型强引用键是弱引用
垃圾回收不受GC影响键在仅被WeakHashMap引用时会被回收
内部实现普通EntryWeakReference包装的Entry
应用场景一般映射缓存、解决内存泄漏问题
性能更好略差(需要处理已被回收的键)

工作原理

  1. WeakHashMap中的键被包装为WeakReference
  2. 当键对象仅被WeakReference引用时,下次GC会回收此对象
  3. ReferenceQueue用于跟踪已被回收的键
  4. 在每次操作(如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方法略
}

适用场景

  1. 缓存实现:不想缓存阻止对象被垃圾回收
  2. 注册回调:当客户端不再可访问时自动移除回调
  3. 防止内存泄漏:在引用关系复杂的场景下避免强引用导致的内存泄漏

注意事项

  • 仅键是弱引用,值仍是强引用
  • 如果值引用键,键不会被回收
  • 回收行为依赖于垃圾收集器运行时间
  • 不适用于生命周期可预测的对象

16. EnumMap和HashMap有什么区别?

答案:

EnumMap是一种专门用于枚举类型键的Map实现,相比于HashMap有很多特殊优化:

主要区别

特性EnumMapHashMap
键类型仅限枚举类型任意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的优势

  1. 性能优势:直接数组访问,无需哈希计算
  2. 内存效率:内存占用低,没有哈希表开销
  3. 有序性:保证枚举定义顺序
  4. 类型安全:编译时检查键类型
  5. 不需要关注hashCode/equals:枚举已正确实现

适用场景

  • 需要使用枚举作为键的任何场景
  • 高性能要求的应用
  • 内存敏感的应用
  • 需要保持枚举顺序的场景

17. 如何理解Java中的Collections工具类?

答案:

Collections是Java集合框架中的工具类,提供了丰富的静态方法来操作和创建集合,类似于数组的Arrays工具类。

主要功能分类

  1. 集合包装

    • 同步包装:将普通集合包装为线程安全版本
    • 不可修改包装:防止集合被修改
    • 类型安全包装:强制集合只能包含特定类型的对象
  2. 集合算法

    • 排序
    • 查找(二分查找)
    • 洗牌(随机排序)
    • 反转
    • 旋转
    • 交换
  3. 集合工厂方法

    • 空集合
    • 单例集合
    • Java 9后提供的不可变集合工厂方法
  4. 其他工具方法

    • 最大/最小值查找
    • 频率统计
    • 集合间操作(并集、交集等)
// 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);

使用建议

  1. 优先使用Collections工具类中的算法,而非自己实现
  2. 需要线程安全时,考虑使用同步包装或并发集合类
  3. 返回集合给客户端时,考虑使用不可修改包装
  4. Java 9+项目中,优先使用新的工厂方法创建不可变集合
  5. 不要在循环中重复创建空集合,使用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)时使用
特性ComparableComparator
接口定义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()); // 嵌套属性

使用建议

  1. 当一个类有明确的自然排序时实现Comparable
  2. 需要多种排序方式时使用Comparator
  3. 无法修改源代码时使用Comparator
  4. Java 8+项目中使用Comparator接口的新方法简化代码

19. 如何正确使用HashMap的自定义对象键?

答案:

在HashMap中使用自定义对象作为键时,必须正确实现hashCode()和equals()方法,否则可能导致意外的行为,如键查找失败、重复键等问题。

正确实现自定义键的步骤

  1. 正确实现equals()方法

    • 遵循自反性、对称性、传递性、一致性原则
    • 包含所有影响对象相等性的字段
    • 考虑null值处理
    • 考虑继承关系
  2. 正确实现hashCode()方法

    • 相等对象必须返回相同哈希码
    • 使用相同的字段计算哈希码
    • 尽量分散哈希值以减少冲突
    • 考虑性能因素
  3. 保持不可变性(推荐)

    • 键对象最好是不可变的
    • 可变对象在修改后哈希码可能变化

错误示例

// 错误实现:只重写了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!无法找到键

最佳实践

  1. 优先使用不可变类作为键(如String, Integer, UUID等)
  2. 自定义键类使用final字段并私有化
  3. 确保equals和hashCode使用相同字段集
  4. 考虑使用IDE或Lombok生成这些方法
  5. 如果必须使用可变对象作为键,确保在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的常用实现类

  1. ArrayDeque:基于可调整大小的数组实现
  2. LinkedList:基于双向链表实现
  3. 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性能对比

操作ArrayDequeLinkedList
添加/删除(两端)O(1)O(1)
随机访问O(n)O(n)
内存占用较低较高
移除中间元素不支持支持
空间利用更高效较低效

Deque应用场景

  1. 实现栈:比Stack类更推荐使用Deque作为栈
  2. 实现队列:可以方便地实现FIFO队列
  3. 滑动窗口:维护固定大小的元素序列
  4. 工作窃取算法:允许线程从其他队列两端窃取工作
  5. 回滚操作:使用双端队列跟踪操作历史
// 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);
}

最佳实践

  1. 需要栈时,优先使用Deque而非Stack
  2. 一般情况下,推荐使用ArrayDeque而非LinkedList
  3. 需要在两端操作且需要线程安全时,使用ConcurrentLinkedDeque
  4. 根据应用场景选择合适的方法(抛异常还是返回特殊值)

更多学习资源

📚 想了解更多Java集合框架知识和面试技巧?欢迎访问以下资源:

  • 🌐 网站: 绘问IT学习平台
  • 📱 微信公众号: 搜索"绘问",获取Java进阶干货和最新面试题库

这20道Java集合面试题已为您全面解析!如果您想获取更多Java面试技巧和学习资源,记得访问我们的网站和关注微信公众号!祝您面试成功!