java集合面试题

一、Java 集合核心概念

Java 集合(Collection)是用于存储、操作一组对象的容器,替代了数组的局限性(固定长度、仅存同类型),主要分为 Collection 体系(存储单个元素)和 Map 体系(存储键值对)两大核心,所有集合类都位于 java.util 包下。

image

1. 集合的核心特点

  • 动态扩容:长度可随元素增减自动调整;
  • 支持泛型:指定存储元素类型,避免类型转换异常;
  • 提供丰富方法:如增删改查、排序、遍历等;

2. 集合体系核心分类

体系 核心接口 典型实现类 核心特点
Collection List(有序,可重复) ArrayList、LinkedList 按索引访问,允许重复元素
Set(无序、不可重复) HashSet、TreeSet、LinkedHashSet 无索引,元素唯一
Queue(队列、先进先出) LinkedList、ArrayDeque 遵循 FIFO 原则
Map Map(键值对、键唯一) HashMap、TreeMap、LinkedHashMap 键唯一,值可重复,无索引

二、Java 集合高频面试题

1. 基础概念题:Collection 和 Collections 的区别?

  • Collection:是接口,集合体系的根接口之一,定义了单列集合的通用方法(如 add()、remove()、size()),List/Set/Queue 都继承它;
  • Collections:是工具类,提供静态方法操作集合(如排序、同步、创建不可变集合),不能实例化。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionVsCollections {
    public static void main(String[] args) {
        // Collection接口的使用(ArrayList实现类)
        List<Integer> list = new ArrayList<>();
        list.add(3);
        list.add(1);
        list.add(2);

        // Collections工具类的使用
        Collections.sort(list); // 排序
        System.out.println(list); // 输出:[1,2,3]

        List<Integer> unmodifiableList = Collections.unmodifiableList(list); // 不可变集合
        // unmodifiableList.add(4); // 抛出UnsupportedOperationException
    }
}

2. 核心对比题:HashMap 和 Hashtable 的区别?

特性 HashMap Hashtable
线程安全 非线程安全(效率高) 线程安全(方法加 synchronized,效率低)
空值支持 允许键为 null、值为 null(仅 1 个 null 键) 不允许键 / 值为 null(抛 NullPointerException)
底层实现 JDK 1.8 后:数组 + 链表 + 红黑树 数组 + 链表
初始容量 初始容量 16,扩容 2 倍 初始容量 11,扩容 2 倍 + 1
继承体系 继承 AbstractMap 继承 Dictionary(已过时)

3.场景题:如何选择合适的集合

问题:请结合场景说明:1)频繁查询选什么?2)频繁增删选什么?3)需键值对存储选什么?4)需保证线程安全选什么?

1.频繁查询:ArrayList(数组底层,随机访问 O (1))、HashMap(哈希表,键查询 O (1));
2.频繁增删:LinkedList(链表底层,增删 O (1))、LinkedHashSet(链表 + 哈希表,有序增删);
3.键值对存储:HashMap(非线程安全,效率高)、ConcurrentHashMap(线程安全,分段锁 / CAS 实现);
4.线程安全:Vector(ArrayList 线程安全版)、Hashtable(HashMap 线程安全版)、ConcurrentHashMap

易错点题:HashMap 为什么线程不安全?

  • JDK 1.7:多线程扩容时,链表会形成环形结构,导致死循环、CPU 100%;
  • JDK 1.8:修复了环形链表,但仍存在数据覆盖问题(如两个线程同时 put 同一个键,可能导致其中一个值丢失);
  • 解决方案:使用 ConcurrentHashMap(JDK 1.8 用 CAS + synchronized 保证线程安全,效率更高)。

三 List 接口面试题

1. ArrayList 深度补充(面试高频)

  • 底层原理:

    1.基于动态数组实现,默认初始容量 10(JDK8),扩容时新容量 = 原容量 ×1.5,扩容会复制数组(耗时,面试常问「如何优化 ArrayList 扩容」)。

    2.线程不安全,多线程下会出现ConcurrentModificationException(并发修改异常)。

面试考点

ArrayList 和 Vector 的区别?

答:① 线程安全:Vector 加了 synchronized(方法级锁),ArrayList 不安全;② 扩容:Vector 扩容 2 倍,ArrayList 扩容 1.5 倍;③ 效率:ArrayList 更高,Vector 已过时。

问:ArrayList 遍历方式效率对比?

