写在前面:这是JavaSE系列的第11篇,进入进阶特性阶段。异常处理是写出健壮代码的必备技能,面试也经常问。很多人写代码只管happy path,不考虑异常情况,结果上线后各种崩溃。今天把Java的异常体系彻底讲清楚。

在这里插入图片描述


一、什么是异常?

1.1 异常的概念

异常:程序在运行过程中出现的不正常情况。

踩坑提醒:新手最容易犯的错误就是忽视异常处理。曾经有一个线上Bug让我记忆犹新——用户上传文件时,因为磁盘满了导致IOException,但代码里没有处理,整个服务直接崩溃,影响了所有用户。

// 常见的异常场景
int a = 10 / 0;              // ArithmeticException
int[] arr = new int[3];
arr[5] = 10;                 // ArrayIndexOutOfBoundsException
String s = null;
s.length();                   // NullPointerException
int num = Integer.parseInt("abc"); // NumberFormatException

异常的本质:Java用对象来表示异常,每个异常都是一个类的实例。

1.2 为什么要处理异常?

不处理异常的后果:
1. 程序直接崩溃,用户体验极差
2. 数据可能丢失或损坏
3. 安全漏洞(异常信息暴露系统细节)
4. 难以定位和修复问题

处理异常的好处:
1. 程序不会意外终止
2. 给用户友好的错误提示
3. 便于记录日志和排查问题
4. 保证资源的正确释放

二、Java异常体系

2.1 异常类层次结构

Throwable(所有异常的根类)
├── Error(错误,程序无法处理)
│   ├── OutOfMemoryError        // 内存溢出
│   ├── StackOverflowError      // 栈溢出
│   └── NoClassDefFoundError    // 类找不到
│
└── Exception(异常,程序可以处理)
    ├── RuntimeException(运行时异常,也叫不受检异常)
    │   ├── NullPointerException       // 空指针
    │   ├── ArrayIndexOutOfBoundsException // 数组越界
    │   ├── ArithmeticException       // 算术错误
    │   ├── ClassCastException        // 类型转换错误
    │   ├── NumberFormatException      // 数字格式错误
    │   └── IllegalArgumentException   // 非法参数
    │
    └── 非RuntimeException(编译时异常,也叫受检异常)
        ├── IOException                // IO异常
        │   ├── FileNotFoundException  // 文件找不到
        │   └── EOFException          // 文件结束
        ├── SQLException              // 数据库异常
        └── ClassNotFoundException    // 类找不到

2.2 Error vs Exception

类型 说明 能否处理
Error JVM级别的严重错误 不能(程序直接退出)
Exception 程序级别的异常 能(try-catch处理)
// Error:不需要也不应该处理
// OutOfMemoryError → 增加JVM内存,优化代码
// StackOverflowError → 检查递归是否正确

// Exception:必须或应该处理
// IOException → try-catch或throws
// NullPointerException → 代码中加null检查

2.3 受检异常 vs 不受检异常

类型 说明 编译器检查 处理方式
受检异常 非RuntimeException ✅ 强制检查 必须try-catch或throws
不受检异常 RuntimeException ❌ 不检查 可以不处理(但建议处理)
// 受检异常:必须处理
public void readFile() throws IOException {  // 必须声明throws
    FileInputStream fis = new FileInputStream("test.txt");  // 编译错误!
}

// 不受检异常:可以不处理
public void divide() {
    int a = 10 / 0;  // 编译通过,运行时才报错
}

三、异常处理的五种方式

3.1 try-catch(捕获异常)

try {
    // 可能出现异常的代码
    int a = 10 / 0;
} catch (ArithmeticException e) {
    // 捕获特定异常
    System.out.println("算术异常:" + e.getMessage());
} catch (Exception e) {
    // 捕获其他异常
    System.out.println("未知异常:" + e.getMessage());
}

多个catch的顺序

try {
    // ...
} catch (NullPointerException e) {
    // 先捕获子类异常
    System.out.println("空指针异常");
} catch (RuntimeException e) {
    // 再捕获父类异常
    System.out.println("运行时异常");
} catch (Exception e) {
    // 最后捕获最大的异常
    System.out.println("异常");
}
// ❌ 错误:先捕获父类,子类永远捕获不到
// catch (Exception e) { }
// catch (NullPointerException e) { }  // 编译错误!

3.2 try-catch-finally

经验之谈:finally块是资源释放的保险,无论try中发生什么(除了System.exit),finally都会执行。这是Java保证资源不泄露的机制。

try {
    // 可能出现异常的代码
    FileInputStream fis = new FileInputStream("test.txt");
    // 使用fis...
} catch (IOException e) {
    // 处理异常
    System.out.println("IO异常:" + e.getMessage());
} finally {
    // 无论是否异常都会执行
    // 通常用于关闭资源
    System.out.println("finally块执行了");
}

finally的执行时机

public static int test() {
    try {
        System.out.println("try");
        return 1;
    } catch (Exception e) {
        System.out.println("catch");
        return 2;
    } finally {
        System.out.println("finally");  // 一定会执行
        // return 3;  // 不建议在finally中return
    }
}

test();  // 输出:try → finally → 返回1

finally不执行的情况

// 1. 在try或catch中调用了System.exit()
try {
    System.exit(0);  // 直接退出JVM
} finally {
    System.out.println("不会执行");  // 不会执行
}

// 2. 程序所在线程死亡
// 3. 关闭CPU

3.3 try-finally(不捕获,只释放资源)

// 没有catch,异常会继续向上抛出
// 但finally中的资源释放代码一定会执行
FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    int data = fis.read();
} finally {
    if (fis != null) {
        try {
            fis.close();  // 关闭资源也可能抛异常
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.4 throws(声明异常)

踩坑提醒:throws不是把异常抛出去就完事了,最终一定要在某个地方捕获处理。如果一路throws到main方法,那就是把锅甩给了JVM,程序会直接崩溃。

// 方法声明throws,表示这个方法可能抛出异常
// 调用者必须处理
public void readFile(String path) throws IOException {
    FileInputStream fis = new FileInputStream(path);
    fis.close();
}

// 调用者处理方式1:try-catch
public void caller1() {
    try {
        readFile("test.txt");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 调用者处理方式2:继续throws
public void caller2() throws IOException {
    readFile("test.txt");
}

经验之谈:在分层架构中,DAO层抛出SQLException,Service层转换为业务异常,Controller层统一处理返回给前端。这种分层处理异常的方式让代码更清晰。

3.5 throw(手动抛出异常)

// throw:在方法内部手动抛出异常
public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄不合法:" + age);
    }
    this.age = age;
}

// throws:在方法签名上声明
public void setAge(int age) throws IllegalArgumentException {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄不合法:" + age);
    }
    this.age = age;
}

四、自定义异常

4.1 为什么需要自定义异常?

// Java内置的异常太笼统
throw new Exception("余额不足");  // 不够具体

// 自定义异常更语义化
throw new InsufficientBalanceException("余额不足,当前余额:" + balance);

4.2 自定义异常的写法

// 自定义运行时异常(不受检)
class InsufficientBalanceException extends RuntimeException {
    public InsufficientBalanceException(String message) {
        super(message);
    }
    
    public InsufficientBalanceException(String message, Throwable cause) {
        super(message, cause);
    }
}

// 自定义受检异常
class BusinessException extends Exception {
    private int code;  // 错误码
    
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
    
    public int getCode() {
        return code;
    }
}

4.3 使用自定义异常

class BankAccount {
    private double balance;
    
    public void withdraw(double amount) throws BusinessException {
        if (amount <= 0) {
            throw new BusinessException(1001, "取款金额必须大于0");
        }
        if (amount > balance) {
            throw new BusinessException(1002, "余额不足,当前余额:" + balance);
        }
        balance -= amount;
    }
}

public class Test {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        
        try {
            account.withdraw(-100);
        } catch (BusinessException e) {
            System.out.println("错误码:" + e.getCode());
            System.out.println("错误信息:" + e.getMessage());
        }
    }
}

五、异常处理的最佳实践

5.1 try-with-resources(Java 7+,推荐)

为什么推荐try-with-resources?

在Java 7之前,关闭资源需要写一大堆finally代码,既繁琐又容易出错。try-with-resources让资源管理变得优雅,而且异常处理也更完善——如果try和close都抛异常,close的异常会被抑制,不会丢失原始异常信息。

// 旧写法:手动关闭资源
FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    int data = fis.read();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 新写法:自动关闭资源(推荐)
try (FileInputStream fis = new FileInputStream("test.txt")) {
    int data = fis.read();
} catch (IOException e) {
    e.printStackTrace();
}
// fis会自动关闭,不需要finally

// 多个资源
try (
    FileInputStream fis = new FileInputStream("input.txt");
    FileOutputStream fos = new FileOutputStream("output.txt")
) {
    int data;
    while ((data = fis.read()) != -1) {
        fos.write(data);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// 两个流都会自动关闭

踩坑提醒:try-with-resources要求资源必须实现AutoCloseable接口。自定义资源时记得实现这个接口。

5.2 异常处理的规范

经验之谈:生产环境绝对不能直接用e.printStackTrace()!它会输出到控制台而不是日志文件,一旦服务重启,异常信息就丢失了。正确的做法是使用日志框架(如SLF4J)记录。

// ✅ 正确做法
public void method() {
    try {
        riskyOperation();
    } catch (SpecificException e) {
        log.error("具体异常", e);  // 记录日志
        throw new BusinessException("用户友好的提示");  // 抛出业务异常
    }
}

// ❌ 错误做法
public void method() {
    try {
        riskyOperation();
    } catch (Exception e) {
        e.printStackTrace();  // 只打印到控制台,生产环境不推荐
    }
}

// ❌ 最恶劣:吞掉异常
public void method() {
    try {
        riskyOperation();
    } catch (Exception e) {
        // 什么都不做,异常被"吞掉"了
    }
}

踩坑提醒:吞掉异常是调试噩梦!曾经排查一个Bug花了整整一天,最后发现是某个工具类把异常吞掉了,导致上层完全不知道发生了什么。

5.3 异常链

public void readFile() throws BusinessException {
    try {
        FileInputStream fis = new FileInputStream("config.txt");
    } catch (FileNotFoundException e) {
        // 保留原始异常信息
        throw new BusinessException(5001, "配置文件加载失败", e);
    }
}

// 获取异常链
try {
    readFile();
} catch (BusinessException e) {
    System.out.println("业务异常:" + e.getMessage());
    System.out.println("原始异常:" + e.getCause().getMessage());
    e.printStackTrace();  // 打印完整异常链
}

六、面试高频考点

考点1:final、finally、finalize的区别

关键字 作用
final 修饰类/方法/变量,表示不可变
finally 异常处理中的块,始终执行
finalize Object的方法,GC时调用(已废弃)

考点2:try-catch-finally的执行顺序

public static int test() {
    try {
        return 1;
    } finally {
        return 2;  // finally的return会覆盖try的return
    }
}
// 返回2(但不建议在finally中return)

考点3:受检异常vs不受检异常

// 受检异常:IOException、SQLException等
// 不受检异常:RuntimeException及其子类

// 编译器只检查受检异常

考点4:throw和throws的区别

关键字 位置 作用
throw 方法体内部 手动抛出异常对象
throws 方法签名上 声明方法可能抛出的异常

参考资料

  1. Oracle官方文档 - Exceptions
  2. Baeldung - Java Exceptions

七、总结

今天我们学习了:

  • ✅ Java异常体系的层次结构
  • ✅ 五种异常处理方式
  • ✅ 自定义异常的写法
  • ✅ try-with-resources自动关闭资源
  • ✅ 异常处理的最佳实践

重点记忆

  1. Error不需要处理,Exception需要处理
  2. 受检异常必须try-catch或throws
  3. finally始终执行(System.exit除外)
  4. 生产环境不要用e.printStackTrace()
  5. 推荐用try-with-resources管理资源

下一步预告
Day12我们将学习集合框架(上)——ArrayList、LinkedList等List家族。


互动话题:你在实际开发中,遇到过哪些让你印象深刻的异常?欢迎在评论区分享!

如果这篇文章对你有帮助,欢迎点赞、收藏!这是【JavaSE全面教学】系列的第11篇,关注我看完整套教程 👇


本文为【JavaSE全面教学】系列第11篇,持续更新中…

更多推荐