目录
一. 什么是异常?
二. 异常家族体系介绍
2.1 Error
2.2 Exception
2.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 Error
(1)Error 是一个类,在java.lang 包下,它继承了 Throwable 类,是 Throwable 类的子类;
(2)Error 通常代表系统级别错误,属于严重问题;
(3)系统一旦出现问题,sun公司会把这些错误封装成 Error 对象;
(4)Error 是 sun 公司自己用的,不是给我们程序员用的,因此对于我们开发人员来讲,不需要特别关注。
2.2 Exception
2.2.1 Exception 详情
(1)Exception 是一个异常类,也在 java.lang 包下,它和 Error 一样都继承了 Throwable 类,是 Throwable 类的子类;
(2)Exception 叫做异常,代表程序中可能出现的问题,是异常体系中的最顶层父类,所有的异常类都继承了 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" + "c";javac 在编写为字节码文件的时候,会自动将 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... 捕获处理,而不会继续向上抛。