目录
1、初步认识异常
1、算数异常
2、空指针异常
3、数组越界异常
2、异常的结构体系
3、异常的分类
1、编译时异常/受查异常
2、运行时异常/非受查异常
4、异常的处理
1、处理异常的编程方式(防御式编程)
1、事前防御性(LBYL)
2、事后认错行 (EAFP)
2、异常的抛出
3、异常的捕获
1、异常声明(关键字throws)
2、try和catch关键字(捕获并处理异常)
3、finally关键字
4、异常的处理流程
5、自定义异常类
1、初步认识异常
在Java中,将程序执行过程中发生的不正常行为成为异常,比如下面常见的例子:
1、算数异常
public class Test {
public static void main(String[] args) {
System.out.println(10/0);
}
}
来看上面的提示的异常:java.lang.ArithmeticException.他表示的是包当中的类,所以这里我们可以认为ArithmeticException(算数异常)是一个类。
2、空指针异常
public class Test{
public static void main(String[] args) {
int[] array = null;
System.out.println(array.length);
}
}
NullPointerException(空指针异常)表示的也是一个类存在于java.lang包当中。
3、数组越界异常
public class Test{
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
System.out.println(array[10]);
}
}
像上述两个异常一样,ArrayIndexOutOfBoundsException(数组越界异常)也属于java.lang包当中。
从上述过程中可以看到,Java中不同类型的异常,都有与其对应的类来进行描述。
❗❗❗【注意事项】
1、当发生异常之后,没有人为处理异常的时候,那么后面的内容不会被执行。
2、在以后的学习当中,我们会遇到各种各样的异常,当我们不知道某种异常产生的原因或者不知道什么错误可以导致这种异常产生的时候
🔆第一种方式:我们可以看这个异常的注释
第二种方式:可以查看这个技术网站:stackoverflow,但是这个网站是国外的网站,查询的时候会非常的慢。
2、异常的结构体系
异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:
从上图可以看到:
- Throwable:是异常体系的顶层类,其派生出两个重要的子类:Error (错误)和 Exception(异常)。
- Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等。
- Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。我们平时所说的一场就是Exception。
3、异常的分类
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:编译时异常和运行时异常
1、编译时异常/受查异常
在程序编译期间发生的异常,称为编译时异常,也称为受查异常(Checked Exception)。
此时这种异常要处理掉,不然编译不会通过。
举例:当我们创建一个student类,让他实现Cloneable接口,并在Student类当中重写clone()方法,在Test类当中创建Student类的对象,并调用clone方法。
像上述的算数异常、空指针异常、数组越界异常,他们在编译时并没有报出异常,都正常编译通过,但上图在编译时,就出现了红色波浪线,表示出现异常,那为什么会出现这种情况呢?
像算数异常、空指针异常、数组越界异常,都是运行时异常(非受查异常) ;像上图出现的克隆异常,被称为编译时异常(受查异常)。
【注意事项】
下面的这种情况不属于编译时异常:编译时出现的语法错误不能称之为异常
以上这两种都不属于编译时异常,这只是语法错误。所以在写代码的时候,一定要分辨清楚是语法错误还是编译时异常。
2、运行时异常/非受查异常
- 在程序执行期间发生的异常,称为运行时异常,也成为非受查异常(Unchecked Exception)
- RuntimeException以及其子类对应的异常,都称为运行时异常,比如:之前的算数异常(ArithmeticException)、空指针异常(NullPointerException)、数组越界异常(ArrayIndexOutOfBoundsException)
注意:运行时异常指的是程序已经编译通过得到class文件了,再由JVM执行过程中出现的错误。
4、异常的处理
1、处理异常的编程方式(防御式编程)
1、事前防御性(LBYL)
在操作之前做充分的检查。每一步操作都进行检查。
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
ret = 选择英雄();
if (!ret) {
处理选择英雄错误;
return;
}
ret = 载入游戏画面();
if (!ret) {
处理载入游戏错误;
return;
}
缺陷:正常流程和错误处理流程代码混在一起,代码整体显得比较混乱。
2、事后认错行 (EAFP)
先操作,遇到问题再处理,在Java当中异常处理选择使用这种方式
try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;
}
......
优势:正常流程和错误是分离开的,程序员更关注正常流程,代码更清晰,容易理解代码。
在Java当中,异常处理主要的5个关键字:throw、try、catch、final、throws。
关键字 | 功能 |
throw | 抛出异常 |
try | 将可能出现的异常代码放在try里 |
catch | 捕获异常 |
finally | finally中的代码一定会被执行 |
throws | 声明异常 |
要处理异常首先得有异常,怎样才能有异常?代码在编译或者运行的时候,触发异常或者抛出异常 。
2、异常的抛出
我们在编写程序的时候,大多数是编译器抛出异常,但是我们可不可以自己抛出异常。
答案是可以,在Java当中,我们可以借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者。
语法格式:throw new XXXException("异常产生的原因");
1、这里抛出的是运行时异常的子类空指针异常 ,所以可以不用处理。
public class Test{
public static void test1(int[] a){
if(a == null){
throw new NullPointerException("空指针异常");
//根据自己的需求,抛出自己想要的异常
//这个语法一般用在自己自定义的异常
}
}
public static void main(String[] args) {
test1(null);
}
}
2、这里抛出的是编译时异常,用户必须自己处理,不然编译无法通过。
如何解决,在下面的内容中。
❗❗❗【注意事项】
- throw必须写在方法体内部
- 抛出的对象必须是Exception或者Exception的子类对象
- 如果抛出的是RunTimeException或者RunTimeException的子类,则可以不用处理,直接交给JVM来处理
- 如果抛出的是编译时异常,用户必须处理,否则编译无法通过
- 异常一旦出现,其后的代码就不会继续执行。
3、异常的捕获
异常的捕获,也就是异常的具体处理方式,只要有两种
异常声明throws |
try - catch捕获处理 |
1、异常声明(关键字throws)
语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2....{ } public static void test1(int[] a) throws CloneNotSupportedException{ }
如上述的编译时异常,CloneNotSupportedException异常如何解决,这里就说明第一种解决方式
声明过后的意思:大概就是站在test1方法的角度来说,我没有责任了 ,这个方法内部会抛出什么异常我已经告诉你了,就需要方法的调用者自己来解决了
声明的作用:当方法中抛出编译时异常,编写该方法的用户不想处理该异常,此时就可以借助throws借助throws将异常抛给方法的调用者来处理,即当前方不处理异常,提醒方法的调用者处理异常。
那么,给main方法也进行声明
可以看见,编译器没有报错,那么没有报错,就说明这个异常被解决了吗?在运行时就不会报错了吗?
可以看见,异常依旧存在 ,这里的处理异常,只不过是让代码在编译的时候不报错,但是代码的逻辑问题还是没有解决,这时候这个异常被交给了JVM进行处理,异常交给JVM处理,代码运行就会直接终止。
【注意事项】
- throws必须跟在方法的参数列表之后
- 声明的异常必须是Exception或者Exception的子类
- 方法内部如果抛出多个异常,throws之后必须跟多个异常类型,之间用逗号隔开。
public class Test{ public static void test1(int[] a) throws CloneNotSupportedException, NullPointerException,ArrayIndexOutOfBoundsException { if(a == null){ throw new CloneNotSupportedException(""); } }
当然,如果觉得上述的方法太麻烦,可以直接声明多个异常的父类异常,这样的写法虽然写的时候方便的,但是这样的代码可读性非常低,Exception异常有很多的子类,这样让方法的调用者,不知道调用的方法会抛出什么异常。
public static void test1(int[] a) throws Exception{}
- 调用声明会抛出异常的方法的时候,调用者必须对该异常进行处理,或者继续使用throws抛出。
【面试题】
throw:用来抛出一个异常 |
throws:用来声明一个异常 |
2、try和catch关键字(捕获并处理异常)
上面说到的throws,并没有对异常进行真正的处理,而是将异常声明了,方法的调用者需要对异常进行处理,这里就需要try - catch。
语法格式:
try{ //将可能出现异常的代码放在这里 }catch(要捕获的异常类型 e){ //如果try中的代码抛出异常了,此时catch捕获时异常类型于try 中抛出的异常类型一致时, //或者时try中抛出异常的基类时,就会被捕获到 //对异常就可以处理,处理完成之后,跳出try-catch结构,继续执行后序代码 }catch(要捕获的异常类型 e){ //第一个catch没有捕获到异常,下一个catch捕获异常,捕获到了,对异常进行处理, //处理完成之后,跳出try-catch结构继续执行后序代码 //后序代码 // 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行 // 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
举例:
public class Test{
public static void main(String[] args) {
try{
test1(null);
}catch(CloneNotSupportedException e){
e.printStackTrace();
System.out.println("捕捉到了 CloneNotSupperException 异常,进行处理异常的逻辑");
}catch(NullPointerException e){
e.printStackTrace();
System.out.println("捕捉到了 NullPointerException 异常");
}
System.out.println("正常的逻辑......");
}
}
可以看出,当使用了try-catch 结构处理异常,可以将异常解决。
【注意事项】
- 对于受查异常来说,当try当中没有抛出catch这个受查异常的时候,catch检测不到就会报错。
- 运行异常(非受查异常)检测没有那么严格:try当中的代码抛出异常和catch检测的异常不相同,编译器在编译阶段并不会报错
- 有的人想到将catch要检测的异常写成Exception,这样就可以一劳永逸了,什么异常都可以捕Exception放在第一个catch中,那么后续的catch都就没有作用了。这样写没有错但是不推荐,这样写的代码可读性不高
- 但是可以用Exception来兜底。将Exception放在最后的catch中。
public static void main(String[] args) { try { //test1(null); int[] array = null; System.out.println(array.length); } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); System.out.println("捕捉到了 Exception 异常,进行处理异常的逻辑"); } catch (NullPointerException e) { e.printStackTrace(); System.out.println("捕捉到了 ArithmeticException 异常,进行处理异常的逻辑"); }catch(Exception e){ e.printStackTrace(); System.out.println("捕捉到了 ArithmeticException 异常,进行处理异常的逻辑"); } System.out.println("正常的逻辑......"); } }
- 当try当中存在多个异常的时候,从上往下执行,谁先抛出异常就捕获那个异常;catch当中程序的书写顺序,不影响异常的捕获。
- 当try当中有多个异常时,在同一时间只会抛出一个异常,就上面图片中显示的一样。
- 当try块内抛出异常的位置之后的代码不会被执行
- 如果try当中抛出的异常,catch没有捕获到,那么就会交给JVM,将异常交给JVM之后就会被中端程序
❗❗补充:printStackTrace ()方法的意思是:在命令行打印异常信息在程序中出错的位置及原因。
3、finally关键字
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库 连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能 导致有些语句执行不到,finally就是用来解决这个问题的。
语法格式:
try{ // 可能会发生异常的代码 }catch(异常类型 e){ // 对捕获到的异常进行处理 }finally{ // 此处的语句无论是否发生异常,都会被执行到 } //正常代码 // 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
finally当中的代码一定会被执行
public static void main(String[] args) {
try {
int[] array = null;
System.out.println(array.length);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到了 ArrayIndexOutOfBoundsException 异常,进行处理异常的逻辑");
}catch(ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到了 ArithmeticException 异常,进行处理异常的逻辑");
}finally{
System.out.println("finally 一般被用于资源的释放....");
}
System.out.println("正常的逻辑......");
}
}
即使try当中抛出的异常没有被捕获处理,但是finally块内的代码还是会被运行。
- 当然你也可以尝试当try中抛出的异常被捕获并解决了之后,finally块内的代码会不会被执行。
- 也可以尝试,当try当中的代码没有异常抛出,finally块内的代码会不会执行。
finally被用来进行资源的释放
第一种写法
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
int a = scanner.nextInt();
int[] array ={1,2,3};
System.out.println(array[1]);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到了 ArrayIndexOutOfBoundsException 异常,进行处理异常的逻辑");
}finally{
scanner.close();//关闭Scanner资源
System.out.println("finally 一般被用于资源的释放....");
}
System.out.println("正常的逻辑......");
}
第二种写法
public static void main(String[] args) {
//写成这种格式,在finally中,就可以不用写scanner.close();
try (Scanner scanner = new Scanner(System.in)) {
int a = scanner.nextInt();
int[] array ={1,2,3};
System.out.println(array[1]);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到了 ArrayIndexOutOfBoundsException 异常,进行处理异常的逻辑");
}finally{
System.out.println("finally 一般被用于资源的释放....");
}
System.out.println("正常的逻辑......");
}
注意:finally中的代码一定会执行的,一般在finally中进行一些资源清理的扫尾工作。
下面我们来看一道题
这个代码你觉得输出的结果会是几?
- finally 执行的时机是在方法返回之前(try 或者 catch 中如果有 return 会在这个 return 之前执行 finally). 但是如果 finally 中也存在 return 语句, 那么就会执行 finally 中的 return, 从而不会执行到 try 中原有的 return.
- 所以我们一般不建议在finally中写return
4、异常的处理流程
我们先来了解一个知识点:调用栈
方法之间是存在相互调用关系的,这种调用关系我们可以应用"调用栈"来描述,在JVM中有一块内存空间称为"虚拟机栈"专门存储方法之间的调用关系。
当func方法中存在异常的时候,没有解决,这个异常会随着调用栈的顺序,进入main方法,main方法中没有解决这个异常,那么这个异常就会交给JVM,那么程序就会直接终止掉。
【异常处理的流程】
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配
- 如果找到匹配的异常类型, 就会执行 catch 中的代码
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
- 如果上层调用者也没有处理的了异常, 就继续向上传递.
- 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.
5、自定义异常类
Java当中虽然已经内置了丰富的异常类,但是并没有完全包含实际开发中所遇到的一些异常,此时就需要我们自己定义一些异常类来供我们自己使用。
来看一个登录程序:
public class Test { public String name = "abcde"; public String password = "12345678"; public void login(String name,String password){ if(!this.name.equals(name)){ System.out.println("用户名错误"); return; } if(!this.password.equals(password)){ System.out.println("密码错误"); return; } } public static void main(String[] args) { Test test = new Test(); test.login("wertd","23455"); } }
可以看到,当按上述代码格式来运行,当输入的密码于写死的密码不相同时,程序就会结束,但是在一个程序中,我们不能因为某个验证错误,让整个程序结束运行,我们是要程序是跑起来的,当遇到异常时,将异常抛出,但是程序还在运行。
这里我们来写一个自定义的异常类,来解决这个问题
定义自定义的异常类
UserNameException类继承Exception类,表示受查异常/编译时异常
UserNameException类继承RuntimeException类,表示非受查异常/运行时异常
下面通过自定义好的异常类,来修改上述的代码
来看两个自定义的异常类
public class UserNameException extends RuntimeException { public UserNameException(){ super(); } //写一个有参构造方法,可以在抛出异常的时候,编辑出现异常的原因 public UserNameException(String s){ super(s); } }
public class PasswordException extends RuntimeException{ public PasswordException(){ super(); } public PasswordException(String s){ super(s); } }
用户登录功能的代码
public class Test { public String name = "abcde"; public String password = "12345678"; public void login(String name,String password) throws UserNameException,PasswordException{ if(!this.name.equals(name)){ System.out.println("用户名错误"); throw new UserNameException("你的用户名错了");//这里“”内的内容,是在运行期间编译器抛出异常后,用作提醒错误的原因 } if(!this.password.equals(password)){ System.out.println("密码错误"); throw new PasswordException("你的密码错了"); } } public static void main(String[] args) { Test test = new Test(); try{ test.login("wertd","23455"); }catch(UserNameException e){ e.printStackTrace(); System.out.println("用户名异常"); }catch(PasswordException e){ e.printStackTrace(); System.out.println("密码异常"); }finally{ } } }
💥总结:
- 自定义异常类,然后继承自Exception或者RuntimeException
- 实现一个带有String类型参数的构造方法,参数含义:出现异常的原因
- 自定义的异常类继承自Exception的异常默认是受查异常 / 编译时异常
- 继承自RuntimeException的异常默认是非受查异常 / 运行时异常。