Java 从入门到精通(十一):异常处理与自定义异常,程序报错时到底该怎么处理?

Java 从入门到精通(十一):异常处理与自定义异常,程序报错时到底该怎么处理?

很多人刚学 Java 时,对"异常"这件事的第一反应通常很直接:

  • 代码报错了
  • 控制台一大片红字
  • 程序停了
  • 然后开始慌

于是很多初学者会把异常理解成一件纯粹负面的事,好像它只是"程序出问题的信号"。

但如果你从工程角度看,异常其实不是敌人。

异常本质上是在告诉你:程序运行过程中,出现了一个当前逻辑没能正常处理的问题。

它的意义不是"吓你",而是:

  • 让错误被明确暴露出来
  • 让调用方知道哪里出了问题
  • 让程序有机会优雅失败,而不是默默出错

如果没有异常机制,很多错误就会变成:

  • 逻辑悄悄错下去
  • 数据悄悄污染
  • 结果看起来还能跑,但已经不可信

这才是更可怕的事情。

所以,学异常处理,不是为了背 try-catch-finally 语法,真正重要的是理解三件事:

  1. 什么是异常,为什么程序会抛异常
  2. 什么情况下应该捕获,什么情况下不该乱捕获
  3. 当系统有自己的业务规则时,为什么需要自定义异常

这一篇就把这些问题彻底讲清楚。


一、先别急着写 try-catch:异常到底是什么?

你可以先把异常理解成:

程序运行期间发生的非正常情况。

比如:

  • 除数是 0
  • 数组下标越界
  • 访问了空对象
  • 文件不存在
  • 输入格式不合法
  • 数据库连接失败

这些都属于"程序没有按预期顺利执行下去"。

看一个最简单的例子:

java 复制代码
public class Demo {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        System.out.println(a / b);
    }
}

运行后会报错:

  • ArithmeticException

这说明:

  • Java 发现这里数学上不合法
  • 所以直接抛出异常
  • 程序中断执行

这里你要先建立一个意识:

异常不是编译器在"找茬",而是运行时在保护程序不继续带着错误状态往下走。


二、为什么 Java 不让很多错误"静默通过"?

如果没有异常机制,上面的除零场景可能就会变成:

  • 返回一个乱七八糟的值
  • 程序继续跑
  • 最后你根本不知道问题出在哪

这对工程是很危险的。

Java 选择的策略是:

一旦发现某些关键错误,就明确抛出来。

这样做有几个好处:

  • 错误不会被悄悄吞掉
  • 更容易定位问题来源
  • 可以由上层决定怎么处理

从这个角度看,异常本质上是:

程序中的错误传递机制。


三、异常出现后,程序会发生什么?

默认情况下,如果异常没有被处理,程序会:

  1. 立刻停止当前出错位置之后的代码
  2. 把异常信息打印到控制台
  3. 结束运行

比如:

java 复制代码
public class Demo {
    public static void main(String[] args) {
        System.out.println("程序开始");
        int result = 10 / 0;
        System.out.println("结果是:" + result);
        System.out.println("程序结束");
    }
}

输出会类似这样:

java 复制代码
程序开始
Exception in thread "main" java.lang.ArithmeticException: / by zero

注意后两句:

  • 结果是...
  • 程序结束

都不会执行。

因为异常一旦没被处理,流程就断掉了。


四、try-catch 是干什么的?

它的核心作用就一句话:

把可能出错的代码包起来,一旦出错,就转到备用处理逻辑。

例如:

java 复制代码
public class Demo {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;
            System.out.println(result);
        } catch (ArithmeticException e) {
            System.out.println("除数不能为 0");
        }

        System.out.println("程序继续执行");
    }
}

这里发生了什么?

  • try 里执行可能出错的代码
  • 一旦出现 ArithmeticException
  • 就进入 catch
  • 然后程序还能继续往下走

输出类似:

java 复制代码
除数不能为 0
程序继续执行

所以你可以把它理解成:

  • try:先尝试执行
  • catch:如果失败,就按预案处理

五、异常对象 e 到底有什么用?

很多初学者会写:

java 复制代码
catch (Exception e) {
    System.out.println("出错了");
}

这虽然能跑,但太粗糙。

因为异常对象 e 里其实带着很多信息。

比如:

java 复制代码
catch (ArithmeticException e) {
    System.out.println(e.getMessage());
}

输出:

java 复制代码
/ by zero

常见用法还有:

java 复制代码
e.printStackTrace();

