JAVA入门 - 异常处理机制

异常

何为谓常,不正常也,程序发生异常,即程序发生错误以致不能正常运行。比如空指针,数组越界,类型转换错误等等。JAVA中设计了一系列类用于处理程序异常,下图为JAVA的异常类体系结构。

Java异常类层次结构

由上图可以看出:

  • 在JAVA中所有的异常都有一个共同的祖先Throwable。
  • Throwable有两个重要的子类:Error(错误)及Exception(异常)。

那么接下来针对Error及Exception进行详细分析

Error(错误):程序运行过程中产生严重问题,以及程序本身已经无法处理。比较常见的如OutOfMemoryError(内存溢出),JVM运行内存不足而产生的错误。
这类错误更多是外因造成,程序本身无法处理,在代码中不会体现出来,而是在运行过程中发生,JVM一般选择线程终止的处理方法。

Exception(异常):一般都是程序代码本身的问题,内因造成。常见的如NullPointException(空指针异常),ArrayIndexOutOfBoundsException(数组越界异常)等。只要在编码过程中,添加相应的判断处理,可以尽量避免。

RuntimeException(运行时异常):Exception类的一个非常重要的子类,这类异常不处理并不会引起编译不通过,但是运行时会报错,JVM自动抛出。

String str = null;
str.equals("123");

以上代码为例,程序运行至第二句会抛出NullPointException。这类异常发生基本都是因为程序编写不严谨的原因,所以需要代码编写者将代码编写地更加严谨。以上代码应该添加对null值的判断

String str = null;
if (str != null) {
    str.equals("123");
}

当然,更好的写法为

String str = null;
"123".equals(str);

与RuntimeException(运行时异常)对应的就是非运行时异常,这类异常,程序中必须对其进行处理,不然编译将不能通过,比如IOException、SQLException等以及用户自定义异常。


异常处理

先看一个程序异常的例子

public class TestException {  
    public static void main(String[] args) {  
        int a, b;  
        a = 6;  
        b = 0; // 除数b 的值为0  
        System.out.println(a / b);  // 1
    }  
}

运行结果:

Exception in thread “main” java.lang.ArithmeticException: / by zero
at Test.TestException.main(TestException.java:8)

可以看出,程序运行至语句[1]处,因为“除数为0”引发程序异常,因为并没有对该异常进行任何处理,所以程序直接报错,显示产生了ArithmeticException异常。但在实际的项目过程中是要杜绝这种错误发生的,即使有异常发生,也不应该让程序直接显示系统错误信息,而应该对错误信息进行封装,显示一个让正常人可以阅读的错误信息。

在 Java 应用程序中,异常处理机制有两种:抛出异常,捕捉异常。

抛出异常

public void cast() throws Exception {
    // do something
}

在方法声明时使用throws关键字进行异常抛出。

意味着该方法中发生异常时,方法自己并不处理,而是将异常信息封装成异常对象,然后向上抛出给调用该方法的其它方法进行处理,如果调用方法也不进行处理,可以继续向上抛出,直到最后有方法对该异常信息进行捕捉处理,如果最后都没有任何地方对该异常进行捕获处理,则还是会系统报错。

捕捉异常

try {
    // do something throw a Exception
} catch (Exception e) {
    // TODO: handle exception
}

使用try-catch语句进行异常捕捉
使try关键字监控其后的代码块,如果发生异常,则将异常抛出,自动去匹配异常处理器catch中声明的异常类型,如果产生的异常跟catch中声明的类型相同,或者是其子类,则异常被捕获,程序转而执行catch异常处理器中的语句,程序继续正常运行。如果异常未能被捕获,该异常就应该继续向上抛出,交由调用的方法进行处理,否则程序将终止运行,报系统错误信息。

例如:

try {
    System.out.println("before exception happen");  // 1
    int i = 1/0;  // 2
    System.out.println("after exception happen");   // 3
} catch (Exception e) {
    System.out.println("catch exception success");  // 4
}

当程序执行至2位置时,就会产生一个ArithmeticException的运行时异常,语句3被放弃执行,ArithmeticException是Exception的一个子类,异常被捕捉,转而执行异常处理器中的语句4,所以最终输出为

before exception happen
catch exception success

try-catch

在上面的例子中,其实已经实现了一个最简单的try-catch语句捕获异常,try-catch语句的语法为

try {  
    // 可能会发生异常的程序代码  
} catch (Type1 id1){  
    // 捕获并处置try抛出的异常类型Type1  
} catch (Type2 id2){  
     //捕获并处置try抛出的异常类型Type2  
}

关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域。Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。

匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。

例1:

public class TestException {  
    public static void main(String[] args) {  
        int a = 6;  
        int b = 0;  
        try { // try监控区域
            if (b == 0) throw new ArithmeticException(); // 1 通过throw语句抛出异常  
            System.out.println("a/b的值是:" + a / b);  // 2
        } catch (ArithmeticException e) { // catch捕捉异常  
            System.out.println("程序出现异常,变量b不能为0。");    // 3
        }  
        System.out.println("程序正常结束。");   // 4
    }  
} 

运行结果:

程序出现异常,变量b不能为0。
程序正常结束。

分析:程序主动创建了一个ArithmeticException异常,并将该异常抛出,系统经寻找发现有可以处理ArithmeticException异常的异常处理器,程序执行异常处理器中代码,打印“程序出现异常,变量b不能为0。”,try-catch语句结束,程序继续向后运行,打印“程序正常结束。”。

例2:

public static void main(String[] args) {  
        int a = 6;  
        int b = 0;  
        try {  
            System.out.println("a/b的值是:" + a / b);  // 1
        } catch (ArithmeticException e) {  
            System.out.println("程序出现异常,变量b不能为0。");   // 2 
        }  
        System.out.println("程序正常结束。");  //3
    }  
}

