目录
1.异常的背景
1.1初识异常
我们平常接触到的异常:
1)除以0
System.out.println(10/0);
结果为:Exception in thread "main" java.lang.ArithmeticException: / by zero
2)数组下标越界
int [] arr={1,2,3};
System.out.println(arr[100]);
结果为:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
3)访问null对象
public class Test {
public int num = 10;
public static void main(String[] args) {
Test t = null;
System.out.println(t.num); } }
结果为: Exception in thread "main" java.lang.NullPointerException
所谓异常就是指程序在运行时出现错误时通知调用者的一种机制
关键字运行时指:
有些错误是这样的,例如将System.out.println拼写错了,此时编译过程中就会出错,这是“编译期”错误。
而运行时是指程序已经编译通过得到class文件了,再由JVM执行过程中出现的错误。
异常是有很多的,不同种类的异常具有不同的意义也有不同的处理方式。
1.2 防御式编程
错误在代码中是客观存在的,因此我们要让程序出现问题及时通知程序员,主要有两种主要方式:
- LBYL:(Look Before You Leap )在操作之前就做充分的检查
- EAFP:It's Easier to Ask Forgiveness than Permission. “事后获取原谅比事前获取许可更容易”,也就是先操作,遇到问题再处理
异常的核心思想就是EAFP
1.3 异常的好处
使用LRYL,正常流程和错误处理代码混在一起,代码整体显得比较混乱,二使用EAFP,正常流程和错误流程是分离开的,更容易理解代码。
2.异常的基本用法
2.1 捕获异常
基本语法:
try{
有可能出现异常的语句;
}[catch (异常类型 异常对象){
}……]
[finally{
异常的出口
}]
- try代码中存放的是可能出现异常的代码
- catch代码块中存放的是出现异常后的处理行为
- finally代码中的代码用于处理善后工作,会在最后执行
- 其中catch和finally都可以根据情况选择加或者不加
代码1:不处理异常
int [] arr={1,2,3};
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
结果为:
我们一旦发现异常,程序就终止了,after没有正确输出
代码2:使用try catch 后的程序执行过程
int [] arr={1,2,3};
try{
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
}catch(ArrayIndexOutOfBoundsException e){
//打印出现异常的调用栈
e.printStackTrace();
}
System.out.println("after try catch");
结果为:
发现:一旦try中出现异常,那么try代码块中的程序就不会继续执行,而是交给catch中的代码来执行,catch执行完毕会继续往下执行。
关于异常的处理方式:
异常的种类有很多,我们要根据不同的业务场景来决定。
- 对于比较严重的问题(例如和钱相关的场景),应该直接让程序崩溃,防止造成更严重的后果。
- 对于不太严重的问(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序员
- 对于可能会恢复的问题(和网络相关的场景),可以尝试进行重试
在我们当前的代码中采取的是经过简化的第二种方式,我们记录的错误日志时出现异常的方法调用信息,能很快速的让我们找到出现异常的位置,以后我们会采取更完备的方式来记录异常信息。
关于“调用栈”:
方法之间是存在相互调用关系的,这种调用关系我们可以用“调用栈”来描述,在JVM中有一块内存空间称为“虚拟机栈”专门存储方法之间的调用关系,当代码中出现异常的时候,我们就可以使用e.printStackTrace();方式查看出现异常代码的调用栈。
代码3:catch只能处理对应种类的异常
int [] arr={1,2,3};
try{
System.out.println("before");
arr=null;
System.out.println(arr[100]);
System.out.println("after");
}catch(ArrayIndexOutOfBoundsException e){
//打印出现异常的调用栈
e.printStackTrace();
}
System.out.println("after try catch");
结果为:
此时,catch语句不能捕获到刚才的空指针异常,因为异常类型不匹配
代码4:catch可以有多个
int [] arr={1,2,3};
try{
System.out.println("before");
arr=null;
System.out.println(arr[100]);
System.out.println("after");
}catch(ArrayIndexOutOfBoundsException e){
//打印出现异常的调用栈
System.out.println("数组下标越界异常");
e.printStackTrace();
}catch (NullPointerException e){
System.out.println("空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
结果为:
一段代码可能会抛出多种不同的异常,不同的异常有着不同的处理方式,因此可以搭配多个代码块,如果多个异常的处理方式是完全相同的,也可以写成:
catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
……
}
Exception类是所有异常类的父类,因此可用这个类型表示捕获所有异常(但是不建议使用)
备注:catch进行类型匹配的时候,不光会匹配相同的异常对象,也会捕捉目标异常类型的子类对象
代码5:finally表示最后的善后作用,例如释放资源
代码可见上面所示代码
注:无论是否存在异常,finally中的代码一定都会执行,保证最终一定会执行的Scanner的close方法。
代码6:使用try负责回收资源
刚才的代码可以有一种等价写法,将Scanner对象在try的()中创建,就能保证在try执行完毕后自动调用Scanner的close方法。
try(Scanner sc=new Scanner(System.in)){
int num=sc.nextInt();
System.out.println("num="+num);
}catch(Exception e){
e.printStackTrace();
}
2.2 异常处理流程
程序先执行try中的代码;
如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配;
如果找到匹配的异常类型,就会执行catch中的代码;
如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者;
无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行);
如果上层调用者也没有处理的了异常,就继续向上传递;
一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止。
2.3 抛出异常
除了Java内置的类会抛出异常一些异常之外,也可以手动抛出某个异常,使用throw关键字完成这个操作。
public static void main(String[] args) {
System.out.println(divide(10, 0));
}
public static int divide(int x, int y) {
if (y == 0) {
throw new ArithmeticException("抛出除 0 异常");
}
return x / y;
}
结果为:
在这个代码中,我们可以根据实际情况来抛出需要的异常,在构造异常对象同时可以指定一些描述信息。
2.4 异常说明
我们在处理异常的时候,通常希望知道这段代码中究竟会出现那些可能的一次异常。
我们可以使用throws关键字,把可能抛出的异常显示的标注在方法定义的位置,从而提醒调用者要注意捕获这些异常。
public static int divide(int x,int y)throws ArithmeticException{
if(y==0){
throw new ArithmeticException("抛出除0异常");
}
return x/y;
}
2.5 关于finally的注意事项
finally中的代码保证一定会执行到,但这也会带来一些麻烦。
public static void main(String[] args) {
System.out.println(func());
}
public static int func(){
try{
return 10;
}finally {
return 20;
}
}
注意:
finally执行的时机是在方法返回之前(try或者catch中如果有return会在这个return之前执行finally),但是如果finally中也存在return语句,那么就会执行finally中的return,从而不会执行到try中原有的return。
一般我们不建议在finally中写return(被编译器当做一个警告)
3.Java异常体系
Java内置了丰富的异常体系,用来表示不同情况的异常
下图表示Java内置的异常类之间的继承关系:
- 顶层类 Throwable派生出两个重要的子类,Error和Exception
- 其中Error指的是Java运行时内部错误和资源耗尽错误,应用程序不抛出此类异常,这种内部错误一旦出现,除了告知用户并使用程序终止之外,再无能无力,这种情况很少出现。
- Exception是我们所使用的异常类的父类
- 其中Exception有一个子类称为RuntimeException。这里又派生出很多我们常见的异常类
Java语言规范将派生出Error类或RuntimeException类的所有异常称为非受查异常,所有的其他异常称为受查异常。
如果一段代码可能抛出受查异常,那么必须显示进行处理
public static void main(String[] args) {
System.out.println(readFile());
}
public static String readFile() {
// 尝试打开文件, 并读其中的一行.
File file = new File("d:/test.txt");
// 使用文件对象构造 Scanner 对象.
Scanner sc = new Scanner(file);
return sc.nextLine();
}
// 编译出错
Error:(13, 22) java: 未报告的异常错误java.io.FileNotFoundException; 必须对其进行捕获或声明以便 抛出
查看Scanner的构造方法可以发现,存在FileNoFoundException这样的异常说明。
如FileNoFoundException这样的异常就是受查异常,如果不显示处理,编译无法通过
显示处理的方式有两种:
方法1:使用try catch 包裹起来
public static void main(String[] args) {
System.out.println(readFile());
}
public static String readFile() {
File file = new File("d:/test.txt");
Scanner sc = null;
try {
sc = new Scanner(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return sc.nextLine();
}
方法2:在方法上加上异常说明,相当于将处理动作交给上级调用者
public static void main(String[] args) {
try {
System.out.println(readFile());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public static String readFile() throws FileNotFoundException {
File file = new File("d:/test.txt");
Scanner sc = new Scanner(file);
return sc.nextLine();
}
小提示:使用IDEA中的alt+enter能够快速修正代码
4.自定义异常类
Java中虽然已经内置了丰富的异常类,但是我们实际场景中可能还有一些情况需要我们对异常类进行扩展,创建符合我们实际情况的异常。
注意事项:
- 自定义异常通常继承自Exception或者RuntimeException
- 继承自Exception的异常默认是受查异常
- 继承自RuntimeException的异常默认是非受查异常。