它会打印完整错误栈,帮助你定位:

  • 哪一行出错
  • 调用链路是什么

工程里排查问题时,这很重要。

所以不要把 e 当摆设。


六、finally 又是干什么的?

finally 的作用是:

无论有没有异常,通常都会执行。

最常见的用途是做"收尾工作"。

比如:

  • 关闭文件
  • 关闭数据库连接
  • 释放资源
  • 打日志

例子:

java 复制代码
public class Demo {
    public static void main(String[] args) {
        try {
            System.out.println("执行 try");
            int x = 10 / 0;
        } catch (ArithmeticException e) {
            System.out.println("执行 catch");
        } finally {
            System.out.println("执行 finally");
        }
    }
}

输出:

java 复制代码
执行 try
执行 catch
执行 finally

所以你要记住:

finally 更像"善后区"。


七、什么时候该捕获异常,什么时候不该乱 catch?

这是异常处理里最容易学偏的一点。

很多初学者一看到报错,就本能地想:

  • 套一个 try-catch
  • 只要不报错就行

这其实很危险。

因为:

捕获异常,不等于解决异常。

比如:

java 复制代码
try {
    int result = 10 / 0;
} catch (Exception e) {
}

这样写最大的问题是:

  • 错误被吞掉了
  • 程序表面没报错
  • 但逻辑可能已经坏了

这类代码在线上最麻烦。

所以更合理的原则是:

1)你知道怎么处理时,再捕获

比如:

  • 输入数字格式不对,可以提示用户重新输入
  • 文件不存在,可以提示检查路径
  • 网络超时,可以稍后重试

2)你处理不了时,不要硬吞

如果你只是捕获了,却没有真正恢复逻辑,那应该:

  • 记录日志
  • 向上继续抛出
  • 或者转换成更合适的业务异常

八、Exception 能不能直接一把抓?

技术上可以。

比如:

java 复制代码
catch (Exception e) {
    e.printStackTrace();
}

但从代码质量角度,通常不推荐一上来就这么写。

因为这样会带来两个问题:

1)范围太大,不够精确

你本来可能只想处理数字格式错误,结果把别的异常也一起抓了。

2)容易掩盖真实问题

本来某个异常不该在这里处理,却被你顺手吞掉了。

更好的做法通常是:

优先捕获你明确知道会发生、也明确知道怎么处理的异常类型。

例如:

java 复制代码
catch (NumberFormatException e) {
    System.out.println("请输入合法数字");
}

这就比直接 catch (Exception e) 更清楚。


九、Java 异常大致分哪两类?

入门阶段你不用背特别细,只要先抓住这两个方向:

1)运行时异常

这类异常通常发生在代码运行过程中,很多是程序员逻辑问题导致的。

常见例子:

  • NullPointerException
  • ArithmeticException
  • ArrayIndexOutOfBoundsException
  • ClassCastException
  • NumberFormatException

特点可以简单理解为:

  • 编译不一定拦你
  • 运行时才爆

2)受检异常

这类异常通常是外部资源或环境相关问题。

比如:

  • 文件不存在
  • IO 失败
  • 数据库连接失败

这类异常 Java 常常要求你显式处理。

入门阶段先知道:

不是所有异常的性质都一样。

有些更偏"程序 bug",有些更偏"外部环境风险"。


十、throw 是什么?和 throws 又有什么区别?

这是很多人会混的地方。

1)throw

throw 是"主动抛出一个异常对象"。

例如:

java 复制代码
public class Demo {
    public static void main(String[] args) {
        int age = -1;
        if (age < 0) {
            throw new RuntimeException("年龄不能小于 0");
        }
    }
}

意思是:

  • 我发现这里的数据不合法
  • 所以我主动报错

2)throws

throws 是写在方法声明上的,表示:

这个方法可能抛出某种异常,调用方要知道。

例如:

java 复制代码
public void readFile() throws IOException {
}

意思是:

  • 这个方法执行时可能有 IO 异常
  • 你调用它时要处理

简单记忆:

  • throw:扔出去
  • throws:先声明我可能会扔

十一、什么时候需要自定义异常?

到这里就进入真正更像业务开发的部分了。

Java 自带很多异常类,但它们解决的大多是:

  • 空指针
  • 下标越界
  • 类型转换错误
  • IO 问题
  • 数学错误

可真实业务里,很多错误并不是"Java 运行机制错误",而是:

  • 余额不足
  • 库存不够
  • 用户未登录
  • 权限不足
  • 订单状态不允许退款
  • 学生成绩超出范围

