Java异常处理详解
文章目录
一、什么是异常,为什么要处理异常?
异常:异常即程序中出现的非正常的情况
为什么要处理异常:当程序抛出异常时,程序往往会发生中断,我们希望能够针对出现的异常作出一些动作使得在不重启程序的情况下能够使得程序恢复到正常的状态
二、异常的种类
1、Throwable:
Java中所有异常的根类,Throwable有两个子类Error和Exception
2、Error:
Error表示错误,错误是程序所无法处理的,出现Error往往意味着出现了一些严重的问题,这些问题往往与编码者执行的操作无关,而是代码运行时JVM出现的问题,例如JVM运行出现问题(VirtualMachineError)等,这些问题时不可查的,它们处在应用程序的控制能力之外,通常也不应该去捕获处理Error
3、Exception:
Exception程序本身可以处理的异常,Exception又分为UnCheckedException与CheckedException
- UnCheckedException:指的是非受检查异常,又可称为运行时异常,RuntimeException即其子类均属于UnCheckedException,这一类异常在编译时不会被发现,通常是由一些程序逻辑错误引起的,在程序中可以选择捕获异常处理,也可以不处理,若不处理这些异常,这些异常就会被抛给调用者进行处理,在编码的过程中应当尽可能的从逻辑角度避免这些异常。常见的运行时异常包括ClassCastException(类型转换异常)、ClassNotFoundException(类加载失败异常)、ArrayOutOfBoundsException(数组越界异常)、NullPointerException等
- CheckedException:指受检查的异常,也称非运行时异常,这一类异常会在编译器被编译器发现,是必须处理的异常,若不处理则编译无法通过。常见的非运行时异常包括:IOException、SQLException、ParseException等
三、异常的抛出与捕获
1、异常的抛出——throw:
首先我们需要知道在Java中,异常也是一种对象,抛出异常时首先要实例化异常对象。在很多时候我们碰到的异常都是由JVM自行判断并抛出的,但有些时候,我们也希望自己的程序在满足某些条件时可以抛出异常,那么就可以使用throw关键字,例如:
if (a+b<c){
throw new Exception("不能构成三角形");
}
2、异常的捕获与处理——try、catch块:
通过throw语句仅能抛出异常,并没有对异常进行处理,若一个方法中出现了异常,那么这个方法就会在异常出现的过程中退出,若我们希望方法能够继续执行,就需要对异常进行捕获并处理。我们可以使用try语句将可能抛出异常的语句包裹起来,使得异常能够被检测到。
在try语句后应当紧跟一个或多个catch语句块,catch用于匹配捕获到的异常并对异常进行处理。我们可以把try语句理解为110接警中心,用于发现异常,但发现的异常属于哪些种类,需要怎样进行处理,他都不需要关心,这些事情都将交由catch语句来做。例如若try语句中抛出了Exception1,那么将执行处理代码1
try{
//可能发生异常的代码块
//如throw new Exception();
}catch(Exception1 e1){
//处理代码1
}catch(Exception2 e2){
//处理代码2
}
... ...
此外还需要注意的一点是,当try语句中出现了异常时,抛出异常之后的语句将不再执行,例如以下代码,显然在遍历数组时会越界,当i=5时抛出ArrayOutOfBoundsException后,并没有执行最后的输出:
public static void test1(){
try{
int[] nums = new int[]{1,2,3,4,5};
for (int i = 0;i < 6;i++){System.out.println(nums[i]);}
System.out.println("try语句运行结束");
}catch (Exception e){
e.printStackTrace();
}
}
运行结果:
1
java.lang.ArrayIndexOutOfBoundsException: 5
2
at lzsf.TestMain.test1(TestMain.java:91)
3
at lzsf.TestMain.main(TestMain.java:61)
4
5
3、catch中抛出新的异常:
若catch语句捕获到了异常,并在异常处理阶段抛出了新的异常,那么该异常则会抛给上一级,即该函数的调用者进行处理,并且与该catch语句处于同一块的其余catch语句均会被忽略,例如如下的代码:
public static void main(String[] args){
try{
test();
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("main捕获了ArrayIndexOutOfBoundsException");
e.printStackTrace();
}
}
public static void test() throws ArrayIndexOutOfBoundsException{
try{
throw new NullPointerException();
}catch (NullPointerException e){
System.out.println("test捕获了NullPointerException");
e.printStackTrace();
throw new ArrayIndexOutOfBoundsException();
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("test捕获了ArrayIndexOutOfBoundsException");
e.printStackTrace();
}
}
运行结果如下,ArrayOutOfBoundsException并没有被test捕获到,而是被main捕获并处理:
test捕获了NullPointerException
main捕获了ArrayOutOfBoundsException
java.lang.NullPointerException
at lzsf.TestMain.test(TestMain.java:74)
at lzsf.TestMain.main(TestMain.java:55)
java.lang.ArrayIndexOutOfBoundsException
at lzsf.TestMain.test(TestMain.java:78)
at lzsf.TestMain.main(TestMain.java:55)
当然try、catch语句也可以嵌套,这里也可以在catch语句中再使用一个try、catch语句使得异常能在本级被处理,如下:
public static void test() throws ArrayIndexOutOfBoundsException{
try{
throw new NullPointerException();
}catch (NullPointerException e){
System.out.println("test捕获了NullPointerException");
e.printStackTrace();
try {
throw new ArrayIndexOutOfBoundsException();
}catch (ArrayIndexOutOfBoundsException a){
System.out.println("test捕获了ArrayIndexOutOfBoundsException");
a.printStackTrace();
}
}
}
4、抛向上级——throws:
若在一段代码中可能出现一些异常,但我们并不想在本级处理,那么我们可以将异常抛给上一级——调用者去处理该异常,就像生活中出现一些情况我们无权处理或无法处理时我们可以向我们的上级汇报一样,用例如下:
public classTest{
public static void main(String[] args){
try {
ThrowsTest(1, 1, 3);
}catch (Exception e){
e.printStackTrace();
}
}
public static void ThrowsTest(int a,int b,int c) throws Exception{
if ((a+b<c)||(a+c<b)||(b+c<a)){
throw new Exception("不能构成三角形");
}
}
}
5、必须执行的代码——finally:
在有些时候,我们需要使用一些资源并在资源使用结束后进行释放,但在使用资源的过程中可能会出现异常,但无论是否出现异常,在函数返回之前我们都需要执行资源的释放,那么我们可以将这样的代码放入finally{}中,finally表示无论是否出现异常都必须执行的代码:
try{
//可能出现异常的代码
}catch(Exception e){
//异常处理代码
}finally{
//必须执行的代码
}
6、try、catch与finally的执行顺序
在没有用到finally之前,关于异常处理的执行顺序似乎清晰:若try中没有抛出异常,则顺序执行try中的代码,若抛出异常,则中断try的执行,并进入catch进行异常处理。但当出现finally出现后,便带来了一个问题——try、catch与finally是否会冲突?例如try、catch与finally中均存在return语句,那么他们的执行顺序是什么样的?
- try、catch与finally中均存在return,但try中不抛出异常,运行结果为2
public static int test3(){
try{
return 0;
}catch (Exception e){
e.printStackTrace();
return 1;
}finally {
return 2;
}
}
- try、catch与finally中均存在return,且try会抛出异常,我们会发现存在编译错误,try中的return 0;是无法到达的语句
public static int test3(){
try{
throw new Exception();
return 0;
}catch (Exception e){
e.printStackTrace();
return 1;
}finally {
return 2;
}
}
- catch与finally中均存在return,且try会抛出异常,运行结果为2
public static int test3(){
try{
throw new Exception();
}catch (Exception e){
e.printStackTrace();
return 1;
}finally {
return 2;
}
}
综上:
当try中没有抛出异常且try、catch、finally均没有return时,执行顺序为:try——>finally——>其他代码
当try中没有抛出异常且存在return语句时,最终返回的是finally中的return,若finally中没有return,则返回try、catch代码块外的return
当try中抛出异常但均不存在return,则执行try、catch代码块外的return
当try中抛出异常且存在return语句时,最终返回的是finally中的return,若finally中没有return则返回catch中的return,若catch中没有return则返回try、catch代码块外的return
7、finally与返回值变量暂存
虽然finally会在最后执行,但要注意的是,在finally之前执行的return会放入一个暂存区域,然后再去执行finally中的语句,若finally中没有返回语句,那么最终的return的则是暂存区域的值,也就意味,finally中对返回的变量进行的操作并不会影响返回的结果.例如以下两端代码的返回值分别为1、2,而均不可能为3
public static int test1(){
int a = 1;
try {
return a;
}
catch (Exception e){
e.printStackTrace();
a = 2;
return a;
}finally {
a = 3;
}
}
public static int test2(){
int a = 1;
try {
throw new Exception();
}
catch (Exception e){
e.printStackTrace();
a = 2;
return a;
}finally {
a = 3;
}
}
8、finally造成的异常丢失
既然finally总在最后执行,那么它就有可能造成前面catch语句中新抛出的异常丢失,例如以下代码:
public class Test{
public static void test(){
try{
throw new Exception();
}catch (Exception e){
e.printStackTrace();
fun1();
}finally {
fun2();
}
}
public static void fun1() throws NullPointerException{
throw new NullPointerException();
}
public static void fun2() throws ArrayIndexOutOfBoundsException{
throw new ArrayIndexOutOfBoundsException();
}
public static void main(String[] args){
test();
}
}
运行结果如下,我们可以看到运行结果中并没有抛出NullPointerException,而是被finally中执行的fun2抛出的ArrayIndexOutOfBoundsException代替了,因此需要注意在使用finally时避免因finally导致的异常丢失
java.lang.Exception
at lzsf.TestMain.test4(TestMain.java:120)
at lzsf.TestMain.main(TestMain.java:60)
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
at lzsf.TestMain.fun2(TestMain.java:132)
at lzsf.TestMain.test4(TestMain.java:125)
at lzsf.TestMain.main(TestMain.java:60)
四、异常的匹配
既然异常也是类,那么就存在继承关系,在异常匹配的过程中,并不需要精确地匹配,即一个子类异常可以与其父类进行匹配,例如NullPointerException继承自RuntimeException,因此以下代码也能正确运行并捕获到RuntimeException。
public static void test5(){
try {
throw new NullPointerException();
}catch (RuntimeException e){
e.printStackTrace();
}
}
当一个try语句有多个catch语句存在时,会根据就近原则进行匹配,一旦找到相符合的异常,就会认为异常已经得到了控制,不需要继续往下匹配。例如以下代码永远不会执行到第二个catch处。当然如果我们把RuntimeException放到NullPointerException前面来,编译器就会报”已捕获到异常“的错误
public static void test5(){
try {
throw new NullPointerException();
}catch (NullPointerException e){
e.printStackTrace();
}catch (RuntimeException e){
e.printStackTrace();
}
}
五、自定义异常
如果我们希望创建一个自己的库所特有的异常,那么在我们可以通过继承现有的异常来创建一个新的异常,当然这个现有的异常应当与我们想要表达的意思相近,例如以下就是一个最简单的自定义异常
class MyException extends Exception {
public MyException(){}
public MyException(String msg){
super(msg);
}
}
public class Test{
public static int test(){
try{
throw new MyException();
}catch (MyException e){
e.printStackTrace();
}
}
public static void main(String[] args){
test();
}
}
运行结果:
lzsf.MyException
at lzsf.TestMain.test3(TestMain.java:109)
at lzsf.TestMain.main(TestMain.java:60)