答:① 普通 for 循环(通过索引):效率最高;② 增强 for(迭代器):次之;③ 迭代器遍历:灵活(可删除元素);④ forEach(Lambda):语法简洁,效率略低。

// 错误:遍历中删除元素会触发ConcurrentModificationException
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (String s : list) {
    if (s.equals("A")) {
        list.remove(s); // 报错
    }
}

// 正确:使用迭代器删除
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("A")) {
        it.remove(); // 安全删除
    }
}

// 优化ArrayList扩容:初始化时指定容量(避免多次扩容)
List<Integer> optimizeList = new ArrayList<>(100); // 已知需存100个元素

2. LinkedList 深度补充

  • 底层原理:

1.基于双向链表实现,无索引,查询需遍历链表(慢),增删只需修改节点指针(快)。

2.同时实现 Deque 接口,可作为队列 / 栈使用(推荐替代 Stack)。

面试考点

问:ArrayList 和 LinkedList 如何选择?

答:① 查多改少:选 ArrayList(索引查询快);② 改多查少:选 LinkedList(增删快);③ 尾部增删:两者效率相近(ArrayList 尾部增删无需扩容时)

问:LinkedList 作为栈使用的方法?

答:push()(入栈)、pop()(出栈)、peek()(查看栈顶),底层调用addFirst()/removeFirst()/getFirst()。

// LinkedList作为队列(FIFO)
Deque<String> queue = new LinkedList<>();
queue.offer("A"); // 入队
queue.offer("B");
System.out.println(queue.poll()); // 出队:A

// LinkedList作为栈(LIFO)
Deque<String> stack = new LinkedList<>();
stack.push("A");
stack.push("B");
System.out.println(stack.pop()); // 出栈:B

四 Set 接口补充(核心考点 + 去重原理)

  • 底层原理:

1.基于 HashMap 实现(底层是数组 + 链表 + 红黑树),Set 的元素作为 HashMap 的 key,value 是固定的PRESENT对象。

2.去重原理:先比较 hashCode,再比较 equals(面试必考「hashCode 和 equals 的关系」)。

面试考点

问:HashSet 如何保证元素不可重复?

答:添加元素时,先计算元素的 hashCode 找到数组位置,再遍历该位置的链表 / 红黑树,用 equals 比较元素;若 hashCode 不同,直接添加;若 hashCode 相同且 equals 为 true,覆盖(不添加);若 hashCode 相同但 equals 为 false,视为不同元素(哈希冲突)。

问:自定义对象放入 HashSet 需要重写什么?

答:必须重写 hashCode () 和 equals (),否则默认用 Object 的方法(比较地址值,无法去重)。

// 自定义User类(必须重写hashCode和equals)
class User {
    private String id;
    private String name;

    // 构造器、getter/setter省略
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id); // 按id去重
    }

    @Override
    public int hashCode() {
        return Objects.hash(id); // 基于id计算hashCode
    }
}

// 测试HashSet去重
Set<User> userSet = new HashSet<>();
userSet.add(new User("1", "张三"));
userSet.add(new User("1", "张三")); // 重复,不会添加
System.out.println(userSet.size()); // 结果:1

2. LinkedHashSet & TreeSet 补充

  • LinkedHashSet:

底层:HashSet + 双向链表,保留元素插入顺序(面试问「HashSet 和 LinkedHashSet 的区别」)。

场景:需要去重且保留顺序时使用(如记录用户操作日志)。

  • TreeSet:

底层:基于 TreeMap(红黑树),可自然排序(Comparable)或自定义排序(Comparator)。

面试考点:
问:TreeSet 去重的依据?

答:基于比较器(compareTo/compare),返回 0 则视为重复元素,而非 hashCode+equals。

// 自定义排序:按年龄降序
Set<User> treeSet = new TreeSet<>((u1, u2) -> {
    // 先按年龄降序,再按姓名升序
    int ageCompare = Integer.compare(u2.getAge(), u1.getAge());
    return ageCompare != 0 ? ageCompare : u1.getName().compareTo(u2.getName());
});
treeSet.add(new User("1", "张三", 20));
treeSet.add(new User("2", "李四", 25));
treeSet.add(new User("3", "王五", 25));

五 Map 接口补充(核心考点 + 底层原理)

1. HashMap 深度补充(面试重中之重)

  • 底层原理:

