1. Java集合框架概述
Java集合框架是Java语言中最重要的基础库之一,它提供了一套完善的接口和类来存储和操作数据集合。在面试中,集合框架相关的问题几乎必问,因为它不仅考察基础知识的掌握程度,还能反映开发者对数据结构和算法的理解深度。
集合框架主要分为三大类:
- List:有序集合,允许重复元素
- Set:无序集合,不允许重复元素
- Map:键值对映射集合
2. 常见集合类解析
2.1 List接口实现类
ArrayList
ArrayList是基于动态数组实现的List,它有以下特点:
- 随机访问速度快(O(1)时间复杂度)
- 插入和删除元素效率较低(需要移动元素)
- 默认初始容量为10,扩容时增加50%
// ArrayList初始化示例 List<String> arrayList = new ArrayList<>(); arrayList.add("Java"); arrayList.add("Python");LinkedList
LinkedList是基于双向链表实现的List,特点包括:
- 插入和删除元素效率高(O(1)时间复杂度)
- 随机访问效率低(需要遍历链表)
- 实现了Deque接口,可以作为队列使用
// LinkedList作为队列使用示例 Queue<String> queue = new LinkedList<>(); queue.offer("First"); queue.offer("Second");2.2 Set接口实现类
HashSet
HashSet是基于HashMap实现的Set,特点包括:
- 元素无序
- 不允许重复元素
- 添加、删除、查找操作的时间复杂度都是O(1)
// HashSet使用示例 Set<Integer> set = new HashSet<>(); set.add(1); set.add(2);TreeSet
TreeSet是基于红黑树实现的Set,特点包括:
- 元素按自然顺序或Comparator排序
- 添加、删除、查找操作的时间复杂度都是O(log n)
// TreeSet使用示例 Set<String> treeSet = new TreeSet<>(); treeSet.add("Banana"); treeSet.add("Apple");2.3 Map接口实现类
HashMap
HashMap是基于哈希表实现的Map,特点包括:
- 键值对存储
- 允许null键和null值
- 非线程安全
- JDK8后当链表长度超过8时会转为红黑树
// HashMap使用示例 Map<String, Integer> map = new HashMap<>(); map.put("Java", 1); map.put("Python", 2);ConcurrentHashMap
ConcurrentHashMap是线程安全的HashMap实现,特点包括:
- 采用分段锁技术提高并发性能
- 不允许null键和null值
- 在JDK8中改为使用CAS+synchronized实现
3. 数据结构基础
3.1 二叉树基本概念
二叉树是每个节点最多有两个子节点的树结构,具有以下特性:
- 第i层最多有2^(i-1)个节点
- 深度为k的二叉树最多有2^k-1个节点
- 对于任何非空二叉树,n0 = n2 + 1(n0是叶子节点数,n2是度为2的节点数)
3.2 二叉树遍历方式
前序遍历
根节点 -> 左子树 -> 右子树
void preOrder(TreeNode root) { if (root != null) { System.out.print(root.val + " "); preOrder(root.left); preOrder(root.right); } }中序遍历
左子树 -> 根节点 -> 右子树
void inOrder(TreeNode root) { if (root != null) { inOrder(root.left); System.out.print(root.val + " "); inOrder(root.right); } }后序遍历
左子树 -> 右子树 -> 根节点
void postOrder(TreeNode root) { if (root != null) { postOrder(root.left); postOrder(root.right); System.out.print(root.val + " "); } }层序遍历
按层次从上到下,每层从左到右
void levelOrder(TreeNode root) { if (root == null) return; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { TreeNode node = queue.poll(); System.out.print(node.val + " "); if (node.left != null) queue.offer(node.left); if (node.right != null) queue.offer(node.right); } }3.3 特殊二叉树类型
满二叉树
所有非叶子节点都有两个子节点,且所有叶子节点都在同一层。
完全二叉树
除最后一层外,其他层节点数都达到最大值,最后一层节点都集中在左侧。
二叉搜索树(BST)
对于任意节点:
- 左子树所有节点值小于该节点值
- 右子树所有节点值大于该节点值
平衡二叉树(AVL)
任何节点的左右子树高度差不超过1。
红黑树
一种自平衡二叉搜索树,具有以下特性:
- 节点是红色或黑色
- 根节点是黑色
- 每个叶子节点(NIL)是黑色
- 红色节点的子节点必须是黑色
- 从任一节点到其每个叶子的路径包含相同数目的黑色节点
4. 常见面试题解析
4.1 HashMap相关
HashMap的工作原理
HashMap基于哈希表实现,通过hashCode()方法计算键的哈希值,然后通过哈希算法确定存储位置。当发生哈希冲突时,JDK8之前使用链表解决,JDK8之后当链表长度超过阈值(8)时会转为红黑树。
HashMap的扩容机制
HashMap默认负载因子为0.75,当元素数量超过容量*负载因子时会进行扩容,扩容后容量变为原来的2倍。扩容时需要重新计算所有元素的位置,这是一个耗时的操作。
4.2 ConcurrentHashMap相关
ConcurrentHashMap如何保证线程安全
在JDK7中,ConcurrentHashMap使用分段锁技术,将数据分成多个Segment,每个Segment独立加锁。在JDK8中,改为使用CAS+synchronized实现,锁的粒度更小,并发性能更好。
4.3 二叉树相关
判断二叉树是否对称
public boolean isSymmetric(TreeNode root) { return root == null || isMirror(root.left, root.right); } private boolean isMirror(TreeNode left, TreeNode right) { if (left == null && right == null) return true; if (left == null || right == null) return false; return left.val == right.val && isMirror(left.left, right.right) && isMirror(left.right, right.left); }二叉树的最大深度
public int maxDepth(TreeNode root) { if (root == null) return 0; return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; }5. 性能比较与选择建议
5.1 List实现类比较
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 随机访问 | O(1) | O(n) |
| 头部插入 | O(n) | O(1) |
| 尾部插入 | O(1) | O(1) |
| 内存占用 | 较小 | 较大 |
选择建议:
- 需要频繁随机访问:ArrayList
- 需要频繁在头部插入删除:LinkedList
- 不确定时:优先选择ArrayList
5.2 Set实现类比较
| 特性 | HashSet | TreeSet |
|---|---|---|
| 排序 | 无序 | 有序 |
| 时间复杂度 | O(1) | O(log n) |
| 允许null | 是 | 否(如果使用自然排序) |
选择建议:
- 需要快速查找且不关心顺序:HashSet
- 需要有序集合:TreeSet
5.3 Map实现类比较
| 特性 | HashMap | TreeMap | ConcurrentHashMap |
|---|---|---|---|
| 排序 | 无序 | 有序 | 无序 |
| 线程安全 | 否 | 否 | 是 |
| 允许null | 是 | 否 | 否 |
选择建议:
- 单线程环境:HashMap
- 需要有序映射:TreeMap
- 多线程环境:ConcurrentHashMap
6. 实际应用场景
6.1 使用HashMap统计词频
public Map<String, Integer> wordCount(String text) { Map<String, Integer> map = new HashMap<>(); String[] words = text.split("\\s+"); for (String word : words) { map.put(word, map.getOrDefault(word, 0) + 1); } return map; }6.2 使用TreeSet实现排行榜
class Player implements Comparable<Player> { String name; int score; // 按分数从高到低排序 public int compareTo(Player other) { return other.score - this.score; } } public class Leaderboard { private TreeSet<Player> players = new TreeSet<>(); public void addPlayer(Player player) { players.add(player); } public List<Player> getTop10() { return players.stream().limit(10).collect(Collectors.toList()); } }6.3 使用优先队列解决Top K问题
public List<Integer> topKFrequent(int[] nums, int k) { Map<Integer, Integer> frequencyMap = new HashMap<>(); for (int num : nums) { frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1); } PriorityQueue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<>((a, b) -> a.getValue() - b.getValue()); for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) { pq.offer(entry); if (pq.size() > k) { pq.poll(); } } List<Integer> result = new ArrayList<>(); while (!pq.isEmpty()) { result.add(pq.poll().getKey()); } return result; }7. 常见问题与解决方案
7.1 HashMap线程不安全问题
问题描述:在多线程环境下使用HashMap可能导致死循环或数据丢失。
解决方案:
- 使用Collections.synchronizedMap包装HashMap
- 使用ConcurrentHashMap(推荐)
// 解决方案1 Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>()); // 解决方案2 Map<String, String> concurrentMap = new ConcurrentHashMap<>();7.2 ArrayList并发修改异常
问题描述:在使用迭代器遍历ArrayList时修改集合会抛出ConcurrentModificationException。
解决方案:
- 使用迭代器的remove方法
- 使用CopyOnWriteArrayList(适合读多写少场景)
- 在遍历前创建副本
List<String> list = new ArrayList<>(); // 错误方式 for (String item : list) { if (condition) { list.remove(item); // 抛出异常 } } // 正确方式1 Iterator<String> it = list.iterator(); while (it.hasNext()) { String item = it.next(); if (condition) { it.remove(); // 安全删除 } } // 正确方式2 List<String> copy = new ArrayList<>(list); for (String item : copy) { if (condition) { list.remove(item); } }7.3 对象作为HashMap键的注意事项
问题描述:自定义对象作为HashMap键时,如果重写了equals方法但没重写hashCode方法,可能导致无法正确获取值。
解决方案:
- 同时重写equals和hashCode方法
- 确保equals和hashCode使用相同的字段
- 保证对象的不可变性
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); } }8. 性能优化建议
8.1 初始化集合时指定容量
对于已知大小的集合,初始化时指定容量可以避免不必要的扩容操作。
// 优化前 List<String> list = new ArrayList<>(); // 默认容量10 Map<String, Integer> map = new HashMap<>(); // 默认容量16 // 优化后 List<String> list = new ArrayList<>(100); // 初始容量100 Map<String, Integer> map = new HashMap<>(128); // 初始容量1288.2 使用entrySet遍历Map
遍历Map时,使用entrySet比先获取keySet再获取value更高效。
Map<String, Integer> map = new HashMap<>(); // 低效方式 for (String key : map.keySet()) { Integer value = map.get(key); // 处理key和value } // 高效方式 for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); // 处理key和value }8.3 考虑使用原始类型集合
对于基本数据类型,使用原始类型集合(如Eclipse Collections、FastUtil)可以避免装箱/拆箱开销。
// 使用FastUtil的IntArrayList IntList list = new IntArrayList(); list.add(1); list.add(2); int first = list.getInt(0); // 不需要拆箱9. Java 8对集合的增强
9.1 Stream API
Stream API提供了更强大的集合操作方式:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); // 过滤和转换 List<String> result = names.stream() .filter(name -> name.length() > 3) .map(String::toUpperCase) .collect(Collectors.toList()); // 分组 Map<Integer, List<String>> groupByLength = names.stream() .collect(Collectors.groupingBy(String::length)); // 统计 IntSummaryStatistics stats = names.stream() .mapToInt(String::length) .summaryStatistics();9.2 forEach方法
集合新增了forEach方法简化遍历:
List<String> list = Arrays.asList("a", "b", "c"); // 传统方式 for (String s : list) { System.out.println(s); } // Java 8方式 list.forEach(System.out::println);9.3 compute方法
Map新增了compute系列方法简化操作:
Map<String, Integer> map = new HashMap<>(); map.put("apple", 1); // 如果存在则更新 map.computeIfPresent("apple", (k, v) -> v + 1); // 如果不存在则添加 map.computeIfAbsent("banana", k -> 0);10. 面试准备建议
10.1 重点掌握内容
- HashMap:工作原理、哈希冲突解决、扩容机制
- ConcurrentHashMap:线程安全实现原理(JDK7和JDK8的区别)
- ArrayList vs LinkedList:底层实现、适用场景
- TreeMap/TreeSet:红黑树原理、时间复杂度
- Fail-Fast机制:快速失败原理及应对方法
10.2 常见问题示例
- HashMap和HashTable的区别?
- ConcurrentHashMap是如何实现线程安全的?
- ArrayList的扩容机制是怎样的?
- 如何实现一个LRU缓存?
- 红黑树有哪些特性?为什么要用红黑树而不用AVL树?
10.3 算法题准备
- 实现一个双向链表
- 实现一个简单的HashMap
- 二叉树的各种遍历(递归和非递归)
- 判断二叉树是否为平衡二叉树
- 两个栈实现队列
11. 总结与个人建议
在实际开发中,选择正确的集合类可以显著提高程序性能。根据我的经验,以下几点特别值得注意:
预估集合大小:对于已知大小的集合,初始化时指定容量可以避免多次扩容带来的性能损耗。我曾经优化过一个性能问题,仅仅通过为ArrayList指定初始容量就将性能提升了30%。
注意集合的线程安全性:在多线程环境下,一定要使用线程安全的集合类或进行适当的同步。我曾经遇到过因为使用非线程安全集合导致的难以复现的bug,花费了大量时间排查。
合理使用Java 8新特性:Stream API可以让代码更简洁,但要注意它不总是性能最优的选择,特别是在处理小数据集时。
理解底层实现:只有深入理解集合类的底层实现原理,才能在面试和实际开发中做出最佳选择。建议阅读JDK源码,特别是HashMap和ArrayList的实现。
关注内存使用:对于大型集合,不同的实现内存开销可能差异很大。在内存敏感的场景下,可以考虑使用原始类型集合或更紧凑的数据结构。