Java 异常 详解
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数组越界RuntimeExceptionpublicclassDemo{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空指针异常RuntimeExceptionStringnamenull;try{name.length();}catch(NullPointerExceptione){System.out.println(捕获运行时异常对象为空无法调用方法);}捕获异常对象为空 程序没有崩溃案例 3文件不存在受检 Exceptionimportjava.io.FileInputStream;publicclassTestFile{publicstaticvoidmain(String[]args){try{// FileNotFoundException 属于受检异常不加try-catch直接编译报错!!!FileInputStreamfisnewFileInputStream(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-finallyfinally是异常处理的收尾模块专门用于资源释放关闭文件流、数据库连接、网络连接等是保证代码健壮性的关键。1. 标准语法格式try{// 可能异常的业务代码}catch(Exceptione){// 异常处理逻辑}finally{// 资源释放/清理操作必定执行}2. finally基础publicclassFinallyBaseDemo{publicstaticvoidmain(String[]args){System.out.println(程序开始运行);Stringfilenull;try{System.out.println(打开文件);filetest.txt;// 模拟文件不存在制造异常if(test.txt.equals(file)){inta1/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 无 returnpublicclassFinallyReturnTest{publicstaticinttest(){try{System.out.println(进入try代码块);// 准备返回值return100;}catch(Exceptione){return-1;}finally{System.out.println(进入finally一定会执行);}}publicstaticvoidmain(String[]args){intrestest();System.out.println(方法最终返回值res);}}进入try代码块 进入finally一定会执行 方法最终返回值100try 抛出异常 return依旧先走 finallypublicclassFinallyExceptionReturnTest{publicstaticinttest(){try{System.out.println(try内部准备抛出异常);intnum1/0;// 算术异常return100;}catch(Exceptione){System.out.println(捕获异常);return-1;}finally{System.out.println(无论报错还是returnfinally必执行);}}publicstaticvoidmain(String[]args){intrestest();System.out.println(最终返回res);}}try内部准备抛出异常 捕获异常 无论报错还是returnfinally必执行 最终返回-1注方法执行return时不会立刻退出会先完整执行完 finally 代码块再执行 return 返回结果如果 finally 内部写了 return会直接覆盖原有返回值同时吞噬上层异常开发规范中严禁这么写。因此在有finally的情况下为保持代码健壮性资源必释放finally 保证流/连接一定会关闭避免内存泄漏、文件占用空值判断关闭资源前判断非空防止空指针不写业务逻辑finally 只做清理不影响主流程禁止 returnfinally 中写 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{FileInputStreamfisnewFileInputStream(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(b0){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.deficitdeficit;}publicdoublegetDeficit(){returndeficit;}}示例用户注册年龄限制// 1. 自定义异常类classInvalidAgeExceptionextendsException{publicInvalidAgeException(Stringmessage){super(message);}}// 2. 使用异常的方法publicclassUserService{publicvoidregister(Stringname,intage)throwsInvalidAgeException{if(age18){thrownewInvalidAgeException(未满18岁无法注册);}System.out.println(name 注册成功);}}// 3. 调用并处理publicclassMain{publicstaticvoidmain(String[]args){UserServiceservicenewUserService();try{service.register(小明,16);}catch(InvalidAgeExceptione){System.out.println(注册失败e.getMessage());}}}输出注册失败未满18岁无法注册示例自定义业务异常// 自定义业务异常publicclassBizExceptionextendsRuntimeException{publicBizException(Stringmsg){super(msg);}}publicclassDemo{publicstaticvoidbuy(intstock){if(stock0){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 只释放资源不写业务不写 returnthrow 提前校验主动抛异常提高系统容错性自定义异常统一业务错误不用通用异常混淆业务异常信息完整带堆栈、带提示、带错误码。六、异常的实质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(age0||age150){// 手动构建异常对象并抛出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、RuntimeExceptionJVM 才会把这个类识别为合法异常支持抛出、捕获、生成堆栈信息。普通类无法使用 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整套异常调度机制。