运行结果:

程序出现异常,变量b不能为0。
程序正常结束。

分析:当程序运行至语句 [1] 位置时,因为”除数为0“错误引发运行时异常,因此语句[1]并没有执行成功,不能输出打印结果。系统将异常信息封装成ArithmeticException异常实例,并抛出。异常抛出后的处理过程跟[例1]中一样,所以输出了同样的结果。

但是因为使用try-catch语句会产生一定的消耗,大量使用会造成程序性能下降,所以能够通过添加条件判断消除异常的情况,尽量不要使用try-catch语句,大部分运行时异常都应该通过这种方法来规避。

public static void main(String[] args) {  
        int a = 6;  
        int b = 0;  
        if (b != 0) {  // 使用条件判断,而不是try-catch
            System.out.println("a/b的值是:" + a / b);  //            1 
        }
        System.out.println("程序正常结束。");  //3
    }  
}

例3:

public static void main(String[] args) {  
        int a = 6;  
        int b = 0;  
        try {  
            System.out.println("a/b的值是:" + a / b);  // 1
        } catch (Exception e) {  
            System.out.println("使用父类Exception进行异常捕获");   // 2
        } catch (ArithmeticException e) {  
            System.out.println("程序出现异常,变量b不能为0。");   // 3 
        } 
        System.out.println("程序正常结束。");  //3
    }  
}

运行结果:

使用父类Exception进行异常捕获
程序正常结束。

分析:
这个例子与上个例子的不同之处在于有多个catch语句,第一个声明可捕获Exception类型异常,第二个声明可捕获ArithmeticException类型异常。当程序运行至语句[1]处,产生一个ArithmeticException异常,可以看出,两个catch语句都可以进行捕获,但系统并不会依次执行,而是查找第一个可以处理该异常的异常处理器,所以会执行语句[2],而不会执行语句[3]。

这个例子有一个潜在的问题,因为ArithmeticException是Exception的子类,而捕获Exception的语句声明在捕获ArithmeticException的语句前面,也就是说所有的ArithmeticException异常肯定会被前面的Exception异常处理器给捕获,而不会流转到ArithmeticException异常处理器,所以后面的catch语句实际上是没有任何意义的,因为她不可能接收到任何的异常。

所以在编码过程中,应该将子类的处理器放在最前面声明,而父类的处理器放在后面声明。

try-catch-finally

try-catch语句其实还有一个兄弟,就是finally,直接翻译过来就是最终的意思,就是不管前面的try-catch怎么处理,最终都要执行我,实际上finally的语义就是这个意思。
他们的语法形式为

try {  
    // 可能会发生异常的程序代码  
} catch (Type1 id1) {  
    // 捕获并处理try抛出的异常类型Type1  
} catch (Type2 id2) {  
    // 捕获并处理try抛出的异常类型Type2  
} finally {  
    // 无论是否发生异常,都将执行的语句块  
}

还是以之前的代码进行改装,添加finally块

public static void main(String[] args) {  
        int a = 6;  
        int b = 0;  
        try {  
            System.out.println("a/b的值是:" + a / b);  // 1
        } catch (ArithmeticException e) {  
            System.out.println("程序出现异常,变量b不能为0。");   // 2 
        } catch (Exception e) {  
            System.out.println("使用父类Exception进行异常捕获");   // 3
        } finally {
            System.out.println("--------------------------");
        }
        System.out.println("程序正常结束。");  //3
    }  
}

运行结果:

程序出现异常,变量b不能为0。
--------------------------
程序正常结束。

分析:
try-catch阶段的运行过程,我们已经知晓,根据运行结果可以看出,在try-catch运行完成之后,执行了finally块中的代码,然后整个try-catch-finally语句执行完成,程序继续运行后面的语句。

小结:

  • try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
  • catch 块:用于处理try捕获到的异常。
  • finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:

1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序。
3)程序所在的线程死亡。
4)关闭CPU。

一个复杂的例子:

package Test;  

public class TestException {  
    public TestException() {  
    }  

    boolean testEx() throws Exception {  
        boolean ret = true;  
        try {  
            ret = testEx1();  
        } catch (Exception e) {  
            System.out.println("testEx, catch exception");  
            ret = false;  
            throw e;  
        } finally {  
            System.out.println("testEx, finally; return value=" + ret);  
            return ret;  
        }  
    }  

    boolean testEx1() throws Exception {  
        boolean ret = true;  
        try {  
            ret = testEx2();  
            if (!ret) {  
                return false;  
            }  
            System.out.println("testEx1, at the end of try");  
            return ret;  
        } catch (Exception e) {  
            System.out.println("testEx1, catch exception");  
            ret = false;  
            throw e;  
        } finally {  
            System.out.println("testEx1, finally; return value=" + ret);  
            return ret;  
        }  
    }  

    boolean testEx2() throws Exception {  
        boolean ret = true;  
        try {  
            int b = 12;  
            int c;  
            for (int i = 2; i >= -2; i--) {  
                c = b / i;  
                System.out.println("i=" + i);  
            }  
            return true;  
        } catch (Exception e) {  
            System.out.println("testEx2, catch exception");  
            ret = false;  
            throw e;  
        } finally {  
            System.out.println("testEx2, finally; return value=" + ret);  
            return ret;  // 1
        }  
    }  

    public static void main(String[] args) {  
        TestException testException1 = new TestException();  
        try {  
            testException1.testEx();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

运行结果会是什么呢?
如果把语句[1]注释,运行结果又会是什么?

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值