Java | 异常类【万字详解,看过来】
万字详解Java异常类,带你感受异常类在开发中的重要性
Hello ,大家好,国庆第一天,为你们带来Java的异常类讲解🎓,希望本文对您有所帮助
Java之异常类讲解
❓概念了解:什么是异常类?
所谓异常就是程序运行时可能出现的一些错误,例如数组下标越界、出现空指针异常或者是算数异常
- 这个时候Java会使用throw去抛出一个Exception子类的实例表示异常发生,举个例子
int number = Integer.parseInt("ab89");
- 上述代码如果去运行的话就会报异常,抛出一个【NumberFormatException】的对象 ,英语稍微好一些的同学应该可以看出来这是【数字格式化异常】,这样的异常种类在Java中还有很多,让我们了解一下吧
📂层次顺理:Java中的异常层次结构
- 在Java中所有的异常都派生于Throwable这个类,在它的下一层,有两个分支,分别是Error和Exception,就是错误和异常,我们先来看一张我画的思维导图
导图顺理
- 可以看出,我们要重点关注Exception层次结构,这个层次结构又可以分为RuntionException和其他异常,其中Runtim~和Error称为运行时异常/非受查异常,在程序运行的时候才会抛出的异常;而其他异常类被称为编译时异常/受查异常,在程序编译时就会抛出异常
- 对于运行时的异常,是我们碰到最多的,也就是我上面提到的三个最常见的
- IndexOutOfBoundsException——下标越界异常
- NullPointerException——空指针异常
- ArithmeticException——算术异常
直击现场
可能大家在程序报出异常的时候也没有去自己看过是什么异常,这里我就给出运行时的报错截图给大家看看
- 看完了这三张图,你应该知道系统报出异常是在何处了吧,赶紧自己去试试吧
🌳再进一步:如何抛出异常?
假如我们遇到了无法处理的情况,Java方法可以抛出一个异常,但是这个异常要手动去抛出,而不是编译器帮你抛出,所以对于上述一些常见的异常,我们应该在实践的过程中将它牢记于心❤️
声明检查性异常
所谓声明检查时异常,就是要通过throws关键字,在方法体的首部,抛出在这个方法体中可能会出现的异常
- 例如下面这个异常就是查找不到文件异常,这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能抛出一个FileNotFoundException的异常。
- 当抛出异常的时候,系统就会通过抛出的这个异常去搜索如何处理这个异常对象的异常处理器
public FileInputStream(String name)throws FileNotFoundException
- 但是对于一个方法,也可能会有多个异常,所以如果一个方法有可能抛出多个检查型异常,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开,例如下面这个示例
public image LoadImage(String s)throws DileNotFoundException,EOFExcetion
- 但是对于一些Java的内部错误,即从Error继承的异常。任何程序代码都有可能抛出哪些异常,所以我们对此完全无法控制
- 不仅如此,对于从Runtime~继承而来的非受查异常,我们其实不应该声明,就比如说下面这个数组越界的异常抛出
void drawImage(int i)throws ArrayIndexOutofBoundsException
- 对于这种异常的抛出其实我们没有必去去做,因为这种事情我们只要再检查一遍自己写的程序,一般都不会出现数组越界的情况。担心这些错误就要多花时间去修正它们,而不是去声明这些错误有可能发生
throw和throws的区别
知道了如何去声明一个检查型异常,接下我们要学会如何去抛出一个异常
- 抛出异常不是在方法头部,乃是在方法体内部,一般都会去判断一下这个条件是否会产生异常,然后再去手动抛出,这里手动抛出乃是要用
throw关键字
,而不是上面我们说到的throws
,这一点大家一定要区分,很重要,具体如何去操作,我们先来看一下吧
public class test4 {
public static int divide(int x, int y) throws ArithmeticException{
//抛出可能会发生的异常
if(y == 0) {
throw new ArithmeticException("y == 0");
//具体执行会抛出异常的动作
}
return x / y;
}
public static void main(String[] args) {
try{
int ret = divide(10,5);
System.out.println(ret);
}catch (ArithmeticException e){
e.printStackTrace();
System.out.println("异常警告,除数为0");
}finally {
System.out.println("程序结束");
}
}
}
- 这个例子是讲到了两数相除除数不能为0的情况,可以看到方法头部通过throws抛出了一个算术异常,然后在方法体内部进行了一个判断,若除数y为0,则用
throw new ArithmeticException
- 可以看到这里不仅使用到了throw关键字,也用到了new关键字,因为你的异常是一个类,所以你要抛出的是它的这么一个对象,然后在小括号里,就写抛出这个异常会触发的条件
- 因此我们可以的出这两个关键字的区别👇
- throws关键字是声明在方法首部的,表示这个方法体会有抛异常的行为,只是一个可能性的评估
- 但是throw关键字是一个具体的关键动作,是会通过判断去抛出具体异常的这么一个行为
初步了解了如何抛出异常后我们来看一下抛出异常的时机
public class test5 {
static class Person implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person(); //*不推荐*
Person person2 = (Person) person1.clone();
}
}
- 上述代码是在通过一个类去是实现了Cloneable这个克隆类,然后重写了其中的克隆方法,接着看到第二个代码块,是想将一个对象克隆给到另一个对象,但是类型由不允许,于是进行了强转,但是强转之后还是会报错
- 从上述代码可以看出是处理
CloneNotCupportedException
这个异常类,从开头的思维导图可以知晓,其为编译时异常,也就是受查异常,这不属于我们上面所说的运行时异常,尽量不要去声明,这个异常的话是在编译阶段就会给你报错,所以我们要手动地去声明这个异常或自己抛出异常,所以通过throws关键字在方法体首部抛出了这个异常
- 此时我们可以通过按下Alt+Enter键,选择第一个,即添加异常到方法首部
- 但是这样的操作并不推荐,为什么呢?通过上述对throws关键字的了解,它只是一个可能性的行为,是面对可能会出现的异常去作出的一个评估,存在一定的风险;而且就算你抛出去了之后别人若是调用你的这个方法那它还是要处理这个异常,所以自己的异常还是自己处理
- 如果是在企业中做项目时,你的老板肯定不允许你这么去写,我们应该用更加安全可靠的方法去检查这个异常,那就要用到try&catch语句块了,下面是具体的代码
public static void main(String[] args){
//受查异常
Person person1 = new Person();
try {
Person person2 = (Person) person1.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
🌳层层递进:如何捕获异常?
💑初次见面:初步try&catch处理异常
面对老板的质疑,看来我们不得不学习这个语句块了,才能对异常的处理灵活可靠⛸
- 首先你要了解的肯定是try&catch这个语句块,我们可以将可能会出现的异常操作都放在try部分中,一旦try抛出异常对象,或是调用可能抛出异常对象的方法,并且该方法抛出了异常对象,那么try部分将即刻结束执行,也就是try下面的语句都不会执行到,然后转向执行相应的catch部分,所以我们可以将发现异常后地处理放在此处
- 接着我们先来看一个示例
public class test2 {
public static int fun(){
int[] arr = {2,3,1};
arr = null;
try{
System.out.println(arr[3]);
return 1; //不会被执行
}catch(NullPointerException e1){
e1.printStackTrace();
System.out.println("捕获到了空指针异常");
return 3;
}
return 0;
}
public static void main(String[] args) {
int ret = fun();
System.out.println("ret = " + ret);
}
}
- 从上述代码可以看到,若是在try语句块会出现异常的语句后写的语句,都不会被执行,出现异常后,比如说这里出现了空指针异常,那即刻就会执行到catch子句
- 这里有同学发出疑问了❓,这个catch小括号里的【NullPointerException e1】是干什么用的,因为NullPointerException是一个空指针异常类,那么它声明出来的变量就是对象,这个对象我们通过是去打印栈的追踪【e1.printStackTrace();】
- 最后看到运行结果,ret返回的是3,可以很明显地看出try语句块中的return 1没有被执行,而是最后执行了catch子句中的return 3这句话
捕获多个异常
当前,有些时候我们可能需要捕获的不止一个异常,就像一个数组可能会出现数组越界,也可以会出现空指针异常,这我们在实际开发的过程中都要能关注到
- 接下来我们通过案例来具体看看
public static int fun(){
int[] arr = {2,3,1};
//arr = null;
try{
System.out.println(arr[3]);
return 1; //不会被执行
}catch(NullPointerException e1){
e1.printStackTrace();
System.out.println("捕获到了空指针异常");
return 3;
}catch(ArrayIndexOutOfBoundsException e2){
e2.printStackTrace();
System.out.println("捕获到了数组越界异常");
}finally {
//finally中的代码一定会被执行
System.out.println("程序运行结束");
//return 2;
// 不建议在finally中出现return语句,因为一定会return此处的数据
}
return 0;
}
- 也就是我们上面所讲到的,加了一个catch的子句判断,除了空指针异常以外还要去判断数组是否越界,这样就可以对不同类型的异常作出不同的处理
- 或者也可以合并catch子句,将多个异常写在一些👇
catch(NullPointerException e1 | ArrayIndexOutOfBoundsException e2)
finally子句的说明
- 从上面的代码中我们可以看出,最后有一个finally,这其实在很早就展示给了大家,那这个子句到底有什么用呢?
- 当我们的代码抛出异常时,就会停止处理这个方法中剩余的代码,并退出这个方法。如果这个方法已经获得了只有它自己知道的一些本地资源,那么这些资源就必须被清理
- 所以,finally子句主要是用来清理捕获异常后的一些空置资源
- 有一点值得一提🎐,不管是否有异常捕获,finally子句中的代码都会被执行
让我们来看一个具体的小例子
public class test3 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try{
int n = sc.nextInt();
System.out.println(n / 10);
} catch (ArithmeticException e) {
e.printStackTrace();
}finally {
sc.close();//关闭输入器
}
}
}
- 可以看到,这里是声明了一个输入器,去输入一个数字,然后输出其除以10后的结果,catch子句处理的是算术异常,最后到了finally子句,写了sc.close();去关闭这个输入器,因为finally中的语句无论如何一定会被执行,因此最后在返回计算的结果后输入器就会被关闭然后结束程序
当然,如果有了finally子句,也是可以不写catch子句,比如就像下面这样
InPutStream in = ...;
try{
code for might throw exceptions
} finally{
in.close();
}
- 无论try语句块中是否遇到异常,finally子句中的in.close()都会被执行
我们再来看一种,就是内外嵌套的try语句
InPutStream in = ...;
try{
try{
code for might throw exceptions
} finally{
in.close();
}
}
catch(IOException e){
show error messsage
}
- 这样声明的话内层的try语句块就只有一个职责就是关闭输入流
- 外层的try语句块也只有一个职责,就是确保报告出现的错误
- 这样的解决方案不仅清楚,而且功能更强:将会报告finally子句中出现的错误
🖊try&catch处理异常的注意事项
初步了解了try&catch语句块,接下来让我们去看一下使用这和语句块需要注意点什么
1、 catch块当中,一定要捕获相应的异常,如果程序抛出的异常在catch块当中,不能被捕获。那么就会交给JVM处理。
- 也就是说,我们在编写程序的时候,就要去思考这个程序会出现什么异常,抛出可能出现的异常,也就是我们在上面所说的编译时异常
2、可以通过catch来捕获多个异常。
- 这个我们在上面也是有讲到过,如果程序存在多个异常的可能时,就要考虑用catch去捕获多个异常
3、不建议大家直接捕获Exception。
- 这个我开头有讲到过,因为有一些异常大家可能在编写程序的时候一时半会考虑不到,但是又觉得要去捕获这个异常,因而直接捕获了异常的父类Exception,这其实是不太好的
- 首先,捕获异常就是为了处理异常,不同的异常需要有不同的处理方式,区分并捕获具体的异常,就可以对具体的异常做出最准确的处理,并且对应的异常信息也是最准确的,直接捕获Exception有时会导致真实出现的异常信息模糊。
4、finally块当中的代码,终究会被执行的。
- 因为finally语句块是在最后执行的,所以无论try中是否有抛出异常,其中的代码都会执行
5、不建议在finally当中出现return语句。
- 这一点的话还是要牢记的,不然一个不小心就会导致一些大问题,因为finally中的语句终究都会被执行,也就是假设你在try语句体抛出异常的语句之前return了1,catch中return了2,但是finally中又return了3,最后返回的结果一定是3,因为这是会产生一个覆盖的,返回的一定是最后执行的那个语句
✈自己动手:创建一个异常类
了解了如何抛出异常,清楚了try&catch子句以及finally的执行原理,接下来我们自己来动手写一个异常类吧
- 首先有一点很重要,就是一定要去继承一个父类,但是最后不要继承运行时间的异常类,最好是总的异常类Exception,如果你去继承了一个运行时的异常类,那在编译阶段就看不出你自己写的这个类有什么问题,就很难去排查😬
/**
* 自定义异常类
*/
//一定要继承一个异常父类
class MyException extends RuntimeException{
public MyException() {
}
public MyException(String message) {
super(message);
}
}
- 然后我们写一个简单一些的func函数,做一个测试,手动抛出异常是的条件是【x == 10】,然后到了主方法中,使用try&catch子句包围起来从而去排查这个异常,可以看到catch中的异常类写的也是我们自己定义的,
public class test6 {
public static void func(int x){
if(x == 10){
throw new MyException("x == 10");
}
}
public static void main(String[] args) {
try{
func(10);
}catch (MyException e){
e.printStackTrace();
System.out.println("发生异常");
}finally {
System.out.println("程序正常结束");
}
}
}
- 从运行结果可以看出,传入10的话就会抛出我们自己所定义的异常,下面我们传入20,
- 从这两张图可以看出,无论是否抛出异常,finally子句中的【程序正常结束】都会被执行,这很好得验证了我们上面所说的理论
🌳项目实战
好,学完所有关于异常类的知识后,我们来做一个项目感受一下异常类在显示现实生活中是如何运用的
- 我们来做一个用户名密码登录检查的案例,首先这个案例如果我们用前面所学的知识,你会怎么去实现呢?这里先给出代码
/**
* 登录检测 - 普通版
*/
public class test7 {
private static String UserName = "abcde";
private static String PassWord = "12345";
public static void login(String name, String code){
if(!UserName.equals(name)){
System.out.println("账户错误");
}else if(!PassWord.equals(code)){
System.out.println("密码错误");
}else{
System.out.println("登录成功");
}
}
public static void main(String[] args) {
login("abcde","12345");
}
}
- 可以看出,就是通过一个方法,传入用户名和密码,使用String类的equals方法进行一个内容的判断,这我们已经是非常熟练了,就不多说
接着我们用异常类的知识去实现一下
/**
* 登录检测 - 精良版
*/
class UserException extends Exception{
//自定义异常类最好使用Exception
public UserException() {
}
public UserException(String message) {
super(message);
}
}
class PassWordException extends Exception{
public PassWordException() {
}
public PassWordException(String message) {
super(message);
}
}
public class test8 {
private static String UserName = "abcde";
private static String PassWord = "12345";
public static void login(String name, String password) throws
UserException,PassWordException{
if(!UserName.equals(name)){
throw new UserException("用户名错误");
}else if(!PassWord.equals(password)){
throw new PassWordException("密码错误");
}else{
System.out.println("登录成功");
}
}
public static void main(String[] args) {
try{
login("abcde","12345");
}catch (UserException userException){
userException.printStackTrace();
}catch (PassWordException passWordException){
passWordException.printStackTrace();
}
}
}
- 好,我们来分析一下,在改良版中,我们将用户名和密码都分别定义成了单独的异常类,然后去继承总的异常父类Exception
- 接着在login()这个登录方法中,我们使用throws关键字去抛出可能会有的异常,也就是我们自定义的异常类,若是出现用户名或者密码错误,就会手动去抛出【用户名错误】或者【密码错误】这两个指令
- 然后看到main主方法中,在try子句去校验这个login()传入的参数是否正确,通过下面的两个catch去分别捕获异常,然后去对不同的异常作出不同的处理
🏃写在最后
- 通过不同的测试,我们也可以看出异常类在分析解决实际问题的时候可以起到很大的帮助,所以大家在学习了本文后,有没有对Java中的异常类有了一个系统的认知,如果没有可以再去查询一些数据资料,我可能有些地方没有讲的很清楚,尽量给大家讲到位
最后,感谢您对本文的观看,如果疑问请于评论区留言或私信我🌸
更多推荐
所有评论(0)