警告:如果你正打算背几个ArrayList和HashMap的方法就去面试,请先停下。我见过太多候选人把集合框架的 API 倒背如流,问一句"遍历时能不能直接 remove"就卡壳,再问"自定义异常应该继承 Exception 还是 RuntimeException"就直接沉默。这两个主题看着基础,其实是 Java 里最典型的"面试造火箭、入职拧螺丝"地带,而真正拧螺丝的时候,你会发现异常处理和集合框架几乎是每天都要碰的东西。
这篇文章我选择把两者放在一起讲,不是出于拼凑篇幅,而是因为它们在真实项目中天然交织:你用HashMap统计数据,用ArrayList拼接结果,用HashSet去重,而这些操作一旦遇到空指针、类型转换失败、并发修改,就需要异常机制来兜底。零基础的同学容易把它们看成两个孤立章节,但实际编码时它们就像左手和右手。全文我会围绕一个贯穿案例展开——库存管理系统中的商品统计与导出功能,从集合选型讲到异常设计,再到两者如何配合,最后附上高频问题和排查思路。无论你是自学入门、准备面试,还是已经写了两年代码想补基础,这篇都值得收藏。
1. 内容整体设计与思路拆解
1.1 为什么把异常处理和集合框架放在一起学
很多初学者把 Java 基础分成一块一块地学:今天是数组、明天是面向对象、后天是集合,异常处理被扔到最后草草看一眼 try-catch 就翻篇。这种学习方式最大的问题是——你永远不知道知识在什么场景下用。集合框架是"数据怎么存、怎么取、怎么遍历",异常处理是"程序出错了怎么办",但实际业务里这两个问题永远是同时出现的。
举个例子,你在写一个电商后台的库存盘点功能,从数据库查出所有商品,塞进List<Product>,然后用Map<String, Integer>按分类统计数量。这个过程中至少有三个机会点会触发异常:查询结果可能为 null,某个商品的价格字段可能是 null 导致排序时空指针,遍历集合时如果有人在另一个线程同时修改了它,还会抛出ConcurrentModificationException。换句话说,集合框架有多常用,异常处理就有多必要。
所以我设计这篇文章的思路很简单:不搞语法罗列,而是以一个"库存统计导出小工具"为贯穿案例,需要什么集合就讲什么集合,会遇到什么异常就讲什么异常,把两个主题自然编织在一起。这样学完以后你不是记住了一堆孤立的 API,而是建立了一种"数据结构和异常处理是配套的"编码直觉。
1.2 零基础学"高级特性"的正确打开方式
"高级特性"这个说法容易劝退新人,但实际集合框架和异常处理都没那么玄乎。集合框架的本质就是帮你把数据组织起来,异常处理的本质就是给程序装上安全网。真正的难点从来不是语法,而是"选型"和"设计"。
从选型上说,你要能在 0.1 秒内判断:这个场景该用List还是Set,该用ArrayList还是LinkedList,该用HashMap还是TreeMap。从设计上说,你要能回答:这段代码可能抛什么异常,该在哪里捕获,是向上抛还是自己处理,自定义异常继承哪个类。这些决策背后都有明确的逻辑,我会在后文逐层拆开。
另外我想强调一个学习顺序上的建议:不要一上来就啃源码。零基础阶段把集合框架的接口体系、常用实现类的适用场景以及时间复杂度这些"宏观地图"搞清楚,比抠HashMap的红黑树细节有用得多。先会用、再问为什么、最后看源码,这是最平滑的路径。
1.3 贯穿案例:库存统计与导出工具
为了让内容可落地,我设计了一个贴近真实业务的案例:一个仓库里有多种商品,每件商品有名称、分类、价格、库存数量等属性。我们需要实现以下功能:
- 用集合存储商品数据,支持按分类统计库存总量;
- 找出库存低于安全线的商品,并给出提示;
- 按价格排序后导出到报表;
- 处理查询过程中可能出现的各种异常,并把错误信息记录到日志,而不是直接让程序崩溃。
这个案例规模不大,但覆盖了ArrayList、HashSet、HashMap、TreeMap、Collections工具类、Comparator排序、自定义异常、try-with-resources 等核心知识点,也几乎包含了一个初级工程师日常开发中会遇到的典型异常场景。后面每一章我都会回到这个案例上来讲,避免空谈理论。
2. 核心细节解析与实操要点
2.1 集合框架总览:先看懂接口体系,再谈实现类
Java 集合框架的顶层设计可以用一句话概括:所有集合分为两大派系——Collection和Map。Collection下面又分List、Set、Queue三个子接口,Map则是独立的键值对体系。搞懂这个继承关系比背十个实现类的方法重要得多,因为面试时最常考的就是"你说一下 List、Set、Map 的区别",这个问题本质上就是在问你接口设计。
List的特点是有序、可重复、可按索引访问,日常开发中最常用的是ArrayList,底层是动态数组,查询快、增删慢;LinkedList底层是双向链表,增删快,但随机访问慢,实际项目中用得相对少。Set的特点是不可重复,HashSet基于哈希表实现,存取效率高但顺序不可控;TreeSet基于红黑树,元素自动排序,代价是读写复杂度为 O(log n)。Queue是队列,遵循先进先出,PriorityQueue则按优先级出队。
我这里用一个表格把选型逻辑快速列出来,方便你直接对照场景:
| 接口 | 实现类 | 底层数据结构 | 特点 | 典型场景 |
|---|---|---|---|---|
| List | ArrayList | 动态数组 | 查询快,尾部增删快 | 存储列表数据、按索引访问 |
| List | LinkedList | 双向链表 | 头尾增删快,随机访问慢 | 频繁增删、实现队列/栈 |
| Set | HashSet | 哈希表 | 去重,无序,O(1) | 去重、判存在 |
| Set | TreeSet | 红黑树 | 去重且排序 | 需要有序去重集合 |
| Map | HashMap | 哈希表+红黑树 | 键值对,查询快 | 缓存、按 key 聚合 |
| Map | TreeMap | 红黑树 | 按键排序 | 范围查询、有序映射 |
记住一句话:没有任何一个实现类是万能的,选型的依据永远是你的业务场景。数据变多后要排序就选TreeMap,仅仅按名字查信息就选HashMap,需要去重还保留插入顺序就选LinkedHashSet。先判断需求,再选集合,代码质量会明显提升。
2.2 异常体系:别再一遇到报错就 try-catch
Java 的异常体系顶层是Throwable,下面分两个分支:Error和Exception。Error是 JVM 层面的严重问题,比如OutOfMemoryError、StackOverflowError,这类错误程序本身无法恢复,不用捕获也不要捕获;Exception才是我们日常要处理的。
Exception又分为两类。受检异常(checked exception)如IOException、SQLException,编译器强制你处理,要么 try-catch 要么 throws 上抛;非受检异常(unchecked exception)如NullPointerException、IllegalArgumentException、ClassCastException,都是RuntimeException的子类,编译器不强制处理。很多新手最大的误区是一看到代码报错就 try-catch 包一层,这其实掩盖了真正的问题。受检异常意味着"外部环境可能出问题,你要有预案",非受检异常意味着"你自己的代码逻辑有 bug,不该用 try-catch 掩盖"。
我个人的判断标准是:如果异常是外部因素导致的,比如文件不存在、网络超时、数据库连接失败,用受检异常并给出降级方案;如果是参数校验失败、状态非法这类编程错误,直接抛IllegalArgumentException或自定义运行时异常,让异常信息尽早暴露。这个原则在很多公司代码规范里都有,只是没人给你讲清楚为什么。
2.3 两者的交汇点:fail-fast 机制与 ConcurrentModificationException
集合框架和异常处理最经典的交汇点就是 fail-fast(快速失败)机制。用ArrayList、HashMap等非线程安全集合时,如果在迭代过程中有其他线程修改了集合结构,会抛出ConcurrentModificationException。这个异常的底层原理是集合内部维护了一个modCount计数器,每次结构性修改(add、remove、clear)都会让modCount加一,而迭代器创建时会记录当时的modCount,每次next()都会检查当前modCount是否等于预期值,不等就立刻抛异常。
这个设计听起来好像很"暴力",但背后的动机很简单:集合在迭代过程中如果结构变了,迭代器无法保证下一个元素的正确性,与其返回错误数据,不如快速失败、尽早暴露问题。这就像你在高速上开着车,仪表盘的监测系统发现问题直接报警,而不是让你继续开下去酿成大祸。
实操中这个异常最常出现在一个场景——遍历ArrayList时直接调用list.remove()删除符合条件的元素。下一轮迭代时modCount对不上,异常就炸了。正确的做法有三个:用Iterator的remove()方法;用removeIf()配合 lambda;或者先收集要删的元素,遍历结束后统一 removeAll。我会在第三章给出完整代码示例。
2.4 HashMap 与 ArrayList 的关键参数:扩容和哈希的底层逻辑
面试问集合框架,十个里有九个会问HashMap的扩容机制。我不建议零基础同学一上来就啃红黑树,但有两个参数你必须理解:负载因子 0.75和容量必须是 2 的幂次方。
负载因子 0.75 是时间和空间的折中。太小意味着哈希表很快就要扩容,空间浪费严重;太大意味着哈希冲突概率升高,查询性能下降。0.75 是 JDK 作者基于泊松分布计算出的平衡点。容量是 2 的幂次方,是因为计算桶位置时用(n - 1) & hash代替取模运算,位运算比取模快得多,而且只有容量是 2 的幂时,(n - 1)的低位才全是 1,哈希值能均匀散列。
ArrayList的扩容相对简单:默认容量 10,每次扩容变成原来的 1.5 倍(新容量 = 旧容量 + 旧容量右移一位)。这里有个细节,如果addAll一次性加入大量元素,扩容是按Math.max(预期容量, 1.5倍旧容量)来的,所以创建ArrayList时如果能预估数据量,直接指定初始容量可以省下多次扩容的拷贝开销。这些参数不用死记,但理解了背后的权衡,面试时就能应对追问。
3. 实操过程与核心环节实现
3.1 案例需求定义:商品与库存的数据模型
我先定义贯穿案例的商品类。为了演示集合框架的排序和去重功能,我给商品类加上分类、价格、库存数量三个属性,并重写equals和hashCode。这里的重写是必要操作:如果不重写equals和hashCode,HashSet去重的判断依据就是对象引用地址,两个内容完全相同的商品会被当成不同对象,去重功能直接失效。
public class Product { private String name; private String category; private double price; private int stock; public Product(String name, String category, double price, int stock) { this.name = name; this.category = category; this.price = price; this.stock = stock; } // getter / setter 省略,按需要生成 @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Product)) return false; Product p = (Product) o; return Double.compare(p.price, price) == 0 && stock == p.stock && Objects.equals(name, p.name) && Objects.equals(category, p.category); } @Override public int hashCode() { return Objects.hash(name, category, price, stock); } }这里有个容易踩的坑:equals里先判断类型,再逐个比较字段,顺序看似无关紧要,但如果是父子类继承关系,用getClass()还是instanceof会导致完全不同的结果。集合框架推荐的做法是用instanceof,保证子类对象也能和父类对象比较,同时要在hashCode里使用相同的字段,否则两个对象 equals 相等但 hashCode 不同,HashSet 就会存进两个桶里,去重失败。
3.2 集合实操一:数据存储、去重和统计
现在开始真正的实操。假设我们从某个数据源拿到了一个原始商品列表,里面有很多重复项,我需要去重、按分类统计库存总量:
List<Product> rawList = new ArrayList<>(); rawList.add(new Product("iPhone 15", "手机", 6999, 100)); rawList.add(new Product("小米手环", "穿戴", 249, 500)); rawList.add(new Product("iPhone 15", "手机", 6999, 100)); // 重复数据 rawList.add(new Product("ThinkPad", "笔记本", 8999, 60)); rawList.add(new Product("AirPods", "穿戴", 999, 80)); rawList.add(new Product("华为Mate60", "手机", 6499, 200)); // 去重 Set<Product> uniqueProducts = new HashSet<>(rawList); System.out.println("去重后商品数量:" + uniqueProducts.size()); // 按分类统计库存总量 Map<String, Integer> stockByCategory = new HashMap<>(); for (Product p : uniqueProducts) { stockByCategory.merge(p.getCategory(), p.getStock(), Integer::sum); } for (Map.Entry<String, Integer> entry : stockByCategory.entrySet()) { System.out.println(entry.getKey() + " 类库存总量: " + entry.getValue()); }HashMap.merge是 JDK8 引入的实用方法:如果 key 不存在,就把 value 放进去;如果 key 存在,就把新值和老值按第三个参数(这里是Integer::sum)合并。这种写法比传统的containsKey判断加put两步操作简洁得多,也不容易漏掉某条分支导致空指针。实际项目中我建议你优先掌握merge、computeIfAbsent、putIfAbsent这几个方法,它们能帮你省掉大量模板代码。
3.3 集合实操二:排序和查找
接下来处理两个高频需求:按价格排序、找出库存低于安全线的商品。排序有多种实现方式,最灵活的是使用Comparator。Java 8 之后可以用 lambda 和链式调用写出非常清晰的排序逻辑:
// 按价格升序排序 List<Product> sortedByPrice = new ArrayList<>(uniqueProducts); sortedByPrice.sort(Comparator.comparingDouble(Product::getPrice)); sortedByPrice.forEach(p -> System.out.println(p.getName() + " - " + p.getPrice())); // 按分类排序,价格降序 sortedByPrice.sort(Comparator .comparing(Product::getCategory) .thenComparing(Product::getPrice, Comparator.reverseOrder())); // 找出库存低于 100 的商品 List<Product> lowStockProducts = uniqueProducts.stream() .filter(p -> p.getStock() < 100) .collect(Collectors.toList()); System.out.println("库存低于 100 的商品数量: " + lowStockProducts.size());这里有个新手很容易忽略的点:sort方法会直接修改原集合,所以排序前我复制了一份new ArrayList<>(uniqueProducts)。如果你不想改动原列表,就必须复制或者用 stream 的sorted方法生成新列表。另外Comparator.comparingDouble对 double 和 Double 都适用,但如果字段是 float 就要用comparingFloat,类型不匹配编译会报错,这其实是在提醒你多用基本类型的专用方法,避免自动装箱带来的性能损失。
3.4 异常处理实操一:自定义业务异常和受检异常的取舍
在库存工具里,商品价格不能为负、库存不能为负,这些约束如果被违反,最好的做法是抛出自定义异常,让上层统一处理。设计自定义异常时有两条路:继承Exception(受检)或继承RuntimeException(非受检)。我的建议和 Spring 等主流框架的做法一致:业务校验失败继承 RuntimeException。
理由很简单:受检异常强制每个调用方都要 try-catch,会导致捕获了又不知道怎么处理的尴尬局面,最终很多团队习惯性地 catch 后吞掉,反而掩盖了问题。而运行时异常可以在顶层统一处理,代码中间层不需要被 try-catch 污染。只有像文件读取、网络请求这种调用方必须知道并且有能力处理失败的情况,才用受检异常。
public class BusinessException extends RuntimeException { public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); } } public class ProductValidator { public static void validate(Product p) { if (p == null) { throw new IllegalArgumentException("商品不能为 null"); } if (p.getPrice() < 0) { throw new BusinessException("商品价格不能为负数: " + p.getName()); } if (p.getStock() < 0) { throw new BusinessException("商品库存不能为负数: " + p.getName()); } } }写自定义异常时还有一个容易被忽视的细节:一定要提供接收Throwable cause的构造方法。因为很多时候你捕获了一个底层异常,想在抛出自定义异常时不丢失原始堆栈,这样才能方便排查真正的根源。只写一个String message构造方法会让异常链断裂,日志里只能看到业务异常信息,看不到底层原因。
3.5 异常处理实操二:try-with-resources 和 finally 的注意事项
库存工具需要把统计结果导出到文件,这就会涉及资源关闭的问题。传统的try-catch-finally写法要在 finally 块里关闭流,代码啰嗦,而且如果关闭流本身抛异常,可能把主异常覆盖掉。JDK7 之后引入了 try-with-resources,只要资源实现了AutoCloseable,就能自动关闭,代码简洁很多:
public static void exportReport(Map<String, Integer> stockByCategory, String filePath) { // 前置校验:路径为 null 或空白时直接抛业务异常 if (filePath == null || filePath.isBlank()) { throw new BusinessException("导出路径不能为空"); } try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(filePath))) { writer.write("分类,库存总量"); writer.newLine(); for (Map.Entry<String, Integer> entry : stockByCategory.entrySet()) { writer.write(entry.getKey() + "," + entry.getValue()); writer.newLine(); } System.out.println("报表导出完成: " + filePath); } catch (IOException e) { throw new BusinessException("导出报表失败: " + filePath, e); } }关于 finally 有一个所有 Java 开发者都必须知道的规则:不要在 finally 里写 return,也不要让 finally 里抛异常。如果 finally 里有 return,它会覆盖 try 或 catch 里的 return 值,这是非常隐蔽的 bug。同样,如果 finally 里抛了异常,也会覆盖原有的异常,导致问题更难排查。我见过一次线上事故,排查了两天才发现是 finally 里的日志写入抛空指针,把真正的业务异常给顶掉了。
3.6 组合实操:遍历时安全删除元素
现在把集合和异常真正组合起来。前面的低库存商品需要从集合中移除,很多新手会这样写:
for (Product p : uniqueProducts) { if (p.getStock() < 50) { uniqueProducts.remove(p); // 运行时会抛 ConcurrentModificationException } }这段代码运行 100% 会抛ConcurrentModificationException,原因就是我在 2.3 节讲的 fail-fast 机制。正确的做法是下面三种中的任意一种:
// 方式一:使用 Iterator.remove() Iterator<Product> iterator = uniqueProducts.iterator(); while (iterator.hasNext()) { Product p = iterator.next(); if (p.getStock() < 50) { iterator.remove(); } } // 方式二:使用 removeIf(最简洁) uniqueProducts.removeIf(p -> p.getStock() < 50); // 方式三:收集后统一删除 List<Product> toRemove = new ArrayList<>(); for (Product p : uniqueProducts) { if (p.getStock() < 50) { toRemove.add(p); } } uniqueProducts.removeAll(toRemove);三种方式的取舍:Iterator方式最传统,能在删除前后做一些额外逻辑;removeIf最简洁,适合简单的条件删除;removeAll适合需要先筛选后决策的场景。注意方式三有个前提:被删除的元素必须在 uniqueProducts 集合中重写过equals和hashCode,否则removeAll删不掉。这也是为什么我在 3.1 要强调重写这两个方法。
3.7 完整运行与日志记录:给异常留证据
最后把整个流程串起来。实际项目中异常不能只打印在控制台,要记录到日志里。这里我用 JDK 自带的java.util.logging做演示,生产环境你可以换成 Log4j2 或 Logback,但思路一致:日志里必须有时间、异常类型、堆栈、业务上下文。
public class InventoryTool { private static final Logger logger = Logger.getLogger(InventoryTool.class.getName()); public static void main(String[] args) { try { List<Product> rawList = loadRawProducts(); // 模拟加载 // 数据校验 for (Product p : rawList) { ProductValidator.validate(p); } Set<Product> uniqueProducts = new HashSet<>(rawList); Map<String, Integer> stockByCategory = new HashMap<>(); for (Product p : uniqueProducts) { stockByCategory.merge(p.getCategory(), p.getStock(), Integer::sum); } uniqueProducts.removeIf(p -> p.getStock() < 50); exportReport(stockByCategory, "stock_report.csv"); } catch (BusinessException e) { logger.log(Level.SEVERE, "业务处理失败", e); } catch (Exception e) { logger.log(Level.SEVERE, "系统异常", e); } } }这样设计后,任何异常都有一条清晰的传递路径:底层方法抛出自定义异常或系统异常,顶层 main 方法统一捕获并记录日志。BusinessException单独一个 catch,方便定位业务问题;其他Exception兜底,保证程序不会因为单个异常直接崩溃。初学者经常会犯的一个错是每个方法都 try-catch 一遍,结果到处是空的 catch 块,日志里什么都看不到。正确的思路是:底层只抛异常,中间层尽量不捕获,顶层统一处理。
4. 常见问题与排查技巧实录
4.1 集合框架高频报错对照表
我把实际操作中最常见的几个异常整理成一张速查表,每一行都是一个实战踩坑点:
| 异常类型 | 触发场景 | 排查思路 |
|---|---|---|
| NullPointerException | 集合中存在 null 元素,调用其方法 | 加非空校验,或使用 Objects.requireNonNull |
| ConcurrentModificationException | 遍历时直接修改集合结构 | 改用 Iterator.remove 或 removeIf |
| IndexOutOfBoundsException | List 访问越界索引 | 检查索引边界,优先用迭代器或增强 for |
| ClassCastException | 从集合取出对象强转为错误类型 | 使用泛型限制元素类型,不要裸用集合 |
| IllegalStateException | Iterator 未调用 next 就调用 remove | 调用的顺序必须 next 后才能 remove |
| UnsupportedOperationException | 对 Arrays.asList 返回的固定长度列表调用 add/remove | asList 返回的是定长列表,要增删就 new ArrayList 包一层 |
这里特别提一下Arrays.asList的坑:它返回的确实是List,但不是ArrayList,而是Arrays内部的一个私有类,底层是固定长度的数组,调用add或remove会直接抛UnsupportedOperationException。我见过不少生产事故就是从这里来的,解决方案很简单:new ArrayList<>(Arrays.asList(...))包装一下再操作。
4.2 三个容易被忽略的原理性盲区
第一个盲区是异常被吞掉却没有任何日志。很多人写try { ... } catch (Exception e) { },异常信息一个字符都没留下。我调试代码时最恨这种空 catch。如果确实觉得某个异常不用处理,那至少写一行注释说明原因,并且用logger.log(Level.FINE, "忽略该异常:...", e)记录到日志的 debug 级别,保留排查线索。
第二个盲区是返回值被 finally 覆盖。我在 3.5 里说过不要在 finally 里 return,这里再补充一个变体:如果在 finally 里修改了 try 块返回的对象的某个属性,同样会影响返回结果。因为 finally 的执行顺序在 return 表达式求值之后、方法真正返回之前,对象引用没变,但对象内部状态可能已经变了。要避免这类问题,最好让 finally 只做资源清理,不要做任何业务操作。
第三个盲区是空集合和 null 的混淆。业务代码里经常要做"集合是否为空"的判断,很多人直接用list != null,但list可能不为 null 却是空集合,遍历没问题,取数就报错。反过来,有人认为空集合和 null 是一回事,拿去给hashCode之类的场景用就出问题。规范的做法是:方法返回集合时,如果没有数据就返回空集合,而不是 null。JDK 里有Collections.emptyList()可以直接用,接收方也只需要判断isEmpty(),不用再判 null。
4.3 面试高频题速查:集合和异常一起考
面试官特别喜欢把两个主题串在一起考,因为能考察出你是真懂还是在背 API。下面这几道题几乎每场面试都会出现,我给出回答要点:
HashMap 的 put 流程是怎样的?先计算 key 的哈希值,通过(n-1) & hash定位桶位置;如果桶为空,直接放入新节点;如果不为空,判断第一个节点的 key 是否相等,相等就覆盖;如果不相等,判断该节点是红黑树节点还是链表节点,链表则遍历查找,找到就覆盖,找不到就尾插新节点;节点数超过阈值且链表长度超过 8 时树化,容量小于 64 时先扩容而不是树化。
ArrayList 和 LinkedList 的区别?底层数据结构不同,前者是动态数组,后者是双向链表;查找复杂度前者 O(1),后者 O(n);插入删除前者平均 O(n),后者头尾 O(1) 但中间 O(n);内存占用上前者连续内存、后者每个节点有额外指针开销。实际项目 90% 的场景选 ArrayList,LinkedList 的"优势"往往被随机访问需求抵消。
受检异常和运行时异常的区别?编译期是否强制处理、继承体系不同,前者继承 Exception 但非 RuntimeException 子类,后者继承 RuntimeException。设计上,调用方必须处理的用受检,编程错误用运行时。自定义业务异常建议继承 RuntimeException。
异常处理最佳实践有哪些?不要捕获Error;不要空 catch;不要在 finally 里 return;try-with-resources 管理资源;捕获具体异常而不是笼统的 Exception;记录异常堆栈而不是只记录 message。
如果你想系统准备 Java 面试,我建议把这些题目整理成自己的"一句话回答",每个知识点能用自己的话讲清楚,比背十篇八股文强得多。面试官追问时,你要能给出例子和相应的权衡,而不是掉书袋。
4.4 学习路线建议:从会用到弄懂底层
最后聊一下自学路线。如果你真的是零基础,我不建议一上来就按本文的深度去啃,可以先按照下面的顺序走一轮:
第一阶段(约 1-2 周):掌握 Collection 和 Map 的接口体系,会用ArrayList、HashSet、HashMap完成增删改查遍历;掌握 try-catch-finally 的基本语法,能读懂异常堆栈。这个阶段的目标是"会写会用"。
第二阶段(约 2-4 周):研究HashMap的哈希冲突、扩容、树化机制,理解ConcurrentModificationException的触发条件;学会自定义异常,理解受检和非受检的适用场景;开始阅读 JDK 里ArrayList、HashMap的源码,不用逐行读,重点看字段、构造方法、add/get/remove 的核心实现。
第三阶段(面试冲刺):整理高频考点,能讲清楚每个实现类的适用场景、时间复杂度和底层原理;准备一些自己在项目中遇到的异常案例,能说明你怎么排查和解决;把本文 4.3 的题目逐个写一遍,形成自己的表达框架。
如果你已经工作了,我建议你把文中的所有代码都亲手敲一遍,然后改造到自己的项目里。比如把库存工具换成"用户权限管理""订单聚合统计",过程会逼你思考选型逻辑,比看十篇文章都有效。这套方法我验证过很多次,带过的新人里,凡是认真敲过代码的,三个月内都能独立处理常见的集合与异常问题。
结尾
我从第一次写 Java 代码到现在,最深刻的体会是:异常处理和集合框架这两个主题,越早建立"设计思维"越好。所谓设计思维,就是每次写代码前先问自己——这里该用什么集合?为什么?这里可能抛什么异常?谁来处理?这两个问题想清楚了,代码的健壮性、可读性和可维护性会一下子提升一个档次。不要等到被线上故障逼着去补课,那代价就大了。最后分享一个小技巧:每次写完一段涉及集合和异常的代码,故意往里面塞一个 null、一个重复元素、一个越界索引,看你的代码能不能优雅地兜住。能兜住,才算真正学会了。