1.数组(桶)+ 链表(哈希冲突时)+ 红黑树(链表长度≥8 且数组长度≥64 时,转为红黑树,提升查询效率)。

2.初始容量 16,负载因子 0.75,扩容阈值 = 容量 × 负载因子,扩容时容量翻倍(必须是 2 的幂,面试问「为什么 HashMap 容量是 2 的幂?」→ 减少哈希冲突,计算索引时用 & 代替 %,效率更高)。

面试考点:

问:HashMap 的 put 方法流程?

答:① 计算 key 的 hashCode(扰动函数:(h = key.hashCode()) ^ (h >>> 16));② 计算索引:hash & (容量-1);③ 若桶为空,直接放入;④ 若桶不为空,判断是链表还是红黑树;⑤ 遍历链表 / 红黑树,存在相同 key 则覆盖 value,否则新增节点;⑥ 新增后若链表长度≥8,转为红黑树;⑦ 若元素数≥扩容阈值,扩容。

问:HashMap 和 Hashtable 的区别?

答:① 线程安全:Hashtable 加 synchronized(方法级锁),HashMap 不安全;② 允许 null:HashMap 允许 key/value 为 null(key 只能一个 null),Hashtable 不允许;③ 效率:HashMap 更高,Hashtable 已过时;④ 扩容:HashMap 扩容 2 倍,Hashtable 扩容 2 倍 + 1。

// HashMap遍历方式(面试常问)
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);

// 方式1:entrySet(推荐,效率高)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ":" + entry.getValue());
}

// 方式2:Lambda(简洁)
map.forEach((k, v) -> System.out.println(k + ":" + v));

// 方式3:keySet(效率低,需二次查询)
for (String key : map.keySet()) {
    System.out.println(key + ":" + map.get(key));
}

// 初始化优化:指定初始容量(避免扩容)
Map<String, String> optimizeMap = new HashMap<>(16); // 已知存10个元素,选2的幂

2. ConcurrentHashMap 补充(并发编程面试必问)

  • 底层原理:

1.JDK1.7:分段锁(Segment 数组),每个 Segment 是一个 HashMap,锁粒度小,效率高于 Hashtable。

2.JDK1.8:取消分段锁,用 CAS+Synchronized(只锁当前桶),进一步提升并发效率。

面试考点:

问:ConcurrentHashMap 如何保证线程安全?为什么比 Hashtable 高效?

答:① JDK1.8 用 CAS 操作无锁更新,冲突时只锁当前桶(而非整个 Map);② Hashtable 是方法级锁(锁整个 Map),并发下竞争激烈;③ ConcurrentHashMap 不允许 key/value 为 null(避免歧义)。

问:ConcurrentHashMap 在多线程下能遍历吗?

答:可以,迭代器是弱一致性的,不会抛 ConcurrentModificationException,能看到遍历过程中新增的元素,但不保证实时性。

六 集合工具类补充(实用 + 面试考点)

  • 常用方法:

    1.Collections.sort(list):自然排序;Collections.sort(list, comparator):自定义排序。

    2.Collections.synchronizedList(list):将非线程安全的 List 转为线程安全(效率低于 CopyOnWriteArrayList)。

    3.Collections.unmodifiableList(list):不可变集合(修改会抛 UnsupportedOperationException)

面试考点:

问:不可变集合的使用场景?

答:① 常量集合(如配置项);② 多线程共享只读数据;③ 避免集合被意外修改。

2. Stream 流(JDK8+,面试高频)

集合的流式操作,简化遍历 / 过滤 / 排序 / 聚合,代码更简洁。


List<User> userList = Arrays.asList(
    new User("1", "张三", 20),
    new User("2", "李四", 25),
    new User("3", "王五", 20)
);

// 过滤+排序+收集
List<User> result = userList.stream()
    .filter(u -> u.getAge() >= 20) // 过滤年龄≥20
    .sorted((u1, u2) -> u1.getName().compareTo(u2.getName())) // 按姓名排序
    .collect(Collectors.toList()); // 收集为List

// 聚合:统计年龄≥20的用户数
long count = userList.stream().filter(u -> u.getAge() >= 20).count();

// 分组:按年龄分组
Map<Integer, List<User>> groupByAge = userList.stream()
    .collect(Collectors.groupingBy(User::getAge));

posted @ 2026-03-04 15:18  白鹿为溪  阅读(81)  评论(0)    收藏  举报