1. 概述

本文系统性地梳理 Java 开发中常见的异常类型,帮助你在实际项目中快速定位问题、避免踩坑。我们不会停留在“什么是异常”这种基础概念上,而是聚焦于实战中高频出现的异常,分析其触发场景和应对策略。

对于有经验的开发者来说,理解异常的本质比死记硬背更重要。掌握这些异常的根源,才能写出更健壮的代码。

2. 异常机制简述

异常(Exception)是指程序运行过程中发生的非正常事件,会中断正常的指令流。 当 JVM 在执行代码时检测到违反语义或资源约束的情况,就会抛出异常。

Java 的异常体系全部继承自 Throwable,主要分为两大类:

  • Checked Exception(受检异常):编译器强制要求处理,否则无法通过编译
  • Unchecked Exception(非受检异常):包括 RuntimeException 及其子类,编译期不检查

💡 小贴士:关于 checked exception 是否应该广泛使用,社区一直有争议(如 Java 泛型之父的批评)。但在 IO、网络、反射等场景下,它们仍是标准设计。

3. 受检异常(Checked Exceptions)

这类异常必须显式处理——要么 try-catch,要么向上抛出。否则编译直接报错 ❌。

3.1. IOException

凡是涉及 I/O 操作失败时,就会抛出 IOException 或其子类。典型场景包括:

  • 使用 java.io 包进行文件读写
  • 使用 java.net 包建立网络连接

FileNotFoundException

最常见的 IOException 子类之一,文件路径不存在或权限不足时触发。

try {
    new FileReader(new File("/invalid/file/location"));
} catch (FileNotFoundException e) {
    LOGGER.info("FileNotFoundException caught!");
}

MalformedURLException

构造 URL 对象时传入非法格式字符串,就会抛出此异常。

try {
    new URL("malformedurl");
} catch (MalformedURLException e) {
    LOGGER.error("MalformedURLException caught!");
}

3.2. ParseException

当你尝试将一个字符串解析成某种结构化对象(比如日期),但格式不匹配时,就会抛出 ParseException

例如用 SimpleDateFormat 解析错误格式的日期字符串:

try {
    new SimpleDateFormat("MM, dd, yyyy").parse("invalid-date");
} catch (ParseException e) {
    LOGGER.error("ParseException caught!");
}

⚠️ 注意:Java 8 引入的 java.time 包使用的是 DateTimeParseException,行为更现代也更清晰。

3.3. InterruptedException

这是多线程编程中最容易被忽略但也最关键的异常之一。当一个线程处于 WAITINGTIMED_WAITING 状态时被其他线程中断,就会抛出 InterruptedException

常见于调用了 sleep()wait()join() 的线程。

class ChildThread extends Thread {
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            LOGGER.error("InterruptedException caught!");
            Thread.currentThread().interrupt(); // 重要:恢复中断状态
        }
    }
}

public class MainThread {
    public static void main(String[] args) {
        ChildThread childThread = new ChildThread();
        childThread.start();
        childThread.interrupt(); // 主动中断子线程
    }
}

✅ 最佳实践:捕获后应重新设置中断标志(interrupt()),否则高层逻辑可能无法感知中断请求。

4. 非受检异常(Unchecked Exceptions)

这类异常继承自 RuntimeException,编译器不强制处理。虽然写起来方便,但一旦发生往往说明代码存在逻辑缺陷。

4.1. NullPointerException

简称 NPE,堪称 Java 开发者的“头号公敌”。只要试图对 null 引用调用方法或访问字段,就会触发。

常见场景:

String strObj = null;
strObj.equals("Hello World"); // ❌ 抛出 NullPointerException
Person personObj = null;
String name = personObj.personName; // ❌ 访问 null 对象字段
personObj.personName = "Jon Doe"; // ❌ 修改 null 对象字段

✅ 建议:使用 OptionalObjects.requireNonNull() 或 IDE 静态检查来提前预防。

4.2. ArrayIndexOutOfBoundsException

数组越界异常。Java 数组索引从 0 开始,最大为 length - 1,任何超出范围的访问都会抛出此异常。

int[] nums = new int[] {1, 2, 3};
int numFromNegativeIndex = nums[-1]; // ❌ 负数索引
int numFromGreaterIndex = nums[4];   // ❌ 超出 length
int numFromLengthIndex = nums[3];    // ❌ 等于 length(合法最大是 2)

⚠️ 提示:集合类如 ArrayList 内部也基于数组,同样可能抛出此类异常。

4.3. StringIndexOutOfBoundsException

字符串操作越界,本质也是数组越界的一种体现。调用 charAt() 时传入非法索引会触发。

String str = "Hello World";
char charAtNegativeIndex = str.charAt(-1); // ❌ 负数索引
char charAtLengthIndex = str.charAt(11);   // ❌ 索引等于 length(最大应为 10)

它是 IndexOutOfBoundsException 的子类,语义更具体。

4.4. NumberFormatException

字符串转数字失败时抛出。常见于 Integer.parseInt()Double.valueOf() 等方法。

String str = "100ABCD";
int x = Integer.parseInt(str);  // ❌ 抛出 NumberFormatException
int y = Integer.valueOf(str);   // ❌ 同样抛出

✅ 建议:在转换前做正则校验,或使用 Apache Commons 的 NumberUtils.isParsable()

4.5. ArithmeticException

算术运算异常。最典型的例子就是整数除以零。

int illegalOperation = 30 / 0; // ❌ 抛出 ArithmeticException

⚠️ 注意:浮点数除以零不会抛异常,而是返回 InfinityNaN

4.6. ClassCastException

类型转换异常。只有在运行时发现对象实际类型与目标类型不兼容时才会抛出。

看这个经典例子:

class Animal {}
class Dog extends Animal {}
class Lion extends Animal {}
Animal animal = new Lion();     // 实际类型是 Lion
Dog tommy = (Dog) animal;       // ❌ 强转为 Dog,抛出 ClassCastException

✅ 正确做法:使用 instanceof 先判断:

if (animal instanceof Dog) {
    Dog tommy = (Dog) animal;
}

4.7. IllegalArgumentException

传入方法的参数不合法。这是一种非常“诚实”的异常,说明调用方传错了参数。

例如线程休眠时间不能为负数:

Thread.currentThread().sleep(-10000); // ❌ 抛出 IllegalArgumentException

其他常见场景还包括集合大小为负、数组长度非法等。

4.8. IllegalStateException

对象当前状态不允许调用某个方法。这通常意味着你调用方法的时机不对。

典型例子是迭代器的 remove() 方法:

Iterator<Integer> intListIterator = new ArrayList<>().iterator(); 
intListIterator.remove(); // ❌ 抛出 IllegalStateException

⚠️ 原因:remove() 必须在 next() 之后调用,否则状态不合法。JDK 内部通过状态变量校验。

5. 总结

本文系统梳理了 Java 中常见的异常类型,涵盖:

  • ✅ 受检异常:IOExceptionParseExceptionInterruptedException
  • ✅ 非受检异常:NPE、越界、类型转换、非法参数等

理解这些异常的触发条件和设计意图,能显著提升代码质量和调试效率。记住:异常不是错误,而是程序在告诉你“这里有问题,请处理”。

示例代码已整理至 GitHub:https://github.com/dev-example/java-exceptions-guide


原始标题:Common Java Exceptions