1.关于异常
异常的概述
异常!!!
在我们生活中是几乎随处可见的,比如说我们生病了,就是身体出现了异常!
既然身体出现异常,是否解决就要看它的异常程度。就比如说发烧,感冒之类的,我们就不得不通过吃药去解决好,采取必要的措施去处理异常。
而在我们的程序中,尤其是学习的过程。出现异常和处理异常是必不可少的环节,而出现的异常的情况也有在编写代码时出现,也有在运行时出现。
在Java中,将程序执行过程中发生的不正常行为称为异常。
我们在编程的过程中,主要会遇到一下两种异常:
Error:是指在Java虚拟机中无法解决的严重问题。比如说:JVM的内部错误、资源耗尽等 ,比较典型的有StackOverflow(栈溢出),OutOfMemoryError(内存溢出)。一般不 会编写特定的代码去处理这样的异常。
Exception:因编写原因,或者外在因素而导致发生的一般异常,可以通过别写一些特定的代码去 解决,使程序继续运行。如:空指针异常,算术异常,网络连接异常等等。
异常的体系结构
异常的种类有很多种,为了能将规范表示和使用,java里也有一个专门将它们分类的体系结构:
可以看得出,在这个体系中,Throwable是这个体系结构的顶层父类。而Error和Exception就是从这里面派生出来的。
异常的分类
关于Exception异常,我们可以分为受查异常(编译时期异常)和非受查异常(非编译时期异常)。
编译时期异常,就是我们在写代码时就会出现的错误,导致代码无法运行(但不是指语法错误导致的无法运行,比如:没加分号,括号)。例如:
此时发生的就是编译时异常,由于调用clone方法返回的类型时object类,而我们的person2初始化是Person1类,所以无法成功赋值,导致发生了CloneNotSupportedException异常,最终我们的代码无法运行。
运行时异常
其实也就是非受查时异常,是在进行代码运行过程中,或者说运行代码之后才发生异常。在编译过程不会发生异常,报错,可以正常运行,但是运行结果无法输出。比如最常见的空指针异常:
public static void main(String[] args) {
int[] arr = null;
System.out.println(arr.length);
}
运行结果:
除此之外还有常见的
计算异常(ArithmeticException)
数组越界异常(ArrayIndexOutOfBoundsException)
输入不匹配异常(ArrayIndexOutOfBoundsException)等等,这些异常都属于运行时异常。这些异常通常都是RunTimeException以及其子类对应的异常。
2.异常的处理
2.1 异常的抛出
在编写程序时,如果程序中出现错误,或者不希望出现的情况被输入。
在java中,我们可以通过throw关键字,抛出一个指定的异常,从而终止程序运行并将错误的信息告知给运行者。比如我不必希望输入“10”:
当我们输入10之后,就会出现异常
关于throw:
1. throw关键字的使用必须在方法体的内部。
2. 抛出的异常必须是Exception或者Exception的子类对象。比如说还可以抛出空指针异常:
运行结果:
可以看得到,异常在第14行,也就是我们抛出异常的地方。
3.如果被抛出的是编译时异常,我们就必须马上处理,否则代码将无法编译。当我们设置的异常一旦被抛出,后面的代码将不会再执行。
2.2 异常的捕获
既然我们想要将异常处理,我们就必须将异常给捕获从而进行处理。而捕获异常的方式通常有两种:
1.异常声明throws
2.通过try-catch-finally方式
2.2.1 异常声明throws
这种处理方式会放在发生异常的方法的参数列表后面,像这样
修饰符 返回类型 方法名 (参数列表)throws 类型1Exception,类型2Exception...{
/...../
}
这种处理方法的用法是:当方法中在编译过程时抛出了异常,用户又不想处理该异常,提醒调用者来处理。即当前异常编写时不提醒,但是会在调用时,提醒调用者处理。
需要注意的是:声明的异常必须是Exception或者是Exception的子类,而且必须写在方法的参数列表之后。当方法内部抛出多个异常时,throws也必须加上多个异常,并且用“,”隔开。(如果抛出的多个异常都是来自于同一个父类,只声明一个父类的异常就可以了)
2.2.2 try-catch-finally捕获并处理
throws的处理方法只是让编译时异常不在报错,从而让代码运行。但是异常的地方还是没有被解决掉。最后还是会导致代码无法正常运行完毕。所以我们就可以通过try-catch-finally的代码段解决出现的异常。
基本语法:
try{
/*可能出现异常的代码*/
}catch(要捕获的异常类型 e){
/*如果在try当中出现了异常,并且被这个catch捕获到对应异常类型,将会运行这段代码里面的内容。*/
/*但如果没有捕获到异常,或者try中没有发生异常,这段代码将不会运行,或者交给下一个catch处理,如果都捕捉不到,就会交给JVM处理。*/
}catch(异常类型 e){
/*对异常处理*/
}.....finally{
/*此处的代码一定会被执行到*/
}
运行结果:
这种捕获的方式,更多需要注意的地方在try部分的代码段:
1. 在try当中,如果有在一处地方发生了异常将不会在继续try里面的剩下的代码,而是到catch中处理异常。
结果还是会和上面一样
2.try当中捕获到一个异常之后,将捕获不到其它异常,一个try-catch代码段只能捕获到一个异常,因为发生异常时,try已经不会再继续执行。
运行结果:
3. 同样当我们不确定try代码中会发生什么样的错误时,我们可以采取多个catch来捕捉异常。
运行结果:
如果我们多次捕获异常后处理方式都一样,可以将多种异常写在一起
----------------------------------------------------------------------------------------------------------------
但是如果我们捕获的异常具有父类子类关系时,必须注意它们的捕获顺序
ArithmeticException | NullPointerException都是Exception的子类,所以会发生错误。
所以我们要将父类的异常放在子类之后,才能正常运行
所以,我们也可以用这种方法来捕获我们代码中未知的错误,当我们用子类的异常去捕获时捕获不到,可以用Exception来放在最后来捕获,使代码正常运行,不被JVM处理。
2.2.3 关于finally
在写程序时,有些特定的代码,无论是否发生异常,都必须要执行的,比如说程序中打开的资源:网络连接,数据库连接,IO流等,在程序正常或者异常退出的时候,都必须要对资源进行回收。有的代码会因为异常的出现而导致执行不到。而finally部分就可以解决这样的问题。
在finally里的语句无论怎么样都会被执行。不论是代码正常运行,还是发生了异常。
运行结果:
可以看见,没有异常时,所有代码都会被正常执行。
但是当代码内出现了异常
运行结果:
代码将会在异常出现的位置开始停止执行其它语句并报错,但是finally里的语句仍然可以运行。这就是finally的特别之处。
所以但我们想关闭一些程序时,或者回收一些资源时,最好的办法就是写在finally语句里,防止出现异常后,不能正常回收或关闭。
---------------------------------------------------------------------------------------------------------------------------------
除此之外,finally语句还有一个特殊的地方。
当我们运行这段代码,并输入10的时候,结果会返回什么呢?
运行结果:
结果竟然是1000。也就是说try里面并没有执行的return,而是执行了finally里的return。
那再通过一段代码进行比较:
运行结果是:
通过对比,可以看得出,finally与return的执行顺序是,先执行finally部分,才会执行try-catchi里的return。
总结finally与return的关系:
finally的执行时机是在方法返回之前执行的,并且一定会执行。当return在try-catch部分时,finally会先执行完自己的代码,再执行try-catch里的return。而当两个部分都同时又return时,会再执行完其它代码之后,直接在finally里执行return。
所以我们一般不在finally里写return语句。
2.3异常处理流程
了解上面之后,我们可以得出我们处理流程的大致过程了!
1. 先将可能会产生异常的代码写到try部分。
2.如果try中的代码出现异常,那么try中的代码会从异常出停止执行,并看出现的异常与catch中的 异常比较是否匹配。
3.如果异常匹配,将会运行对应的catch里的代码。
4.当在出现异常的方法中无法处理,将会把异常一直向上传递。比如在这段代码中:
可以很明显看出会发生算数异常,但是我们的catch无法捕获和处理这个异常,而运行结果是这样的:
可以看到,它会在getData方法中先尝试处理,处理失败后报错,交给main方法处理。但是main方法也无法处理,最后交给JVM处理,所以最后就终止了程序。
5.无论是否能找到匹配的异常类型,finally中的代码都会在该方法结束或者返回结果之前被执行。
6.如果一直都没有合适的代码来处理发现的异常,最后就会交给JVM来处理,此时程序就会异常终止。
3. 自定义异常
在java中,虽然它本身就自带了一套异常体系,但是还是不足以满足我们在各种各样的场景下,会发生各种各样的异常。所以自定义异常,就是用来设计符合我们要维护的实际的异常情况。
3.1 自定义异常的定义方式
1.一般我们的自定义异常,都是来自于已有异常类的继承或者扩展。也就是继承Exception和RunTimeException。
当我们自定义的运行是异常时,继承的是Exception。
自定义的是编译时异常时,继承的时RunTimeException。
2.实现一个带有String类型参数的构造方法。该参数用于接受出现异常的原因。
3.2 自定义异常的设计
我们先设计一个登录系统。
我们在这段代码可以自定义两个异常,账号异常(UserNameException)和密码异常(PassWordException)。
先创建两个类来创建自定义异常
在利用Generate,将构造方法自动生成。
然后我们再回到登录系统中,如果我们使用throws将我们自定义的异常捕获,就需要再方法参数之后加上异常类型,并在代码中用throw来将异常抛出。
如果我们用try-catch-finally的代码段捕获,就不用再方法的参数列表之后声明异常类型,直接在try中写出可能会抛出异常的代码。再用catch捕获异常即可。
代码中e.printStackTrace();是用来提醒出现异常的地方。根据上面的代码运行结果为: