初学异常总是容易将异常与错误弄混觉得为什么我出现的问题就会在开发阶段爆红,那些“规定”的异常却仅仅是抛出就可以了?

本文将对 throwstry-catch、受检异常、运行时异常进行分析

一些灵魂拷问: 

  • 为什么文件上传、IO 操作一写就报错,必须加 throwstry-catch
  • 为什么空指针、数组越界加throws没用,必须改代码?
  • 我加了 throws异常去哪了?谁在帮我处理?
  • 为什么有的错误抛出去就能跑,有的不改根源直接崩

一、Java 异常分两大类

1. 运行时异常(非受检异常 Unchecked Exception)

  • NullPointerException 空指针
  • ArrayIndexOutOfBoundsException数组越界 
  • ArithmeticException 除0错误 
  • ClassCastException 类型转换异

本质:代码逻辑 Bug

不是环境问题,是逻辑规则问题,是代码写炸了,只要跑到这一行,100%崩溃。

特点: 

  • 编译器不强制处理,不加 `try-catch` 也能编译
  • 加 `throws` 完全没用,只是掩耳盗铃
  • 必须从根源改代码,否则程序必死
  • 举个最经典的例子
String str = null;
str.length(); // 空指针!代码逻辑错误

就算写成运行照样崩:

public void test() throws NullPointerException {
    String str = null;
    str.length();
}

因为错误在代码本身,不在外部throws 救不了 Bug。

2. 受检异常(Checked Exception)

代表:

  • IOException`文件/IO 异常
  • SQLException数据库异常 
  • FileNotFoundException 文件不存在
  • ClassNotFoundException类找不到

本质:外部环境风险

 不是我们代码错,是外界不可控: 磁盘满了、文件夹没了、权限不够、网络断了、文件被占用……

特点:

  • 编译器强制处理,不处理直接编译报错 
  • 必须二选一:try-catch 自己处理 或throws给上层 - 不用改根源(根源你也改不了),只要**声明风险**就行 比如文件上传:

file.transferTo(new File("E:/images/1.jpg"));

行代码本身没问题,但可能因为环境失败

Java :我知道你控制不了天气,但你必须带伞(try-catch)承认可能下雨(throws)

二、throws 到底在干嘛?为什么加了就能编译运行?

很多小伙伴以为 throws是“解决异常”,大错特错!

throws 的真实身份:甩锅声明 【举个栗子】

public Result upload(MultipartFile file) throws IOException {
    file.transferTo(...);
}

翻译一下:我们的这个方法可能会出 IO 问题,不在这里处理,谁调用这个方法,谁负责收拾烂摊子。

加了 throws 为什么就能跑?

  • 编译器:你显式声明了风险,我就让你编译通过
  • 运行时: 没出问题 → 程序正常跑 - 真出问题 → 异常一路往上抛,Spring/Tomcat/JVM 帮我兜底 

谁在帮我们处理? 我们在 Controller 抛异常,调用我们方法的是 Spring 框架

框架有全局异常处理机制:

1. 捕获异常

2. 打印日志

3. 返回错误信息

4. 不让服务器崩溃

所以我们只需要抛,上层框架会帮我们扛。

三、try-catch 与 throws:when use who?

1. 用 try-catch 的场景

能自己处理,处理后程序能继续跑。 比如:

  •  文件上传失败,给前端返回“上传失败,请重试” 
  • 事务异常,手动回滚
  • 读取配置失败,给默认值
public Result upload(MultipartFile file) {
    try {
        file.transferTo(...);
    } catch (IOException e) {
        log.error("文件上传失败", e);
        return Result.error("上传失败,请稍后重试");
    }
    return Result.success();
}

2. 用 throws 的场景

我们处理不了,必须交给上层统一处理。 比如:

  • 工具类方法
  • Service 层抛给 Controller 
  • 统一异常处理前的业务层
// Service 层
public void uploadFile() throws IOException {
    // IO 操作
}

// Controller 层统一捕获或继续抛

四、迷惑症结

为什么每次用 IO之类 都必须处理? 因为文件、IO、数据库、网络全部属于外部不可控,Java 语法死规定: 只要调用可能抛出受检异常的方法,必须显式处理。 没得逃,每次都要处理,这就是 Java 的严谨。

客观不受控因素只要可能造成问题有造成问题的风险就会显示异常需要异常处理,无论是否真的出了问题,抛出异常就是告诉上层调用者我们知道会有出问题的风险并选择承担这个风险将异常抛出,直到异常抛到最顶层有两种结果

  • 是真的出了意料之中问题,此时程序也不能正常运行,需要切实解决这个环境因素导致的问题
  • 没有出现意料之中的问题,相当于处在风险概率的安全范围,承担住了风险,此时,程序正常运行

来一个形象比喻~: 运行时异常是我们题算错了,必须改答案;受检异常是出门可能下雨,不用改天气,只要带伞或声明就行。

 六、图标总结

特性 受检异常(如 IOException) 运行时异常(如 NPE)
是否强制处理必须,编译不通过不强制,可编译
根源外部环境风险代码逻辑 Bug
throws 有用吗有用,编译通过甩锅上层没用,运行照样崩
解决方案try-catch / throws必须改代码逻辑
是否可避免无法彻底避免完全可以避免

看到这里是否还会对着 `throws` 百思不得其解: 为什么有的错误抛出去就没事,有的必须改代码?

现在终于明白:Java 异常的设计,本质是区分“人为错误”和“客观风险”**。 -

  • 人为错误:必须我们自己修好 
  • 客观风险:只需声明,交给框架兜底

最后希望这篇博客能帮你彻底走出 Java 异常的迷雾,以后写 IO、文件、数据库代码时,再也不慌、不懵

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