Java 异常 详解
一、什么是 Java 异常?
异常就是程序运行时出现的意外情况,会中断正常的代码执行流程。
出现异常后,正常流程会中断,但程序不会直接崩溃,Java 会启动专门的异常处理流程,用来容错、记录错误、恢复程序。
所有异常和错误的顶层父类都是Throwable,整体结构:
Error(错误):JVM 系统级错误,代表 JVM 运行环境故障,如内存溢出、栈溢出。代码无法修复,我们无法处理,不需要捕获;
Exception(异常)[经常发生]:代码异常,我们可以捕获和处理,分为两类:
- 受检异常/受控异常RuntimeException(编译期异常):编译时强制处理,如 IO 异常、SQL 异常;
- 非受检异常/非受控异常(运行时异常)[极少发生]:代码逻辑 BUG,如空指针、数组越界,不强制捕获。
注意:两种异常都在运行是才检测发生的,不要被名字迷惑了。
关于异常,对于经常发生的,我们要时常预防(就如同流感,满足“条件”就“发病”,所以要“打疫苗”),对于极少发生的,人为能够避免的(比如除零)(如同代码得了绝症)我们不处理,这就是我们面对异常的态度。
二、try-catch 核心用法
try-catch是 Java 异常处理的核心语法,用来捕获并处理异常,避免程序直接崩溃。
1. 标准语法结构
try{// 可能出错的代码}catch(异常类型 e){// 出错了在这里处理}或者更进一步的解释:
try{// 1. 只包裹可能抛出异常的代码(粒度最小化)// 2. 一旦这里发生异常,本行之后的代码立刻停止执行}catch(指定异常类型 变量名){// 3. 捕获后必须打印日志/输出提示,不能空捕获// 4. 针对性处理异常,提高程序健壮性}2. 完整代码示例
案例 1:数组越界(RuntimeException)
publicclassDemo{publicstaticvoidmain(String[]args){int[]arr={10,20,30};try{System.out.println(arr[10]);System.out.println("异常后代码不会执行");}catch(ArrayIndexOutOfBoundsExceptione){System.out.println("捕获运行时异常:数组下标越界!");}System.out.println("程序正常运行,没有崩溃");}}捕获异常:数组下标越界! 程序继续运行 案例 2:空指针异常(RuntimeException)Stringname=null;try{name.length();}catch(NullPointerExceptione){System.out.println("捕获运行时异常:对象为空,无法调用方法");}捕获异常:对象为空! 程序没有崩溃案例 3:文件不存在(受检 Exception)
importjava.io.FileInputStream;publicclassTestFile{publicstaticvoidmain(String[]args){try{// FileNotFoundException 属于受检异常,不加try-catch直接编译报错!!!FileInputStreamfis=newFileInputStream("test.txt");}catch(FileNotFoundExceptione){System.out.println("捕获受检异常:文件不存在");}}}捕获受检异常:文件不存在3. 为维持代码的健壮性
- try 范围最小化:只包裹可能出错的代码,不把整个方法包起来,方便精准定位异常;
- 捕获具体异常:不直接捕获
Exception,避免把所有错误一锅端(Exception e); - 不空 catch:必须打印日志或提示,禁止
catch {}吃掉异常; - 程序不中断:异常被处理后,后续代码正常运行,提升系统稳定性。
4. 多 catch 标准格式(子类在前,父类在后)
一段代码可能抛出多种异常时,使用多 catch 分别处理,严格遵守子类异常在前,父类兜底在后:
try{// 可能抛出多种异常的代码}catch(NullPointerExceptione){System.out.println("空指针异常:对象未初始化");e.printStackTrace();}catch(ArrayIndexOutOfBoundsExceptione){System.out.println("数组越界异常:索引超出范围");e.printStackTrace();}catch(Exceptione){// 父类异常兜底,防止未知异常导致崩溃System.out.println("未知异常,请检查代码");e.printStackTrace();}三、try-catch-finally
finally是异常处理的收尾模块,专门用于资源释放(关闭文件流、数据库连接、网络连接等),是保证代码健壮性的关键。
1. 标准语法格式
try{// 可能异常的业务代码}catch(Exceptione){// 异常处理逻辑}finally{// 资源释放/清理操作,必定执行}2. finally基础
publicclassFinallyBaseDemo{publicstaticvoidmain(String[]args){System.out.println("程序开始运行");Stringfile=null;try{System.out.println("打开文件");file="test.txt";// 模拟文件不存在,制造异常if("test.txt".equals(file)){inta=1/0;}System.out.println("读取文件内容");}catch(Exceptione){System.out.println("读取文件发生错误");}finally{// 无论有没有异常,这段代码一定执行System.out.println("执行finally:关闭文件,释放资源");}System.out.println("程序执行结束");}}程序开始运行 打开文件 读取文件发生错误 执行finally:关闭文件,释放资源 程序执行结束3. finally 执行
- 只要 JVM 不退出,finally 一定执行;
- try 中无论是否发生异常,finally 都会执行;
- try 中即使有
return,也会先执行 finally,再执行 return; - 唯一不执行的情况:
System.exit(0)关闭 JVM。
1.正常执行,规范写法,finally 无 return
publicclassFinallyReturnTest{publicstaticinttest(){try{System.out.println("进入try代码块");// 准备返回值return100;}catch(Exceptione){return-1;}finally{System.out.println("进入finally,一定会执行!");}}publicstaticvoidmain(String[]args){intres=test();System.out.println("方法最终返回值:"+res);}}进入try代码块 进入finally,一定会执行! 方法最终返回值:100try 抛出异常 + return,依旧先走 finally
publicclassFinallyExceptionReturnTest{publicstaticinttest(){try{System.out.println("try内部,准备抛出异常");intnum=1/0;// 算术异常return100;}catch(Exceptione){System.out.println("捕获异常");return-1;}finally{System.out.println("无论报错还是return,finally必执行");}}publicstaticvoidmain(String[]args){intres=test();System.out.println("最终返回:"+res);}}try内部,准备抛出异常 捕获异常 无论报错还是return,finally必执行 最终返回:-1注:方法执行return时,不会立刻退出,会先完整执行完 finally 代码块,再执行 return 返回结果;如果 finally 内部写了 return,会直接覆盖原有返回值,同时吞噬上层异常,开发规范中严禁这么写。
因此在有finally的情况下,为保持代码健壮性:
- 资源必释放:finally 保证流/连接一定会关闭,避免内存泄漏、文件占用;
- 空值判断:关闭资源前判断非空,防止空指针;
- 不写业务逻辑:finally 只做清理,不影响主流程;
- 禁止 return:finally 中写 return 会覆盖返回值、吞噬异常,严重破坏健壮性。
四、throw 和 throws
这两个关键字是异常抛出的核心
1. throws:方法声明异常—— 声明可能抛出的异常
throws 修饰在方法签名末尾,用于提前声明当前方法可能向外抛出的异常类型。表示“调用本方法时,可能会抛出这些异常”,调用者必须处理或继续向上抛出。
- 位置:方法签名末尾
- 作用:声明方法可能抛出的异常,交给调用方处理
- 标准格式:
返回值类型 方法名(参数列表)throws异常类型1,异常类型2{// 方法体 //语法规范:多个异常用逗号分隔,只写异常类名,//不实例化对象。}// 受检异常必须声明,提高代码可读性和健壮性publicstaticvoidreadFile()throwsIOException{// 方法业务逻辑}强制使用规范(核心)
- 仅针对受检异常必须声明
JDK 语法强制:所有直接继承Exception的受检异常,不写throws则编译报错。
底层逻辑:编译器静态检查,强制开发者感知 IO、数据库等不可控外部风险。 - 工具层、底层通用方法统一使用throws上抛,不在底层捕获处理
分层规范:底层只做能力封装,异常交给上层业务统一处理。 - 禁止笼统声明throws Exception
规范约束:精准列出方法实际抛出的异常,便于调用方针对性捕获,避免掩盖未知故障。
特点
- 不处理异常,只是预警。
- 可以同时声明多个异常。
- 调用 throws 声明了受检异常的方法时,调用者必须用 try-catch 捕获,或者自己也用 throws 继续向上抛。
示例
importjava.io.FileInputStream;importjava.io.IOException;publicclassDemo{// 声明:我可能报错,我不处理publicstaticvoidreadFile()throwsIOException{FileInputStreamfis=newFileInputStream("test.txt");}// 调用者必须处理publicstaticvoidmain(String[]args){try{readFile();}catch(IOExceptione){System.out.println("文件读取失败");}}}文件读取失败2. throw:手动抛异常—— 实际抛出异常
throw 写在方法内部,用于手动实例化并抛出异常对象,执行后立即中断当前代码流程。会在代码中主动制造并抛出一个异常。
- 位置:方法内部
- 作用:主动创建并抛出异常对象,立即中断流程
语法
thrownew异常类名(参数);强制使用规范
- 参数校验、业务状态校验统一使用 throw 主动抛出
入参为空、参数范围非法、业务状态不满足执行条件,提前抛出异常拦截,不进入后续业务逻辑; - 业务场景优先抛自定义运行时异常,原生运行时异常仅用于基础参数校验;
- 抛出异常时必须携带清晰描述信息,禁止throw new RuntimeException(“”)空提示;
- 受检异常使用 throw 抛出时,方法必须配套throws声明。
特点
- 一旦执行 throw,当前方法立即结束,后面的代码不再执行。
- 抛出的异常对象可以是 JDK 内置的,也可以是自己定义的。
- 如果抛出的是 受检异常(checked),则当前方法必须用 throws 声明,或者用 try-catch 捕获。
示例
publicintdivide(inta,intb){if(b==0){thrownewArithmeticException("除数不能为零");}returna/b;}3.作用
- 提前校验:throw 用于参数/状态校验,防患于未然;
- 明确错误信息:抛出带提示的异常,方便快速定位;
- throws 明确风险:调用方一看便知需要处理异常,提升协作健壮性。
4. 一句话区分
- throws:方法声明,我可能抛异常
- throw:方法内部,我现在抛异常(底层靠 JVM native 实现)
五、自定义异常
1、为什么要自定义异常?
- 使错误信息更具体、更有业务含义(如:InsufficientBalanceException 比 IllegalArgumentException 更清晰)。系统异常无法表达业务含义(用户不存在、余额不足、权限不足);
- 针对特定场景单独捕获和处理。
- 封装额外的错误信息(字段、状态码等)。
- 精准区分异常类型,避免用通用 Exception 混淆业务错误。
2. 一般标准格式
// 定义classMyExceptionextendsException{// 或 RuntimeExceptionpublicMyException(Stringmsg){super(msg);}}// 抛出thrownewMyException("自定义错误");// 捕获try{// ...}catch(MyExceptione){e.printStackTrace();}创建步骤
1). 继承Exception—— 受检异常 (Checked)
调用者必须显式处理(try-catch 或 throws)。
publicclassInvalidAgeExceptionextendsException{publicInvalidAgeException(){super();}publicInvalidAgeException(Stringmessage){super(message);}publicInvalidAgeException(Stringmessage,Throwablecause){super(message,cause);}}2). 继承RuntimeException—— 非受检异常 (Unchecked)
调用者可以不处理(编译不强制),适合编程错误(如参数校验失败)。
publicclassInsufficientBalanceExceptionextendsRuntimeException{privatedoubledeficit;// 额外信息:缺多少钱publicInsufficientBalanceException(Stringmessage,doubledeficit){super(message);this.deficit=deficit;}publicdoublegetDeficit(){returndeficit;}}示例:用户注册年龄限制
// 1. 自定义异常类classInvalidAgeExceptionextendsException{publicInvalidAgeException(Stringmessage){super(message);}}// 2. 使用异常的方法publicclassUserService{publicvoidregister(Stringname,intage)throwsInvalidAgeException{if(age<18){thrownewInvalidAgeException("未满18岁,无法注册");}System.out.println(name+" 注册成功");}}// 3. 调用并处理publicclassMain{publicstaticvoidmain(String[]args){UserServiceservice=newUserService();try{service.register("小明",16);}catch(InvalidAgeExceptione){System.out.println("注册失败:"+e.getMessage());}}}输出:
注册失败:未满18岁,无法注册示例:自定义业务异常
// 自定义业务异常publicclassBizExceptionextendsRuntimeException{publicBizException(Stringmsg){super(msg);}}publicclassDemo{publicstaticvoidbuy(intstock){if(stock<=0){thrownewBizException("库存不足,无法下单");}}publicstaticvoidmain(String[]args){try{buy(0);}catch(BizExceptione){System.out.println("业务提示:"+e.getMessage());}}}业务提示:库存不足,无法下单3)最佳实践
| 建议 | 说明 |
|---|---|
| 提供多个构造方法 | 至少提供无参、带 message、带 cause 的构造器 |
添加serialVersionUID | 如果异常可能被序列化(RMI、分布式),建议加上 |
类名以Exception结尾 | 符合 Java 命名惯例,如MyBusinessException |
| 区分 checked / unchecked | 可恢复的异常用Exception,编程错误用RuntimeException |
| 携带额外信息 | 通过自定义字段提供更丰富的错误上下文 |
自定义异常让你的代码更健壮、更易读、更容易调试。
4)养成习惯:
- 标准格式统一:try-catch-finally 按规范写,不随意简写;
- try 最小化:只包裹危险代码,不扩大捕获范围;
- 不空 catch:必须打印日志/提示,禁止吃掉异常;
- finally 只释放资源:不写业务,不写 return;
- throw 提前校验:主动抛异常,提高系统容错性;
- 自定义异常统一业务错误:不用通用异常混淆业务;
- 异常信息完整:带堆栈、带提示、带错误码。
六、异常的实质
1. 面向对象层面
异常的实质是普通Java对象,全部实例化自Throwable及其子类。
- 不管是系统自动抛出的空指针、数组越界,还是你手写
throw手动抛出的自定义异常,本质都是堆内存里创建出来的对象; - 这个对象内部封装了三类关键信息:
- 异常描述信息:错误文字提示(
getMessage()); - 完整调用栈帧:哪一行代码、哪个方法、哪个类触发的错误;
- 异常类型:用来被
catch精准匹配区分。
- 异常描述信息:错误文字提示(
- Java设计这套对象体系的目的:把错误信息、错误位置、错误类型封装成统一对象,统一传递、统一捕获、统一处理,替代C语言单纯返回错误码的简陋方案。
简单总结:异常 = 封装了完整错误信息的Throwable对象。
2.throw 代码层底层逻辑
1.执行 throw new 异常() 时,JVM 会创建一个异常对象(包含堆栈、类型、信息);
2.随后立即停止当前方法后续所有代码执行;
3.接着程序会一层一层往调用它的上层方法找,对比 catch 里写的异常类型,看能不能匹配上。
4.
1.找到匹配 catch:进入 catch 执行异常处理逻辑,处理完继续往后运行;
2.全程没有任何 catch 接住:JVM 在控制台打印完整红色异常堆栈,直接结束当前程序。
而异常本质是对象,throw 就是把这个封装好错误信息的对象向上传递给上层代码(抛给上层)。
配套简单示例
publicstaticvoidcheckAge(intage){// 参数校验,拦截非法数据if(age<0||age>150){// 手动构建异常对象并抛出thrownewIllegalArgumentException("年龄不合法,必须在 0-150 之间");}System.out.println("年龄校验通过");}调用checkAge(-5)时,直接抛出异常,后面打印语句不会执行。
3、throw 的 JVM Native(C++) 底层逻辑(深层原理)
我们写的 throw 只是 Java 语法关键字,真正完成异常整套操作的不是 Java,是 JVM 底层用 C++ 写的本地 native 方法。
完整流程:
- Java 代码读到 throw 关键字,通知 JVM 暂停当前线程执行;
- JVM 调用内部C++函数,完成异常对象初始化,抓取当前线程所有栈帧信息;
- C++ 底层自动遍历当前线程的调用栈,挨个查找能匹配该异常的 catch 代码块;
- 平时打印报错信息的
printStackTrace(),底层也是 native 方法,读取 C++ 保存的栈帧数据展示给用户; - 流程中断、异常向上传递、类型匹配判断,全部由C++代码实现,Java 只负责书写语法。
一句话总结:Java 只是提供了 throw 书写语法,真正抛异常、检索catch、生成报错堆栈,全靠 JVM 底层 C++ native 代码运行。
4、自定义异常底层运行逻辑
自定义异常没有独立全新的底层机制,完全依托 Java 继承体系和JVM统一异常规则,分4点讲清楚:
- 硬性规则:必须继承
Throwable
只有直接/间接继承Throwable(包含 Exception、RuntimeException),JVM 才会把这个类识别为合法异常,支持抛出、捕获、生成堆栈信息。普通类无法使用 throw 抛出。 - catch 匹配只看类型,不看类名
捕获异常时,JVM 通过类的继承关系判断是否匹配,和自定义类的名字无关。 - 父类决定异常分类
- 直接继承
Exception:受检异常,编译器强制要求要么 try-catch 捕获,要么方法加 throws 声明; - 继承
RuntimeException:运行时异常,编译无强制校验,业务开发最常用。
- 构造方法必须写
super(message)
子类构造方法调用父类 Throwable 的构造,作用是把自定义错误提示交给父类底层保存,后续打印异常时才能正常展示提示文字。
5、自定义异常依赖的 Native 底层支撑
打开 Throwable 源码可以看到核心 native 方法:
publicclassThrowableimplementsSerializable{// 生成异常堆栈的核心方法,由C++本地方法实现privatenativeThrowablefillInStackTrace(intdummy);}- 只要继承 Throwable,自定义异常会自动继承这个 native 底层能力,不用自己实现堆栈抓取、异常传递逻辑;
- 堆栈采集、异常抛出、栈帧遍历全部复用JVM统一的C++底层逻辑;
- 自定义异常仅做业务区分包装(用来分辨库存不足、用户名错误等业务场景),底层调度逻辑和系统自带异常完全共用一套native机制。
最终总结:自定义异常 = 业务场景分类包装 + 继承Throwable体系 + 复用JVM底层native整套异常调度机制。