在我们的程序设计中,对异常的处理是一种司空见惯的情况了。一个程序异常处理框架的好坏直接影响到整个项目的代码质量以及后期维护成本和难度,试想一下,如果一个项目从头到尾没有考虑过异常处理,那么当程序出现问题的时候我们怎么解决呢?所以,充分发挥异常的优点,可以提高程序的可读性、可靠性和可维护性,如果使用不当,又会严重影响到代码质量以及程序的性能。本文主要简单的介绍java异常的基础知识,然后根据《Effective java 第2版》及前人经验提供一些异常的使用原则。
本文目录如下:
1、什么是异常
2、java中异常的分类
3、如何处理异常
4、throws和throw的区别
5、自定义异常
6、finally关键字
7、继承中方法重写的异常
8、异常的总结
1、什么是异常
异常就是指程序运行中出现的非正常情况,简单的理解就是指程序运行中出现的错误,包括程序的逻辑错误和系统错误。
2、java中异常的分类
java是一种纯面向对象语言,即所谓的“万事万物皆对象”。所以,毫无疑问,java中的异常也被当做对象来处理。在整个java异常体系中,java.lang.Throwable类是基类,在它下面定义有许许多多异常类,比如我们非常熟悉的NullPointerException、IndexOutOfBoundsException、IllegalArgumentException、IOExeption、SQLException、OutOfMemoryError等。这些异常又被总的分成两大类:Error和Exception。
java异常体系结构图
其中Error不用我们关心。出现Error这种问题一般都是很严重的,比如说内存溢出(OutOfMemoryError)。所以JVM会选择终止程序的运行。
Exception就是我们编程中常见的异常了,它也被分成了两大类:checked exception和unchecked exception。
unchecked exception(未检查的异常)也被称为运行时期异常,即RuntimeException,java编译器不要求必须进行异常捕获处理或者抛出声明,由我们自行决定,但是我们一般都不对这种异常进行处理,因为这种异常的出现常常是由于我们代码不够严谨所引起的。编程中常见的运行时期异常有NullPointerException、IndexOutOfBoundsException、IllegalArgumentException等。
checked exception(受检查的异常)也被称为编译时期异常(所有继承自Exception且不是RuntimeException及其子类的异常都是编译时期异常),java编译器强制我们必须进行处理的异常,如果不对编译时期异常进行处理的话将不能通过编译。编程中常见的编译时期异常有IOExeption、SQLException等。
3、如何处理异常
在java中,如果我们的程序出现了异常,而且我们又没有对异常进行任何处理的话,JVM就会对异常进行默认的处理,即简单的在控制台中输出异常的名称、原因、位置等信息。同时JVM会终止我们的程序。
package com.gk.exception;
public class ExceptionDemo1 {
public static void main(String[] args) {
fun();
}
public static void fun() {
int[] array = {1, 2, 3, 4, 5}; // 定义一个数组,并给数组赋值
int length = array.length;
System.out.println(array[length]);
System.out.println("结束啦...");
}
}
可以看到,当出现异常后,下面的语句(System.out.println("结束啦..."))就不执行了。但这显然不是我们期望的结果。我们希望的是当我们的程序出现异常后还能继续往下执行。那么有什么方法可以帮助我们解决呢?
在java中提供了两种方式帮助我们处理异常:捕获(try…catch…finally)和抛出(throws或throw)
捕获异常的具体格式为:
try{
//有可能出现异常的代码。
}
catch(异常类名 变量名){
//处理异常的代码。
}
......//表示可以有多个catch语句
finally{
//一般情况下一定会被执行的代码。
}
其中try是必须要有的,catch和finally都是可选的,但是两者必须要有一个。catch也可以有多个。
捕获异常的运行机制为:首先执行try中的代码块,一旦遇到了异常,JVM就会创建相应的异常对象,然后就近匹配catch的异常类型,如果匹配不成功(找不到相应的异常或其父异常),则执行finally的代码块(可选,如果有finally就执行),然后结束程序,并在控制台简单打印异常的堆栈信息;如果匹配成功,则执行相应的catch代码块后继续往下执行程序。
package com.gk.exception;
public class ExceptionDemo2 {
public static void main(String[] args) {
fun();
}
public static void fun() {
int[] array = {1, 2, 3, 4, 5}; // 定义一个数组,并给数组赋值
int length = array.length;
try {
System.out.println(array[length]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("出错啦,数组下标越界...");
}
System.out.println("结束啦...");
}
}
从运行结果可以看到捕获异常之后下面的语句依然可以得到执行,不过需要注意的是,如果有多个catch,父类异常的catch要写在子类异常catch的后面,不然的话子类异常catch得不到执行也会报错。
虽然不要求catch里面一定要有语句,但是如果catch里面是空的话就等于隐藏了该异常,就会导致我们不清楚异常的情况,也会给我们后期的维护增强了困难。这种情况还不如不要捕获该异常。
这里还需要注意一点的是try里面不要有太多的语句,因为try里面的代码是要走异常处理机制的,所以JVM就会开辟一些新的资源来管理这些代码。如果代码越多所开辟的资源也就越多,效率也就越低下。所以建议只把有可能出现异常的代码放在try里面。
try…catch…finally...异常捕获机制一般是应用于我们知道该怎样处理异常的情况,但是我们也有可能遇到了不知道如何处理的异常,这时候java给我们提供了另一种异常处理的方式:“抛出异常”,然后让方法的调用者去处理。
package com.gk.exception;
public class ExceptionDemo3 {
public static void main(String[] args) {
try{
fun(); // 调用fun()方法并处理异常
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("出错啦,数组下标越界...");
}
System.out.println("结束啦...");
}
// 在fun()方法中声明此方法有可能会抛出ArrayIndexOutOfBoundsException
public static void fun() throws ArrayIndexOutOfBoundsException{
int[] array = {1, 2, 3, 4, 5}; // 定义一个数组,并给数组赋值
int length = array.length;
System.out.println(array[length]);
}
}
上面的例子,在fun()方法中,当试图访问array[array.length]的时候,就会出现ArrayIndexOutOfBoundsException异常,此时我们不处理该异常,而是把它给抛出去了,在main()方法调用的fun()方法的时候再进行捕获处理。
当然也可以采用throw关键字来手动抛出异常对象。
package com.gk.exception;
public class ExceptionDemo4 {
public static void main(String[] args) {
try{
fun();
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("出错啦,数组下标越界...");
}
System.out.println("结束啦...");
}
public static void fun() {
int[] array = {1, 2, 3, 4, 5}; // 定义一个数组,并给数组赋值
int length = array.length;
if (length == 5)
throw new ArrayIndexOutOfBoundsException();
}
}
当length ==5的时候,我们就抛出ArrayIndexOutOfBoundsException异常对象,然后main()方法调用fun()方法的时候再处理该异常。
其实main()方法也可以选择继续把异常抛出给JVM处理,但是一般不建议这样做。
上面已经介绍了捕获异常(try...catch...finally...)和抛出异常(throws或throw)的用法,但是还是有必要再总结一下,究竟我们什么时候用try...catch...finally...,什么时候用throws(或者throw)呢?一个普遍的处理原则是:如果该异常我们自己能够处理就用try...catch...finally...,否则就将异常抛出,并用throws告知方法的调用者该方法可能会出现的异常,让调用者去处理该异常。
4、throws和throw的区别
可能初学者对throws和throw这两个“抛出”理解并不是那么深刻,总是会搞混,下面说说这两者的区别。
throws表示抛出异常,由该方法的调用者来处理。用在方法声明后面,跟的是异常类名,可以跟多个异常类名,中间用逗号隔开。需要注意的是throws只是表示出现异常的一种可能性,并不一定会发生这些异常。
throw表示抛出异常,由方法体内的语句处理。用在方法体内,跟的是异常对象名,而且只能抛出一个异常对象。执行throw则一定抛出了某种异常。
5、自定义异常
在java中,不必拘束于已有的异常类,java异常体系中提供的类不可能完全预见我们程序中所有可能出现的错误。所以我们完全可以自定义异常类来处理我们程序中可能出现的异常。
但并不是随便写一个类就可以的,需要我们继承Exception或者RuntimeException。类里面也很简单,只需写一个无参的构造器和一个带参的构造器即可。(参考java官方文档,大部分的异常类都只是提供了构造器)
// 继承自Exception
package com.gk.exception;
public class MyException extends Exception{
public MyException() {
super();
}
public MyException(String msg) {
super(msg);
}
}
//继承自RuntimeException
package com.gk.exception;
public class MyRuntimeException extends RuntimeException{
public MyRuntimeException() {
super();
}
public MyRuntimeException(String msg) {
super(msg);
}
}
继承自Exception的自定义异常属于checked exception,即编译时期异常,所以在方法内部抛出此自定义异常的时候方法的调用者必须对此异常进行处理;
继承自RuntimeException的自定义异常属于unchecked exception,即运行时期异常,所以在方法内部抛出此自定义异常的时候方法的调用者可以不用对此异常进行处理。
// 测试继承Exception的自定义异常
package com.gk.exception;
import java.util.Scanner;
public class TestException {
public static void main(String[] args) {
try {
fun(); // 抛出的是编译时期异常,所以必须对异常进行处理,不然的话将不能通过编译
} catch (MyException e) {
e.printStackTrace();
}
}
public static void fun() throws MyException{
System.out.print("请输入一个不是47的数:");
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
scanner.close(); // 释放资源
if (n == 47)
throw new MyException("输入的数不能是47"); // 自定义异常需要我们自己抛出。
else
System.out.println("你输入的数是:" + n);
}
}
// 测试继承RuntimeException的自定义异常
package com.gk.exception;
import java.util.Scanner;
public class TestRuntimeException {
public static void main(String[] args) {
fun(); // fun()方法抛出的是运行时期异常,可以不对异常进行处理
}
public static void fun() throws MyRuntimeException{
System.out.print("请输入一个不是47的数:");
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
scanner.close(); // 释放资源
if (n == 47)
throw new MyRuntimeException("输入的数不能是47"); // 自异常异常需要我们自己抛出。
else
System.out.println("你输入的数是:" + n);
}
}
请注意观察两种异常处理方式的不同,测试结果都是一样的。
需要注意的是JVM不会帮我们抛出自定义异常,所以往往需要我们自己抛出。
虽然我们可以自定义异常,但是还是建议优先使用标准的异常。(《Effective java 第2版》第60条)
6、finally关键字
特点:一般情况下finally里面的语句体一定会被执行,除非在finally之前JVM强制退出了。下面请看两个例子:
package com.gk.exception;
public class TestFinally {
public static void main(String[] args) {
fun();
}
public static void fun() {
int x = 10;
int y = 0;
int result = 0;
try{
result = x / y;
System.out.println(x + " / " + y + " = " + result);
}catch(ArithmeticException e){
System.out.println("出错啦,除数不能为0...");
return ;
}finally{
System.out.println("finally...");
}
System.out.println("结束啦...");
}
}
package com.gk.exception;
public class TestFinally2 {
public static void main(String[] args) {
fun();
}
public static void fun() {
int x = 10;
int y = 0;
int result = 0;
try{
result = x / y;
System.out.println(x + " / " + y + " = " + result);
}catch(ArithmeticException e){
System.out.println("出错啦,除数不能为0...");
System.exit(0);
}finally{
System.out.println("finally...");
}
System.out.println("结束啦...");
}
}
上面两个例子唯一的区别就是catch块中一个有return语句,另一个包含System.exit(0)语句,从运行结果中我们就可以大概知道finally的特性了。
一般情况:
在包含return语句的例子中,即使return语句在finally块之前,但是finally块的语句体还是得到了执行。说明finally块的语句是在return语句之前执行的。不过由于return语句在System.out.println("结束啦...")语句之前,所以System.out.println("结束啦...")就不执行了,通过对比更加说明了finally语句一定会执行。
特殊情况:
在包含System.exit(0)语句的例子中,由于当前正在运行的JVM被强制终止了,所以在System.exit(0)语句后面的语句全都得不到执行,当然也包含finally块。
分析了finally的特点之后,我们真正关心的应该是finally有什么用?对吧,实际上,finally一般是用于释放资源的,比如IO流的操作、网络连接或是JDBC的操作等。
7、继承中方法重写的异常
关于继承中方法重写时方法体内出现异常的情况,请看下面结论:
1)、子类重写父类方法时,子类方法只能抛出与父类方法相同的异常,或父类异常的子异常,或不抛出异常。子类方法抛出的异常不能超过父类方法异常的范畴。
2)、如果被重写的方法没有异常抛出,那么子类的方法不能抛出异常,如果此时子类方法内部发生了异常,那么子类方法只能try,不能throws。
注意:以上结论只针对编译时期异常(checked exception),运行时期异常不受影响。有关编译时期异常和运行时期异常的区别请看上面“java中异常的分类”这一小节。
8、异常的总结
下面根据《Effective java 第2版》及前人经验提供一些有效的异常使用原则,至于为什么,这里就不给出具体的解释了,还是建议读者多看书,多思考,勤动手。
1)、只针对有异常的情况才使用异常
2)、对可恢复的情况使用编译时期异常,对编程错误使用运行时期异常
3)、优先使用标准的异常
4)、避免不必要的使用编译时期异常
5)、如果可以,请将编译时期异常转化为运行时期异常
6)、在细节消息中包含能捕获失败的信息
7)、try块里面的语句越少越好,不要将不会出现异常的语句放在try里面
8)、catch块什么情况下都不要为空,哪怕只是输出简单的一句提示也行
9)、finally用于关闭或释放资源
10)、不要忽略异常