这些错误如果都直接写成:

java 复制代码
throw new RuntimeException("余额不足");

也不是不能用,但可读性一般。

因为它没有明确表达:

  • 这是什么业务错误
  • 上层该怎么分类处理

所以这时候就会需要:

自定义异常。


十二、一个简单的自定义异常例子

比如我们定义一个"余额不足异常":

java 复制代码
class InsufficientBalanceException extends RuntimeException {
    public InsufficientBalanceException(String message) {
        super(message);
    }
}

然后在业务里使用:

java 复制代码
public class Account {
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    public void withdraw(double amount) {
        if (amount > balance) {
            throw new InsufficientBalanceException("余额不足,当前余额为:" + balance);
        }
        balance -= amount;
        System.out.println("取款成功,剩余余额:" + balance);
    }
}

测试:

java 复制代码
public class Demo {
    public static void main(String[] args) {
        Account account = new Account(500);
        account.withdraw(800);
    }
}

这样一看就很清楚:

  • 抛出的不是模糊的 RuntimeException
  • 而是明确的业务异常 InsufficientBalanceException

这对代码可维护性帮助很大。


十三、自定义异常到底带来了什么价值?

它最重要的价值有三个。

1)语义更清楚

看到异常类名就知道发生了什么。

比如:

  • LoginExpiredException
  • PermissionDeniedException
  • OrderStatusException

比"出错了"这种模糊表达强太多。

2)更方便分类处理

比如上层可以专门处理:

  • 参数异常
  • 权限异常
  • 订单异常
  • 支付异常

3)更贴近业务建模

异常不只是错误,它也是系统规则的一部分。

很多时候,异常类本身就在表达业务边界。


十四、初学者最容易踩的异常处理坑

1)一出错就 catch (Exception e)

这样写太粗,不利于维护。

2)catch 了却什么都不做

最典型错误:

java 复制代码
catch (Exception e) {
}

这会把问题直接吞掉。

3)把异常处理当成"消灭报错"

异常处理的目标不是让控制台安静,而是让程序在出错时有合理反应。

4)该校验的数据不提前校验,全靠异常兜底

比如用户输入年龄时,很多问题本来可以提前判断,不一定非要等异常来收场。

异常不是日常流程控制的替代品。


十五、写业务代码时,一个更实用的思路

以后你遇到异常,不要先问:

  • 这里怎么写 try-catch?

而先问这三个问题:

1)这个错误是谁造成的?

  • 程序逻辑问题?
  • 用户输入问题?
  • 外部资源问题?
  • 业务规则问题?

2)当前层能不能处理?

  • 能处理,就处理
  • 处理不了,就往上抛

3)这个错误值不值得做成业务异常?

如果它反复出现,而且有明确业务语义,通常值得自定义异常。

这比一味套模板更重要。


十六、小结

这一篇最重要的,不是背语法,而是把下面几件事真正理顺:

  1. 异常是程序运行时的非正常情况
  2. try-catch 是为了在出错时切换到备用处理逻辑
  3. finally 适合做收尾和资源释放
  4. 捕获异常不等于解决异常,不要乱吞异常
  5. 自定义异常的意义,在于让业务错误表达得更清楚

如果你把这几条理解透,后面学 IO、数据库、网络编程、Spring 全局异常处理时,都会顺很多。


相关推荐
sR916Mecz1 小时前
JavaParser使用指南
开发语言·c#
aP8PfmxS21 小时前
Lab3-page tables && MIT6.1810操作系统工程【持续更新】
java·linux·jvm
无籽西瓜a1 小时前
【西瓜带你学设计模式 | 第十二期 - 装饰器模式】装饰器模式 —— 动态叠加功能实现、优缺点与适用场景
java·后端·设计模式·软件工程·装饰器模式
吴声子夜歌1 小时前
Node.js——zlib压缩模块
java·spring·node.js
海参崴-2 小时前
深入剖析C语言结构体存储规则:内存对齐原理与实战详解
java·c语言·开发语言
南山乐只2 小时前
Java并发工具:synchronized演进,从JDK 1.6 锁升级到 JDK 24 重构
java·开发语言·后端·职场和发展
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第十三期 - 组合模式】组合模式 —— 树形结构统一处理实现、优缺点与适用场景
java·后端·设计模式·组合模式·软件工程
翊谦10 小时前
Java Agent开发 Milvus 向量数据库安装
java·数据库·milvus
晓晓hh10 小时前
JavaSE学习——迭代器
java·开发语言·学习