Java 异常体系全面总结详解

发布时间:2026/7/14 13:26:46
Java 异常体系全面总结详解 目录一. 什么是异常二. 异常家族体系介绍2.1 Error2.2 Exception2.2.1 Exception 详情2.2.2 运行时异常2.2.3 编译时异常2.2.4 Exception 分类总结三. 从类加载的全过程深入理解编译时异常与运行时异常3.1 类加载的全过程3.2 什么是编译时异常3.3 什么是运行时异常3.4 Java问什么要分为编译时异常与运行时异常四. 编译时异常代码展示4.1 日期格式转换异常4.2 IO流文件找不到异常五. 运行时异常代码展示5.1 除 0 异常5.2 数组越界异常六. 异常处理的三种方法6.1 JVM 默认处理异常6.2 通过 try...catch...自己处理异常6.3 使用 throws 和 throw 抛出异常6.3.1 使用 throws 抛出异常6.3.2 使用 throw 抛出异常七. try...catch.. 捕获到异常之后代码的执行顺序八. try...catch... 相关的四个面试题8.1 如果 try...catch... 中没有出现异常程序如何执行8.2 如果 try...catch... 中出现多个异常程序会怎么执行8.3 如果 try... catch... 中遇到的异常没有被捕获该怎么执行8.4 如果 try 中遇到了问题那么下面的其他代码还会执行吗九. 什么时候使用 try...catch... ?什么时候使用 throws或 throw一. 什么是异常异常其实就是代表程序出现的问题我们知道在编写代码时偶尔会编出一些 bug那么这些 bug它们有一些可能是异常有一些可能不是异常。这里有一个误区需要各位注意学习异常不是为了让我们以后在写代码的过程中不出现异常但其实也很难避免不出现异常学习异常是为了让我们知道在程序出现异常之后该怎么去处理它。二. 异常家族体系介绍我们知道在Java中有各种各样的异常它们其实是一个大的家族而这个家族的首领就是 java.lung.Throwable。而 Throwable 又分为俩大家族一个是Error(错误)一个是Exception(异常)。如下图中关系所示2.1 Error1Error 是一个类在java.lang 包下它继承了 Throwable 类是 Throwable 类的子类2Error 通常代表系统级别错误属于严重问题3系统一旦出现问题sun公司会把这些错误封装成 Error 对象4Error 是 sun 公司自己用的不是给我们程序员用的因此对于我们开发人员来讲不需要特别关注。2.2 Exception2.2.1 Exception 详情1Exception 是一个异常类也在 java.lang 包下它和 Error 一样都继承了 Throwable 类是 Throwable 类的子类2Exception 叫做异常代表程序中可能出现的问题是异常体系中的最顶层父类所有的异常类都继承了 Exception 异常类3我们通常会用 Exception 以及它的子类来封装程序出现的问题。异常又可以分为运行时异常与编译时异常。2.2.2 运行时异常RuntimeException 及其子类编译阶段不会出现异常提醒。是在程序运行的时侯才会出现的异常一般是由于参数传递出现的异常如数组索引越界异常。2.2.3 编译时异常编译时异常没有继承 RuntimeException 直接继承了 Exception 在我们编写代码时就会出现的异常在编写代码时必须手动处理解决作用在于提醒程序员通常情况下IDEA编辑器会给出我们相应的的异常处理提示我们可以采取对应的方案去解决(如日期解析异常)。2.2.4 Exception 分类总结对于我们程序员来说需要着重掌握 Exception 这一家族体系中的各成员。三. 从类加载的全过程深入理解编译时异常与运行时异常3.1 类加载的全过程我们都知道一个Java类从编写到运行是需要经历一个过程的如下图所示我们编写一个 Java 代码文件想要运行它并得到结果是需要以下的一个过程的。首先1我们编写Java文件其次2编译阶段编写完成之后执行 Javac 命令将编写的 Java 文件编译成字节码文件再次3运行阶段运行我们编译完成得到的字节码文件最终4程序运行完毕输出结果。3.2 什么是编译时异常通过我对上面类加载的过程叙述想必大家此时对编译时一场应该有一个模糊的了解到了吧我们知道在编写代码时我们编写的其实是 Java 文件在编译完成之后我们会通过javac命令使 Java 文件编译成字节码文件如下图所示在编译阶段我们的系统会检查你所编写的代码有没有语法层面的错误但是不会检查你的代码有没有逻辑层面的错误这样说各位应该明白了吧因为此时我们写的代码还没有运行呢就所有逻辑错误系统在没有运行之前也是件查不到的此外我们的 javac命令在编写为字节码文件时不仅会做语法检查还会对我们的代码做一个优化处理。例如我在写代码时定义一个字符串 String str a b cjavac 在编写为字节码文件的时候会自动将 str 做优化实际编写为字节码文件之后是 String str abc还有就是泛型我们所写的泛型其实在 Java 中属于伪泛型它只有在编写代码时我们可以看得到在javac编译成字节码文件之后泛型就消失不见了我们想一想你在给一个List容器添加了泛型之后假设你添加的泛型为Integer若你再添加 String 类型的数据各位想想是不是会报错这种通过泛型约束添加数据类型也属于编译时异常需要在代码编写阶段就提前处理好的。3.3 什么是运行时异常刚才我们详细的了解了什么是编译时异常也那么现在我们再来看运行时异常可以说是非常简单了吧编译时异常是对我们代码语法层面的检查运行时异常就是我们程序运行之后才出现的逻辑异常执行编译完成得到的字节码文件再运行得出结果如果在编译时检查除了一部分语法异常那么逻辑异常就会在运行阶段给我们展示出来。3.4 Java问什么要分为编译时异常与运行时异常不知道各位有没有想过Java问什么要分为编译时异常与运行时异常直接把他们归为一类不好吗其实通过上方的讲解各位应该也知道一个大概了因为在编译阶段我们的代码还尚未运行即便是有逻辑错误但程序在运行之前也无法检查出来这个阶段其实更多的是在提醒我们程序员检查本地语法是否有误。而且在编译阶段不仅是在检查语法是否有误同时也做了代码优化这一步骤将一些错误和对程序性能的优化提前到了程序运行之前提高了程序的执行效率同时也减少了出错率。而运行时异常只有程序运行之后我们才能知道它的真正原因就是因为代码出错而导致的异常与语法无关如果发生这种异常我们就可以根据它的提示归根究源迅速找到问题所在从而解决程序出现的问题在某方面也提高了程序的健壮性。下面我简单给各位列举几个异常各位就会对异常有更加深入的理解了。四. 编译时异常代码展示4.1 日期格式转换异常我编写了一段代码做了一个字符串日期格式转换的操作public class TestException { public static void main(String[] args) { // 演示编译时异常 // 定义一个日期字符串 String time 2023年8月3日; // 创建一个 SimpleFormatter 的对象 SimpleDateFormat sdf new SimpleDateFormat(yyyy年MM月dd日); // 传入 time 字符串转化 Data data sdf.parse(time); // 输出 data System.out.println(data); } }但在IDEA编辑器中它提醒我在进行 parse 操作时可能会出现异常我们按键盘上的 alt Enter 键IDEA就可以给出我们相应的解决方案这种异常就属于编译时异常是可以在编写代码时解决的异常4.2 IO流文件找不到异常定义一个 FileInputStream 的对象但这里它却报了一个异常我们鼠标点过去 Alt Enter它会让我们对异常做处理。那么这里会有什么异常发生呢我们的代码明明写的没有错为啥什么会出现异常呢这种异常就属于是编译时异常一般情况下我们本身编写没有问题而是语法检查时可能会出现的问题像上面的IO流我们本身创建对象没有问题但是程序不知道你填写的路径是否有这个文件的存在因此才会爆出异常。五. 运行时异常代码展示5.1 除 0 异常如下代码我们定义一个变量a 10一个变量b 0让变量 c a/b再打印出c的值。其实我们可以看出每一行代码在语法层面都没有问题但是0不能作为除数着我们是知道的因此在编译阶段这种错误不会被提示出来只有在代码运行之后陈程序才知道有这样一段逻辑运算这种异常就属于运行时异常。5.2 数组越界异常如下所示我定义一个 int 数组长度为3但我却给它赋四个值它就会在运行的时候爆出数组越界异常(ArrayIndexOutOfBoundsException)。可以看到我们的每一行代码在语法层面上其实都没有错误吧只是一个简单的赋值操作但是我们的程序在运行之前根本就不知道数组没有第四个位置只有才运行之后数组出现在内存时它才知道也就是说必须在运行时才能检验出来的异常就是运行时异常。六. 异常处理的三种方法6.1 JVM 默认处理异常如果我们对异常不做任何处理的话Java底层会使用JVM虚拟机默认的异常处理方式当程序出现异常时JVM虚拟机会把异常的名称异常的信息以及异常出现的位置输出在控制台此外一旦程序出现异常JVM虚拟机就会立即停止程序出现异常的代码下方的代码都不会执行。如下代码我简单写一个数组越界异常使用JVM默认的异常处理方式来看看它会怎么做public static void main(String[] args) { int[] arr new int[3]; arr[0] 0; arr[1] 1; arr[2] 2; arr[3] 3; System.out.println(看看我会执行吗); }运行上述代码可以在控制台得到如下结果​我们可以看到System.out.println(看看我会执行吗)这一行代码并没有打印出来在控制台中红色语句便是异常的描述Exception in thread main 表示异常出现在在线程 main 中也就是我们所编写的 main 方法java.lang.ArrayIndexOutOfBoundsException 表示我们出现的异常的名称翻译过来就是数组越索引界异常at 是英语单词意思是在cn.itcast.user.pojo.Test01.main 表示的是我们项目中的 包名 类名 方法名括号内蓝色字体 Test01.java:12 表示异常出现在Test01.java 类中的第十二行把整个异常信息综合起来意思就是说包名为 cn.itcast.user.pojo 类名为 Test01.java 的 main 方法的第十二行出现了 ArrayIndexOutOfBoundsException 数组索引越界异常还有一些情况可能会出现多个异常这种情况下我们应该从下往上读这里我就不展示了。6.2 通过 try...catch...自己处理异常如果我们不采用 JVM 默认的异常处理机制我们就可以自己手动添加对异常的处理而 try...catch... 就是方法之一使用格式如下图所示​在 try 语句块中我们需要将可能出现异常的代码写在括号内然后在 catch 语句块中我们需要写对于异常出现时的处理逻辑。如果我们使用 try...catch... 语句块处理异常那么当程序出现异常时程序不会立即停止而是继续向下将代码执行完我们仍然拿刚才的的代码举例选中刚才出现异常的哪一行代码我们按快捷键 Ctrl Alt t就会出现像屏幕中这样的提示我们点击 try/catch 环绕当然也可以自己手动敲都可以懒得写的同学可以记住这个快捷键挺方便的​环绕完成之后就会变成下图这样的结果​我们再次运行 main 方法在控制台可以发现刚才采用JVM默认处理机制没有运行的这行代码在加入 try...catch.. 语句块之后也成功运行了。​catch 语句块中的额 e.pritStackTrace 这行代码的意思在文章下面会说到这里暂时先不管。try...catch.. 语句块的好处就是当程序出现异常时它不会让程序马上停止而是会继续向下执行将程序运行完毕。6.3 使用 throws 和 throw 抛出异常上面我们说到了自行处理异常采用可以使用 try..catch...包裹代码的方式下面还有另一种方式就是采用 throws 和 throw 关键字捕获异常这里我们还可以使用关键字 throws 或者 throw 第三种解决异常的方式即抛出异常。6.3.1 使用 throws 抛出异常throws 关键字你是在写在方法定义处表明声明一个异常也可以写多个异常使用格式如下所示​当我们定义一个方法时如果方法中出现异常但是又不想使用 try...catch... 进行捕获我们此时就可以使用 throws在方法声明处将该方法中可能出现的异常类声明并抛出。throws 关键字在抛出异常时还有一个点需要注意如果说出现的异常是编译时异常必须抛出如果是运行时异常也可以选择不抛出。例子如下我们定义一个文件输入流对象这里IDEA爆红了提示我们有异常需要处理​因为这里有可能我们定义的文件不存在我们就可以使用 throws 将异常抛出直接写在方法上如下所示我们还可以写多个异常类类与类之间中间用 隔开。​6.3.2 使用 throw 抛出异常throw 关键字则使用在方法的内部可以用来结束方法它可以手动抛出异常对象交给调用者一旦抛出异常对象那么下方的代码就不会再继续执行了使用方法如下图所示​代码如下public static void main(String[] args) { // 定义一个空数组 int[] arr null; // 先对数组做一个判空操作如果为空抛出异常 if (arr null){ // 数组为空抛出异常 throw new NullPointerException(空指针异常); } // 因为 throw 抛出异常所以下方的打印语句不会执行 System.out.println(这句话根本不会执行); }运行上方代码可以在控制台得到如下结果​可以看到我们定义的 空指针异常 输出说明该异常被抛出来了而且下方的输出语句也没有执行。说明 throw 抛出异常时会直接结束方法的运行。七. try...catch.. 捕获到异常之后代码的执行顺序当我们使用 try...catch... 捕获异常之后其语句块内代码是有一定的执行顺序的如下代码public static void main(String[] args) { int[] arr new int[3]; arr[0] 0; arr[1] 1; arr[2] 2; // 第一步开始进入 try...catch...语句块 try { // 第二步执行此行代码发现此处代码有异常底层会创建出一个此异常的对象也就是 // ArrayIndexOutOfBoundsException 数组越界的对象 arr[3] 3; // 第三步将刚才创建的对象与 catch 括号内的对象中做比较看看能否接收 } catch (Exception e) { // 第四步若接收成功就会执行 catch 语句块内部的这一行代码 // 若接受不成功不会执行这一行代码程序直接跳出catch 语句块并不再向下执行程序停止 e.printStackTrace(); } System.out.println(看看我会执行吗); }使用 try...catch... 捕获异常我们通常会对异常做处理上述代码中括号内创建了 Exception 的对象 e这里是可以的因为 Exception 是所有异常类的父类当出现异常产生异常对象时底层就会出现Exception e new 异常对象Exception父类引用指向 Exception 子类对象这是一种多态的写法各位应该能看得懂当然如果你在编写代码的时候就能确定代码会出现哪种类型的异常也可以直接精确的写成 Exception 的子类。例如上面的例子我确切知道它会产生数组越界异常所以直接将 Exception 写成ArrayIndexOutOfBoundsException 也没有问题其实不只是可以写成这两种只要是产生的异常类的父类都可以写在括号内接收子类异常对象。八. try...catch... 相关的四个面试题8.1 如果 try...catch... 中没有出现异常程序如何执行我们来验证一下如下代码public static void main(String[] args) { // 第一步开始进入 try...catch...语句块 try { int[] arr new int[3]; arr[0] 0; arr[1] 1; arr[2] 2; // 直接定义父类 Exception 接受可能出现异常的类对象 } catch (Exception e) { // 自定义异常处理逻辑打印一句话 System.out.println(索引越界异常); } System.out.println(看看我会执行吗); }代码本身没有任何问题我们直接运行在控制台得到如下结果​可以看到程序正常运行没有任何问题我们也可以就此得到结论如果我们的 try...catch... 语句块中没有任何异常出现程序会把 try 语句块中的代码全部执行完毕不会执行 catch 语句块中的代码。8.2 如果 try...catch... 中出现多个异常程序会怎么执行如下代码我定义两个异常一个索引越界异常一个算术运算异常在 catch 中 我们也定义与之精准对应的两个 catch 异常类运行代码看啊可能会有什么结果public static void main(String[] args) { // 第一步开始进入 try...catch...语句块 try { int[] arr new int[3]; arr[0] 0; arr[1] 1; arr[2] 2; // 制造一个索引越界异常 arr[3] 3; // 制造一个 除0 异常 int a 10/0; // 定义数组越界异常的catch语句块处理方案 } catch (ArrayIndexOutOfBoundsException e) { // 自定义数组越界异常处理逻辑 System.out.println(索引越界异常); // 定义算术运算异常的catch语句块处理方案 }catch (ArithmeticException e){ // 自定义除0异常处理逻辑 System.out.println(算术运算异常); } System.out.println(看看我会执行吗); }运行此方法我们在控制台得到如下结果可以看到控制台纸打印了索引越界异常说明算术运算异常的 catch 语句块根本就没有执行。那么我们可以得出结论如果 catch 语句块中有多个异常那么在接收返回第一个异常之后其余的异常不会接收。​此外有一点需要做补充如果有多个异常可能出现并且我们定义的 catch 语句块中异常类有父子继承关系那么父类要写在子类的下边不能写在子类的上边。看下面我演示的代码可以看到当我们定义了一个顶级父类异常Exception 之后IDEA提示我们后面两个异常有错误​点过去 按Alt Enter键IDEA提示我们删除此异常或者将定义的这个数组越界异常放在 Exception 异常之前。​原因是什么呢其实很简单当我们程序出现问题之后进入 catch 语句块它还是会从上向下执行 catch 语句块你现在把父类定义在了子类的前面父类就可以把异常接受那么子类存在的意义何在呢因此要记住当程序出现多个异常时catch 语句块中父类异常不能在子类异常的前面必须放在子类异常的后面。由此我们可以得到以下结论如果 try 语句块中出现了多个问题那么在出现第一个异常的时候try 语句块中的代码就不会再继续向下执行了程序会直接跳到 catch 语句块中判断出现的异常与哪个异常对象相匹配就会执行 catch 语句块中的内容如果没有与之相匹配的异常对象程序会报错。当程序出现多个异常时catch 语句块中父类异常不能在子类异常的前面必须放在子类异常的后面。8.3 如果 try... catch... 中遇到的异常没有被捕获该怎么执行接着刚才的例子如果我 catch 语句块修改一下如下代码程序首先会出现数组越界异常但在下方 catch 语句中我没有对出现的异常进行捕获再次运行代码在控制台得到如下结果​可以看到程序报错了但还是只报了数组越界异常我们定义的算术运算异常并没有显示出来没有执行算术运算一场语句块中的 sout 输出语句。由此得出结论当我们程序爆出的异常没有被 catch 语句块捕获时会采用JVM默认的异常捕获机制捕获也就是说异常没有被捕获tyr...catch... 代码块无效白写了。8.4 如果 try 中遇到了问题那么下面的其他代码还会执行吗如下代码我在出现异常的语句下方打印一句话看看是否会执行public static void main(String[] args) { // 第一步开始进入 try...catch...语句块 try { int[] arr new int[3]; arr[0] 0; arr[1] 1; arr[2] 2; // 制造一个索引越界异常 arr[3] 3; // 打印一句话 System.out.println(这句话根本不会执行); // 定义数组越界异常的catch语句块处理方案 } catch (ArrayIndexOutOfBoundsException e) { // 自定义数组越界异常处理逻辑 System.out.println(索引越界异常); } }运行代码得到如下结果​可以发现我们定义在异常下方的输出语句根本没有执行也证实了我们的结论如果 try 语句中出现了问题出现问题的代码下方的代码根本不会执行它会直接进入到 catch 语句块中如果catch 没有捕获该异常还是会交给 JVM虚拟机去处理。九. 什么时候使用 try...catch... ?什么时候使用 throws或 throw如果我们想要知道什么时候应该使用捕获什么时候应该使用抛出就要知道它们两个的核心思想。捕获异常让程序不要停止一直运行下去抛出异常告诉调用者出错了其实通过上面的讲解我们也大概明白了捕获异常是将遇到的异常自己解决而抛出异常是将遇到的异常向上抛出抛给该方法的调用者。可以举个简短的例子当你在工作的过程中遇到了很麻烦的一个问题你既可以自己解决它也可以向你的上级汇报自己解决就好比是我们在代码中使用 try...catch...汇报给上级就好比是throws与throw上级可以继续throws给上级的上级但是这个问题也不能一直向上抛吧你见过主管会给老板汇报说工作遇到了大难题解决不了的吗这么说各位应该明白了吧。在开发过程中当我的一个方法调用另一个方法时如果被调用的方法可能出现异常它就可以将异常抛给调用者让调用者知并使用 try...catch... 将异常捕获对异常加以处理但如果是在第一调用者或者 main 方法中或者我们想要执行完不中断的业务中通常或进行 try...catch... 捕获处理而不会继续向上抛。