- 教程
- 技术博客
- 文档
🔥【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
本文是 YCBlogs 仓库中 bug/00.常见的异常.md 的深度展开。该文档系统梳理了 Java 标准库中 25 个常见异常(Exception)与 22 个常见错误(Error)的触发场景,是排查运行时崩溃、阅读崩溃日志的速查手册。读完本文,你将能够:从崩溃堆栈第一行快速判断异常/错误归属类别并定位根因;理解 Throwable 异常体系与检查/非检查异常的划分逻辑;掌握 try-catch-finally、throw/throws 的正确用法;并从字节码层面理解 JVM 如何通过异常表完成异常捕获,以及 try-catch 对性能的真实影响。
一、异常体系总览:先理解 Throwable、Exception、Error
在逐条解读常见异常与错误之前,必须先建立整体框架。Java 中所有的异常与错误都继承自Throwable,其直接子类只有两个分支:
Error:程序无法处理的严重问题,如内存溢出、栈溢出。大多数 Error 与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。这些错误发生时,JVM 一般会选择终止线程。不应该也不适合由应用程序捕获处理。Exception:程序本身可以处理的异常,是日常开发中主要面对的对象,又分为运行时异常(RuntimeException 及其子类)和编译期(非运行时)异常。
从"是否需要强制处理"的角度看,Java 异常又分为两类(详见 Java异常问题):
| 分类 | 范围 | 是否强制处理 |
|---|---|---|
| 受检查异常(checked) | Exception 中除 RuntimeException 及其子类以外的所有异常,如 IOException、SQLException | 必须显式处理:要么 try-catch 捕获,要么在方法签名上用 throws 声明,否则无法通过编译 |
| 不受检查异常(unchecked) | Error 全部、RuntimeException 及其子类,如 NullPointerException、ClassCastException | 编译器不检查,可以不捕获不声明,由 JVM 在运行时自动抛出 |
在 Throwable异常体系 中有更完整的架构说明:Exception 可以是可被控制(checked)或不可控制的(unchecked),表示由程序员导致的错误,应在应用程序级被处理;Error 总是不可控制的,经常用于表示系统错误或底层资源错误,如有可能应在系统级被捕捉。
一个有效使用的异常应当清晰回答三个问题(what / where / why):
- 异常类型:告诉"什么"被抛出;
- 异常堆栈跟踪:告诉"在哪里"抛出;
- 异常信息:告诉"为什么"抛出。
例如仓库中的一条典型 Android 崩溃信息:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.app.Activity.finish()' on a null object reference at com.com.yc.toollib.crash.CrashTestActivity.onClick(CrashTestActivity.java:48) at android.view.View.performClick(View.java:7187) ... at android.os.Looper.loop(Looper.java:230) at android.app.ActivityThread.main(ActivityThread.java:7742)其中异常类型是NullPointerException,异常信息说明是在一个 null 对象上调用了finish()方法,堆栈轨迹则展示了从ActivityThread.main到onClick的完整调用链,帮助定位到第 48 行的具体代码。
二、Exception 分类详解:25 种常见异常及触发场景
以下为 bug/00.常见的异常.md 中完整收录的 25 种常见异常,按触发场景分组解读,并补充了触发原因与规避思路。
2.1 算术与数字类
| 异常类 | 触发场景 |
|---|---|
java.lang.ArithmeticException | 算术条件异常,如整数除零等。典型例子int a = 520 / 0;会抛出该异常 |
java.lang.NumberFormatException | 数字格式异常。试图将 String 转换为指定的数字类型,而该字符串不满足数字类型要求的格式时抛出,如Integer.parseInt("abc") |
java.lang.NegativeArraySizeException | 数组大小为负值异常。使用负数大小值创建数组时抛出,如new int[-3] |
2.2 数组与索引类
| 异常类 | 触发场景 |
|---|---|
java.lang.ArrayIndexOutOfBoundsException | 数组索引越界异常。对数组的索引值为负数或大于等于数组大小时抛出 |
java.lang.ArrayStoreException | 数组存储异常。向数组中存放非数组声明类型对象时抛出,如向Object[]中放入与其运行时类型不兼容的对象 |
java.lang.IndexOutOfBoundsException | 索引越界异常。访问某个序列的索引值小于 0 或大于等于序列大小时抛出,是数组/字符串越界异常的基类 |
java.lang.StringIndexOutOfBoundsException | 字符串索引越界异常。使用索引值访问字符串中的字符,而索引值小于 0 或大于等于字符串长度时抛出 |
2.3 类型转换与反射类
| 异常类 | 触发场景 |
|---|---|
java.lang.ClassCastException | 强制类型转换异常。类 A 和 B 互不是父类或子类,O 是 A 的实例,强制将 O 构造为类 B 的实例时抛出,即常说的"强制类型转换异常" |
java.lang.ClassNotFoundException | 找不到类异常。应用根据字符串形式的类名构造类(如Class.forName),在遍历 CLASSPATH 之后找不到对应名称的 class 文件时抛出 |
java.lang.IllegalAccessException | 违法的访问异常。通过反射方式创建类的实例、访问类属性、调用类方法,而当时无法访问类、属性、方法或构造方法的定义时抛出 |
java.lang.InstantiationException | 实例化异常。试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时抛出 |
java.lang.TypeNotPresentException | 类型不存在异常。以类型名称的字符串表达方式访问该类型,但根据给定名称找不到该类型时抛出。与 ClassNotFoundException 的区别在于:该异常是 unchecked(不被检查)异常,而 ClassNotFoundException 是 checked(被检查)异常 |
2.4 状态与线程类
| 异常类 | 触发场景 |
|---|---|
java.lang.IllegalStateException | 违法的状态异常。在 Java 环境和应用尚未处于某个方法的合法调用状态,而调用了该方法时抛出。在 Android 开发中非常常见,如 崩溃bug日志总结1 中记录的资源未初始化就使用、Can't compress a recycled bitmap等 |
java.lang.IllegalMonitorStateException | 违法的监控状态异常。线程试图等待一个自己并不拥有的对象(O)的监控器,或者通知其他线程等待该对象(O)的监控器时抛出。典型场景是在synchronized块之外调用wait()/notify() |
java.lang.IllegalThreadStateException | 违法的线程状态异常。线程尚未处于某个方法的合法调用状态而调用了该方法时抛出,如在已启动的线程上再次调用start() |
java.lang.InterruptedException | 被中止异常。线程处于长时间的等待、休眠或其他暂停状态,此时其他线程通过Thread.interrupt()方法终止该线程时抛出 |
2.5 空值与属性方法类
| 异常类 | 触发场景 |
|---|---|
java.lang.NullPointerException | 空指针异常。在要求使用对象的地方使用了 null 时抛出,如调用 null 对象的实例方法、访问 null 对象的属性、计算 null 对象的长度、使用 throw 语句抛出 null 等。这是 Java 异常中最常见也最"令人头疼"的异常 |
java.lang.NoSuchFieldException | 属性不存在异常。访问某个类不存在的属性时抛出 |
java.lang.NoSuchMethodException | 方法不存在异常。访问某个类不存在的方法时抛出 |
2.6 其他专用异常
| 异常类 | 触发场景 |
|---|---|
java.lang.CloneNotSupportedException | 不支持克隆异常。没有实现 Cloneable 接口或对象不支持克隆方法时,调用其clone()方法抛出 |
java.lang.EnumConstantNotPresentException | 枚举常量不存在异常。通过名称和枚举类型访问枚举对象,但该枚举对象并不包含该常量时抛出 |
java.lang.SecurityException | 安全异常。由安全管理器抛出,用于指示违反安全情况 |
java.lang.UnsupportedOperationException | 不支持的方法异常。指明请求的方法不被支持的情况。它是 RuntimeException 的子类,常见于未实现的方法体(如throw new UnsupportedOperationException("Not implemented")) |
2.7 两个"根"异常
| 异常类 | 说明 |
|---|---|
java.lang.Exception | 根异常。用以描述应用程序希望捕获的情况,是所有受检查异常与运行时异常的公共父类 |
java.lang.RuntimeException | 运行时异常。是所有 Java 虚拟机正常操作期间可以被抛出的异常的父类。它及其子类属于不受检查异常,程序可以选择捕获处理也可以不处理,但一旦发生会导致程序中断执行,因此开发时最好仍用 try-catch 处理以保证程序健壮性 |
关于运行时异常与编译期异常的划分,在 Throwable异常体系 中有详细说明:
- 编译时异常:Java 程序必须显式处理,否则程序会发生错误无法通过编译;
- 运行时异常:无需显式处理,也可以和编译时异常一样处理。运行时异常一般由程序逻辑错误引起,应从逻辑角度尽可能避免这类异常的发生;
- 被检查异常:
Exception类本身及其子类中除"运行时异常"之外的其它子类,编译器会检查它。例如CloneNotSupportedException,通过clone()接口克隆一个未实现Cloneable接口的对象时抛出,必须显式处理才能编译通过。被检查异常通常都是可以恢复的。
三、Error 分类详解:22 种常见错误及触发场景
Error 是程序无法处理的严重问题,大多数与代码编写者执行的操作无关,而表示代码运行时 JVM 出现的问题。这些错误是"不可查"的,因为它们在应用程序的控制和处理能力之外。以下为原文档完整收录的 22 种常见错误,按触发场景分组。
3.1 虚拟机与资源类
| 错误类 | 触发场景 |
|---|---|
java.lang.VirtualMachineError | 虚拟机错误。指示虚拟机被破坏或者继续执行操作所需的资源不足的情况 |
java.lang.InternalError | 内部错误。指示 Java 虚拟机发生了内部错误 |
java.lang.UnknownError | 未知错误。指示 Java 虚拟机发生了未知严重错误的情况 |
java.lang.OutOfMemoryError | 内存不足错误。可用内存不足以让 Java 虚拟机分配给一个对象时抛出。在 Java异常问题 中专门讨论了 OOM 能否被 try-catch 的问题(详见后文) |
java.lang.StackOverflowError | 堆栈溢出错误。应用递归调用的层次太深而导致堆栈溢出时抛出 |
java.lang.ThreadDeath | 线程结束。调用 Thread 类的 stop 方法时抛出,用于指示线程结束。注:Thread.stop()已被废弃,不应在业务代码中使用 |
3.2 类加载与字节码类
| 错误类 | 触发场景 |
|---|---|
java.lang.ClassFormatError | 类格式错误。Java 虚拟机试图从一个文件中读取 Java 类,而检测到文件内容不符合类的有效格式时抛出 |
java.lang.ClassCircularityError | 类循环依赖错误。初始化一个类时,检测到类之间循环依赖则抛出 |
java.lang.NoClassDefFoundError | 未找到类定义错误。Java 虚拟机或类装载器试图实例化某个类,而找不到该类的定义时抛出。与ClassNotFoundException的区别在于:前者是 Error(编译期存在、运行期找不到),后者是 checked 异常 |
java.lang.VerifyError | 验证错误。验证器检测到某个类文件中存在内部不兼容或者安全问题时抛出 |
java.lang.UnsupportedClassVersionError | 不支持的类版本错误。Java 虚拟机试图读取某个类文件,但发现该文件的主、次版本号不被当前 Java 虚拟机支持时抛出。即常说的"类由更高版本 JDK 编译导致版本不兼容" |
3.3 链接与依赖类
| 错误类 | 触发场景 |
|---|---|
java.lang.LinkageError | 链接错误。该错误及其所有子类指示某个类依赖于另外一些类,在该类编译之后,被依赖的类改变了其类定义而没有重新编译所有类,进而引发错误的情况 |
java.lang.IncompatibleClassChangeError | 不兼容的类变化错误。正在执行的方法所依赖的类定义发生了不兼容的改变时抛出。一般在修改了应用中的某些类的声明定义而没有对整个应用重新编译而直接运行时容易引发 |
java.lang.AbstractMethodError | 抽象方法错误。应用试图调用抽象方法时抛出 |
java.lang.IllegalAccessError | 违法访问错误。应用试图访问、修改某个类的域(Field)或调用其方法,但又违反域或方法的可见性声明时抛出 |
java.lang.NoSuchFieldError | 域不存在错误。应用试图访问或修改某类的某个域,而该类的定义中没有该域的定义时抛出 |
java.lang.NoSuchMethodError | 方法不存在错误。应用试图调用某类的某个方法,而该类的定义中没有该方法的定义时抛出 |
java.lang.UnsatisfiedLinkError | 未满足的链接错误。Java 虚拟机未找到某个类的声明为 native 方法的本机语言定义时抛出。在 Android 上典型表现为 so 库加载失败,见 崩溃bug日志总结1 中的libijkffmpeg.so崩溃案例 |
3.4 初始化与断言类
| 错误类 | 触发场景 |
|---|---|
java.lang.ExceptionInInitializerError | 初始化程序错误。执行一个类的静态初始化程序的过程中发生异常时抛出。静态初始化程序指直接包含于类中的 static 语句段 |
java.lang.InstantiationError | 实例化错误。应用试图通过 Java 的 new 操作符构造一个抽象类或接口时抛出 |
java.lang.AssertionError | 断言错误。用来指示一个断言失败的情况。注意:Error本身是所有错误的基类,用于标识严重的程序运行问题,这些问题通常描述一些不应被应用程序捕获的反常情况 |
四、异常处理机制:try-catch-finally 与 throw/throws
理解了异常分类之后,需要掌握如何正确处理它们。完整的机制梳理见 异常处理的流程机制。
4.1 五个关键字
| 关键字 | 作用 |
|---|---|
try | 用于监听。将要被监听的代码(可能抛出异常的代码)放在 try 语句块之内,当 try 块内发生异常时,异常就被抛出 |
catch | 用于捕获异常。catch 用来捕获 try 语句块中发生的异常 |
finally | 无论是否捕获或处理异常,finally 块里的语句都会被执行。主要用于回收在 try 块里打开的物理资源(数据库连接、网络连接、磁盘文件)。只有 finally 块执行完成之后,才会回来执行 try 或 catch 块中的 return 或 throw 语句;如果 finally 中使用了 return 或 throw 等终止方法的语句,则不会跳回执行,直接停止 |
throw | 用于在方法体内抛出异常对象 |
throws | 用在方法签名中,用于声明该方法可能抛出的异常 |
两种处理方式的使用原则:如果该功能内部可以将问题处理,用 try;如果处理不了,交由调用者处理,则用 throws。区别在于:后续程序需要继续运行就用 try;后续程序不需要继续运行就用 throws。
4.2 try-catch-finally 的执行顺序
完整的执行顺序分析如下:
- try 没有捕获到异常时:try 语句块中的语句逐一被执行,程序跳过 catch 语句块,执行 finally 语句块和其后的语句;
- try 捕获到异常,但 catch 中没有处理此异常的语句:此异常将抛给 JVM 处理。finally 语句块仍会被执行,但 finally 之后的语句不会被执行;
- try 捕获到异常,且 catch 中有处理此异常的语句:程序跳到 catch 语句块逐一匹配,找到对应的处理程序,其它 catch 语句块不会被执行,try 中异常之后的语句也不会被执行;catch 执行完后执行 finally,最后执行 finally 之后的语句。
4.3 throw 与 throws 的区别
throws:用在方法声明后面,跟的是异常类名,可以跟多个异常类名用逗号隔开。表示抛出异常,由该方法的调用者来处理;throws 表示出现异常的一种可能性,并不一定会发生这些异常。throw:用在方法体内,跟的是异常对象名,只能抛出一个异常对象(自己 new 出来的)。表示抛出异常,由方法体内的语句处理。throw 语句之后的执行流程会立即停止,所有后续语句都不会执行,然后依次检查所有 catch 语句是否与异常类型匹配;如果没有匹配的 catch,默认的异常处理程序会终止程序并输出堆栈踪迹。
一个典型用法对比:方法声明static void pop() throws NegativeArraySizeException表示 pop 方法可能抛出该异常,由调用者(如 main)处理;而在方法内部throw new NullPointerException("NullPointer")则是主动抛出一个新的异常对象,由上层 try-catch 捕获或交由 JVM 默认处理程序。
4.4 捕获异常时的关键注意事项
- catch 中要注意异常层级关系:异常子类必须位于异常超类之前,因为使用了某个超类的 catch 语句会捕获这个超类及其所有子类的异常。如果子类位于超类之后,永远也不会到达子类,不可到达的代码会被编译器提示错误。例如先
catch (Exception e)再catch (RuntimeException e)会报 "exception java.lang.RuntimeException has already been caught" 的编译错误,因为 Exception 是 RuntimeException 的超类,所有 RuntimeException 都会被第一个 catch 块捕获。 - 多 catch 子句的匹配:抛出异常时,异常处理系统按代码书写顺序找出"最近"的处理程序,找到匹配的处理程序后认为异常已得到处理,不再继续查找。查找时并不要求抛出的异常和处理程序声明的异常完全匹配,派生类的对象也可以匹配其基类的处理程序。也可以用
catch (ArithmeticException | ArrayIndexOutOfBoundsException e)的方式在同一个 catch 子句中捕获多种异常,每个多重捕获参数被隐式声明为 final 类型。 - 覆盖方法的异常声明限制:覆盖一个方法时,不能声明与覆盖方法不同的异常,声明的任何异常必须是被覆盖方法所声明异常的同类或子类。例如父类方法
throws IOException,子类覆盖时只能抛 IOException 或其子类,不能抛其超类 Exception。 - finally 不一定会执行:在 4 种特殊情况下 finally 块不会被执行——finally 语句块中发生异常、前面的代码用了
System.exit()退出程序、程序所在的线程死亡、关闭 CPU。另外如果 try 语句本身没有被执行到(如在 try 之前就 return),finally 也不会执行。
五、finally 与 return 的微妙关系
finally处理逻辑介绍 用多个可运行案例验证了 finally 与 return 的执行顺序,结论如下:
- finally 在 return 语句执行之后、return 返回之前执行:try 中的
return b += 80先执行(此时 b 变为 100),但并没有直接返回,而是等 finally 执行完之后再返回结果 100; - finally 中的 return 会覆盖 try 中的 return:如果 finally 里也有 return 语句,会直接返回 finally 中的值,try 中是否还有 return 语句都不再起作用,此时 try 之后的 return 会变成不可到达语句(需注释掉否则编译器报错);
- finally 中修改基本类型变量不影响返回值:
finally里b = 150并不会改变 try 中 return 的值(返回 100),因为 return 已经确定了返回值;但如果 return 的是一个引用类型(如 HashMap),finally 中修改该对象的内容(如map.put(...))会生效,而map = null这类重新赋值不生效——这正是 Java 只有值传递的体现; - try 中的 return 在异常情况下不会执行:若在 return 之前发生除零异常,则转而执行 catch 和 finally,此时它们对变量的修改都会影响最终返回的
return b的值。
六、JVM 层面的异常处理:异常表、字节码与性能真相
要真正理解异常,需要下沉到 JVM 与字节码层面。这部分是 JVM处理异常解析 与 异常try-catch性能 的核心内容。
6.1 异常实例的构造十分昂贵
在构造异常实例时,JVM 需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并记录下各种调试信息,包括栈帧所指向的方法的名字、方法所在的类名、文件名,以及在代码中的第几行触发该异常。在生成栈轨迹时,JVM 会忽略掉异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起。
由此引出一个实践问题:既然异常构造昂贵,能否缓存异常实例在需要时直接抛出?语法上允许,但该异常对应的栈轨迹并非 throw 语句的位置,而是新建异常的位置,这会误导开发人员定位到错误位置。因此实践中仍应抛出新建异常实例。
6.2 虚拟机如何捕获异常:异常表
每个编译后的方法都附带一个异常表(Exception table)。异常表中的每一个条目都代表一个异常处理器,由 from、to、target 指针及其异常类型构成,这些指针的值是字节码索引(bytecode index, bci)。其中 from 和 to 标示了该异常处理器所监控的范围(即 try 代码覆盖范围),target 指向异常处理器的起始位置(即 catch 起始位置)。
例如对如下代码编译后查看字节码(javap -c):
0: invokestatic mayThrowException:()V 3: goto 11 6: astore_1 7: aload_1 8: invokevirtual java.lang.Exception.printStackTrace 11: return Exception table: from to target type 0 3 6 Class java/lang/Exception该条目的 from 指针和 to 指针分别为 0 和 3,代表监控范围从索引 0 的字节码开始到索引 3 的字节码结束(不包括 3);target 是 6,代表异常处理器从索引 6 的字节码开始;最后一列说明捕获的异常类型是 Exception。
运行时的匹配过程:当程序触发异常时,JVM 从上至下遍历异常表中的所有条目。如果触发异常的字节码索引值落在某个条目监控范围内,JVM 判断所抛出异常与该条目要捕获的异常是否匹配,匹配则将控制流转移至 target 指向的字节码。如果遍历完所有条目仍未匹配到异常处理器,JVM 会弹出当前方法对应的 Java 栈帧,在调用者(caller)中重复上述操作。最坏情况下,JVM 需要遍历当前线程 Java 栈上所有方法的异常表。
6.3 finally 代码块如何编译
finally 代码块的编译是一个"复制"策略:当前 JVM 的做法是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块并捕获所有种类的异常(javap 中以 any 指代),这些条目的 target 指向另一份复制的 finally 代码块,并且在这个 finally 代码块的最后重新抛出所捕获的异常。
以try-catch-finally的经典案例编译后查看,可以发现有三份 finally 代码块:前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口,最后一份作为异常处理器监控 try 块和 catch 块,捕获 try 块触发的、未被 catch 捕获的异常,以及 catch 块触发的异常。
由此引出一个调试陷阱:如果 catch 块捕获了异常并且触发了另一个异常,finally 捕获并重抛的是后者(catch 中触发的异常),原本的异常便会被忽略掉,这对代码调试十分不利。为此 Java 7 引入了Suppressed 异常,允许将一个异常附于另一个异常之上,抛出的异常可以附带多个异常信息;并配套提供了try-with-resources语法糖,在字节码层面自动使用 Suppressed 异常,同时极大精简了资源打开关闭的用法——程序可以在 try 关键字后声明并实例化实现了AutoCloseable接口的类,编译器将自动添加对应的 close() 操作。运行该类程序可以看到输出中的Suppressed: java.lang.RuntimeException: Foo2 / Foo1 / Foo0,正是多资源关闭异常被正确保留的证据。
6.4 try-catch 影响性能吗:用实验说话
异常try-catch性能 通过字节码和耗时实验给出了明确结论:
在未抛出异常的情况下,try-catch 几乎没有性能开销。原因在于:类会跟随一张异常表,每个 try-catch 都会在表里添加行记录(try 开始地址、结束地址、异常处理起始位、异常类名称)。代码在运行时抛出异常时,首先拿着抛出位置到异常表中查找是否可以被 catch;如果异常没发生,也就不会去查表。try 的范围大小其实就是异常表中开始地址和结束地址两个值的差异而已,也不影响性能。
实验数据也印证了这一点:同样一段循环代码,不触发异常(i>0)运行耗时约 1133(纳秒级别计数),触发一次除零异常(i>=0)后耗时飙升至约 44177——一旦程序进入 catch 分支,是非常耗资源的(因为需要构造异常实例并生成栈轨迹)。同时,无论把 try 放在 for 循环外面还是里面,从运行时长和字节码指令角度看性能都基本一致,并不存在"for 循环里放 try 会资源消耗加倍"的问题。真正昂贵的不是 try 块本身,而是异常实例的构造与异常发生的频率。
七、异常链、自定义异常与 OOM 能否被捕获
7.1 异常链
"异常链"是 Java 中非常流行的异常处理概念:在进行一个异常处理时抛出了另外一个异常,由此产生一个异常链条。该技术大多用于将"受检查异常"封装成为"非受检查异常"或 RuntimeException。如果因为异常决定抛出一个新的异常,一定要包含原有的异常,这样处理程序才可以通过getCause()和initCause()方法访问异常最终的根源(详见 Java异常问题)。
7.2 自定义异常与空 catch 块
自定义异常一般继承 Exception 类或其子类,步骤大体为:定义异常类继承合适的父类、提供无参/带参构造器、按需重写方法。开发中常见的自定义异常场景包括业务校验失败、参数校验失败等需要携带特定业务语义的错误。
空 catch 块是最糟糕的编程例子:异常被捕获后没有任何处理或提示,将失去关于异常的全部信息,成为调试的噩梦。catch 中至少要打印异常信息(哪怕是一条输出语句),不能将异常信息隐藏。
7.3 OOM 能被 try-catch 吗
原则上来讲,catch 的是 Error 时,触发 Error 的执行状态已经无法恢复,需要终止线程甚至终止虚拟机,这是不应该被应用层捕获的异常。但严格来说 OOM 在特定条件下是可以被 catch 住的,需要两个先决条件:
- 触发 OOM 的代码是开发者可控的;
- 在 try 块中申明对象并申请了大段内存,导致触发 OOM。
只有满足这两个条件,才对 OOM 有控制权。但通常不推荐主动 catch OOM——哪怕在此处 catch 住了,App 当前状态也已经处于"濒危"状态,如果不采取措施,此时不崩,换一个地方也会崩。如果确实 catch 住了 OOM,应主动释放一些可控内存、做好内存管理,避免后续操作立即再次触发 OOM。
八、实战:从崩溃日志速查常见异常与错误
8.1 Android 上的典型崩溃案例
在 Android 开发中,异常与错误直接表现为应用崩溃。仓库的 崩溃bug日志总结1 收录了大量真实崩溃日志,可与上文分类一一对应:
java.lang.UnsatisfiedLinkError(对应上文 3.3 节):找不到 so 库导致崩溃,例如播放视频时找不到libijkffmpeg.so。排查思路:检查 so 在安装过程中是否丢失、loadLibrary是否使用了正确的 so 文件名并捕获处理、so 架构是否与设备架构一致(如在 64-bit 架构下调用 32-bit 的 so)。可通过abiFilters 'armeabi-v7a'等配置控制打包的 CPU 类型。java.lang.IllegalStateException(对应上文 2.4 节):非法状态异常,如Can't compress a recycled bitmap——对已经 recycle 的 Bitmap 执行压缩操作。android.content.res.Resources$NotFoundException:资源找不到异常。java.lang.IllegalArgumentException:参数不匹配异常。java.lang.NullPointerException(对应上文 2.5 节):空指针异常,Android 崩溃占比最高的异常类型。android.view.WindowManager$BadTokenException:窗口 token 非法导致的弹窗崩溃。java.lang.ClassCastException(对应上文 2.3 节):类转化异常。
8.2 ANR 不是异常也不是错误
值得注意的是,ANR(Application Not Responding)本身不属于 Error 或 Exception,没有异常日志,在第三方崩溃日志中也不会有记录。ANR 的排查需要从 ANR 文件、进程堆栈、CPU/IO 使用情况等维度入手,具体分析见 ANR治理优化实践。
8.3 崩溃日志排查方法论
结合 Throwable异常体系 中关于ThrowableAPI 的介绍,排查崩溃时通常使用三个核心方法:
| 方法 | 作用 |
|---|---|
getCause() | 返回抛出异常的原因,如果 cause 不存在或未知则返回 null |
getMessage() | 返回异常的消息信息(错误性质) |
printStackTrace() | 将对象的堆栈跟踪输出至错误输出流(System.err) |
printStackTrace()所提供的信息也可以通过getStackTrace()直接访问,它返回由栈轨迹中元素构成的数组,每个元素表示栈中的一帧:元素 0 是栈顶元素,是调用序列中的最后一个方法调用(即这个 Throwable 被创建和抛出之处);数组中的最后一个元素和栈底是调用序列中的第一个方法调用。每个StackTraceElement可以获取类名、源文件名、行号、方法名,以及是否为 native 方法等信息。
九、异常处理的最佳实践
综合 异常处理的流程机制 与 Java异常问题,整理出以下实践要点:
- 捕获特定异常而非泛化异常:尽量不要捕获类似 Exception 这样的通用异常,应该捕获特定异常。泛泛的 Exception 会隐藏代码意图,还可能捕获到不希望捕获的 RuntimeException。除非深思熟虑,否则不要捕获 Throwable 或 Error,很难保证能正确处理 OutOfMemoryError。
- 不要生吞异常:生吞异常(捕获后既不抛出也不记录日志)往往基于"这段代码可能不会发生"的假设,但千万不要在产品代码中做这种假设。否则程序可能在后续代码以不可控的方式结束,没人能判断究竟哪里抛出了异常。
- catch 中的代码不要太多:
printStackTrace()输出到标准错误流(STDERR),在复杂生产系统中这不是合适的输出选项,很难判断输出到哪里去了。 - 不要用异常处理机制代替判断:本来应该判 null 的,结果使用了异常处理机制来代替,这是本末倒置。
- 不要打印堆栈后再抛出异常:打印异常后重新抛出,调用者可能也打印了异常,重复的打印信息会增添排查问题的难度。
- 调用方法返回布尔值代替返回 null,避免 NullPointerException。
- 在 finally 块中关闭资源(数据库连接、查询、流处理),或使用 try-with-resources。
- 恰当使用异常:在知道该如何处理的情况下才捕获异常;让类库和程序更安全(既是为调试做短期投资,也是为程序健壮性做长期投资)。
- 不要在 for 循环内用异常控制流程:利用异常控制代码流程远比条件语句(if/else、switch)低效,因为异常实例的构造需要对栈做快照。
结语
从 bug/00.常见的异常.md 的速查清单出发,本文将其中的 25 种常见异常与 22 种常见错误逐一展开,并串联了仓库中 Throwable异常体系、异常处理的流程机制、JVM处理异常解析、finally处理逻辑介绍、异常try-catch性能 等系列笔记,以及 崩溃bug日志总结1、崩溃bug日志总结2、崩溃bug日志总结3 中的真实案例。当你在日志中看到某个异常类名时,可按本文分类定位其归属(异常还是错误、检查还是非检查、触发场景是什么),再结合堆栈与异常信息快速锁定代码位置;当编写异常处理代码时,则可对照最佳实践检查自己的 try-catch 是否得当。异常机制的价值在于把"正常做的事儿"与"出现问题怎么办"完全隔离开来,让代码读写更加井井有条——前提是你真正理解它。
- 教程
- 技术博客
- 文档
🔥【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
相关推荐
openJiuwen DeepSearch 错误码与异常体系深度解析:StatusCode 与 Custom*Exception 的工程实践
openJiuwen DeepSearch 错误码与异常体系深度解析:StatusCode 与 Custom Exception 的工程实践 openJiuwe
人工智能大模型AI AgentRAG深度研究搜索引擎后端代码智能体LanguageExt 错误处理核心:深入解析 `Error` 抽象记录类型与 Expected / Exceptional / ManyErrors 错误分类体系
LanguageExt 错误处理核心:深入解析 Error 抽象记录类型与 Expected / Exceptional / ManyErrors 错误分类体系
后端Sass JS API 异常体系深度解析:Exception 类的设计与实践指南
Sass JS API 异常体系深度解析:Exception 类的设计与实践指南 本篇技术指南以 Sass 官方规范仓库(gh_mirrors/sa/sass)
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考