异常
何为谓常,不正常也,程序发生异常,即程序发生错误以致不能正常运行。比如空指针,数组越界,类型转换错误等等。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]注释,运行结果又会是什么?