目录
1. 异常处理的三种方法
1.1 JVM 默认处理异常
1.2 通过 try...catch...自己处理异常
1.3 使用 throws和throw 抛出异常
1.3.1 使用 throws 抛出异常
1.3.2 使用 throw 抛出异常
2. try...catch.. 捕获到异常之后代码的执行顺序?
3. try...catch... 相关的四个面试题
3.1 如果 try...catch... 中没有出现异常,程序如何执行?
3.2 如果 try...catch... 中出现多个异常,程序会怎么执行?
3.3 如果 try... catch... 中遇到的异常没有被捕获,该怎么执行?
3.4 如果 try 中遇到了问题,那么下面的其他代码还会执行吗?
4. 什么时候使用 try...catch... ?什么时候使用 throws或 throw?
1. 异常处理的三种方法
上篇重点讲解了 Java 异常体系家族的分类,分析了运行时异常和编译时异常的由来,又举了几个例子,那么本片着重讲解 Java 异常的三种处理方式以及它们的使用细节。
在Java中,对于程序出现的异常,我们有以下三种解决方案
1.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 数组越界异常";
还有一些情况,可能会出现多个异常,这种情况下,我们应该从下往上读,这里我就不展示了。
1.2 通过 try...catch...自己处理异常
如果我们不采用JVM默认的异常处理机制,我们就可以自己手动添加对异常的处理,而try...catch...就是方法之一,使用格式如下图所示
在 try 语句块中,我们需要将可能出现异常的代码写在括号内,然后在 catch 语句块中,我们需要写对于异常出现时的处理逻辑。
如果我们使用 try...catch... 语句块处理异常,那么当程序出现异常时,程序不会立即停止,而是继续向下将代码执行完,我们仍然拿刚才的的代码举例,选中刚才出现异常的哪一行代码,我们按快捷键 Ctrl + Alt + t,就会出现像屏幕中这样的提示,我们点击 try/catch 环绕,当然也可以自己手动敲,都可以,懒得写的同学可以记住这个快捷键,挺方便的
环绕完成之后,就会变成下图这样的结果
我们再次运行 main 方法,在控制台可以发现,刚才采用JVM默认处理机制没有运行的这行代码在加入 try...catch.. 语句块之后也成功运行了。
catch 语句块中的额 e.pritStackTrace 下面会说。
try...catch..语句快的好处就是,当程序出现异常时,不会马上停止,而是会继续向下执行将程序运行完毕。
1.3 使用 throws和throw 抛出异常
上面我们可以使用 try..catch...包裹代码的方式捕获异常,这里我们还可以使用关键字 throws或者throw 抛出异常
1.3.1 使用 throws 抛出异常
throws 关键字你是在写在方法定义处,表明声明一个异常,也可以写多个异常,使用格式如下所示
当我们定义一个方法时,如果方法中出现异常,但是又不想使用 try...catch... 进行捕获,我们此时就可以使用 throws,在方法声明处将该方法中可能出现的异常类声明并抛出。
throws 关键字在抛出异常时还有一个点需要注意,如果说出现的异常是编译时异常,必须抛出,如果是运行时异常,也可以选择不抛出。
例子如下,我们定义一个文件输入流对象,这里IDEA爆红了,提示我们有异常需要处理
因为这里有可能我们定义的文件不存在,我们就可以使用 throws 将异常抛出,直接写在方法上,如下所示,我们还可以写多个,中间用 "," 隔开
1.3.2 使用 throw 抛出异常
throw 关键字则使用在方法的内部,可以用来结束方法,它可以手动抛出异常对象,交给调用者,一旦抛出异常对象,那么下方的代码就不会再继续执行了,使用方法如下图所示
代码如下
public static void main(String[] args) {
// 定义一个空数组
int[] arr = null;
// 先对数组做一个判空操作,如果为空抛出异常
if (arr == null){
// 数组为空,抛出异常
throw new NullPointerException("空指针异常");
}
// 因为 throw 抛出异常,所以下方的打印语句不会执行
System.out.println("这句话根本不会执行");
}
运行上方代码,可以在控制台得到如下结果
可以看到,我们定义的"空指针异常"输出,说明该异常被抛出来了,而且下方的输出语句也没有执行。说明 throw 抛出异常时会直接结束方法的运行。
2. 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 写成ArrayIndexOutOfBoundsException 也没有问题,其实不只是可以写成这两种,只要是产生的异常类的父类,都可以写在括号内接收子类异常对象。
3. try...catch... 相关的四个面试题
3.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 语句块中的代码。
3.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 语句块中异常类有父子继承关系,那么父类要写在子类的下边,不能写在子类的上边。看如下我演示的代码
可以看到,当我们定义了一个顶级父类异常Exception 之后,IDEA提示我们后面两个异常有错误,点过去 按Alt + Enter键,IDEA提示我们删除此异常,或者将定义的这个数组越界异常放在 Exception 异常之前。
原因是什么呢?
其实很简单,当我们程序出现问题之后,进入catch语句块,它还是会从上向下执行 catch 语句块,你现在把父类定义在了子类的前面,父类就可以把异常接受,那么子类存在的意义何在呢?因此要记住,当程序出现多个异常时,catch 语句块中父类异常不能在子类异常的前面,必须放在子类异常的后面。
由此我们可以得到以下结论:
如果 try 语句块中出现了多个问题,那么在出现第一个异常的时候,try 语句块中的代码就不会再继续向下执行了,程序会直接跳到 catch 语句块中,判断出现的异常与哪个异常对象相匹配,就会执行 catch 语句块中的内容,如果没有与之相匹配的异常对象,程序会报错。当程序出现多个异常时,catch 语句块中父类异常不能在子类异常的前面,必须放在子类异常的后面。
3.3 如果 try... catch... 中遇到的异常没有被捕获,该怎么执行?
接着刚才的例子,如果我 catch 语句块修改一下,如下代码,程序首先会出现数组越界异常,但在下方 catch 语句中我没有对出现的异常进行捕获,再次运行代码,在控制台得到如下结果
可以看到,程序报错了,但还是只报了数组越界异常,我们定义的算术运算异常并没有显示出来,没有执行算术运算一场语句块中的 sout 输出语句。
由此得出结论,当我们程序爆出的异常没有被 catch 语句块捕获时,会采用JVM默认的异常捕获机制捕获,也就是说,异常没有被捕获,tyr...catch... 代码块无效,白写了。
3.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虚拟机去处理。
4. 什么时候使用 try...catch... ?什么时候使用 throws或 throw?
如果我们想要知道什么时候应该使用捕获,什么时候应该使用抛出,就要知道它们两个的核心思想。
捕获异常:让程序不要停止,一直运行下去;
抛出异常:告诉调用者出错了;
其实通过上面的讲解,我们也大概明白了,捕获异常是将遇到的异常自己解决,而抛出异常是将遇到的异常向上抛出,抛给该方法的调用者。
可以举个简短的例子,当你在工作的过程中遇到了很麻烦的一个问题,你既可以自己解决它,也可以向你的上级汇报,自己解决就好比是我们在代码中使用 try...catch...,汇报给上级就好比是throws与throw,上级可以继续throws给上级的上级,但是这个问题也不能一直向上抛吧!你见过主管会给老板汇报说工作遇到了大难题解决不了的吗?
这么说,各位应该明白了吧。
在开发过程中,当我的一个方法调用另一个方法时,如果被调用的方法可能出现异常,它就可以将异常抛给调用者,让调用者知并使用 try...catch... 将异常捕获,对异常加以处理,但如果是在第一调用者或者 main 方法中,或者我们想要执行完不中断的业务中,通常或进行 try...catch... 捕获处理,而不会继续向上抛。