Java中的异常处理
异常概述
Java的基本设计思想是“Badly formed code will not be run!”。这句话的大致意思是:错误形式的代码不会被运行。
我们在写代码的时候,提升错误恢复能力是提升代码健壮的重要措施。而“为了创建一个更加健壮的系统,那么每一个组件都必须是健壮的”。从而,在Java中出现了异常处理机制。
不像C语言,基本处理错误的代码都是程序员写上去的,而在Java中,除非是要自己自定义异常的时候,我们一般都是通过异常处理代码块来解决问题的。不但提高了代码的健壮性,还提高了代码的可读性。
那么,异常处理的定义是什么呢?当程序运行时出现了异常(不是错误),可能是空指针异常等等很多异常,能够对当前出现异常的代码进行处理,或是直接报告异常,或是将异常抛给特定的位置进行决断处理。
同大多数的需求一样,异常处理也被设计者设计成了一个类:Throwable。在这个类的下面,又有Error(错误)、和Exception(异常)。Error(错误)一般情况下不会通过代码进行处理,因为一般能报错误的情况,都是十分严重的情况,大多数错误都是由JVM(Java虚拟机)引起的。例如下面的代码:
byte[] buf = new byte[1024*1024*1024];
System.out.println(buf);
执行这段代码,肯定是会报错的。原因如下:
JVM默认情况下只管理了64M的内存,而我们的程序需要的是1G的内存,很显然已经超出了管理范围(内存溢出),所以会报错。
在Exception(异常)类下,又有RunTimException(运行时异常)以及费运行时异常。这里的“……类下”指的是继承关系。
下面,我会逐个类介绍,并且会附上相应的代码供大家参考。
Throwable类
基本方法
1. toString() 输出该异常的类名
2. getMessage() 输出异常的信息,需要通过构造方法传入异常信息,也就是new一个对象的时候传入的参数(手动滑稽)
3. printStackTrace() 打印栈信息。
代码片段如下:
Throwable able = new Throwable("恶心!!!");
System.out.println(able.toString()); // 输出该异常的类名
System.out.println(able.getMessage()); // 输出异常的信息
able.printStackTrace(); // 打印栈信息
运行结果如下:
那么,我们来看一个出现异常的例子:
这个函数传入的参数的y值可能是0,程序会出现异常并停止
public static void div(int x, int y) {
System.out.println(x / y);
System.out.println("除法运算");
}
那么对于这种情况,我们应该如何进行处理呢?这就正式引入了我们要讨论的话题,异常处理的方式。首先我们来介绍第一种。
1.try{//可能发生异常的代码 }catch(异常类 变量名){//处理}。
我们以上面那个除法运算函数作为最基本的例子。当我们没有进行异常处理的时候,程序遇到问题时会停止。进行了异常处理时,程序还会继续执行,并且会按照我们给出的格式进行报错。
try{
div(10,0);
}catch(Throwable t){
System.out.println(t.toString());
System.out.println(t.getMessage());
t.printStackTrace();
System.out.println();
System.out.println("除数不能为0");
}
程序运行的结果如下图所示:
那么,一个异常的处理解决了,我们该如何进行多个异常的处理呢?
多个异常的处理
为了实现多个异常的处理情况,这里我们使用最简单的方法:设定一个数组。代码如下:
public class Exception {
public static void main(String[] args) {
int[] arr = new int[] {1,2};
div(1,0,arr);
}
public static void div(int i,int j,int arr[]) {
try {
System.out.println(arr[1]);
System.out.println(i / j);
}catch(ArithmeticException e) {
e.toString();
e.getMessage();
e.printStackTrace();
System.out.println("******算术异常*******");
}catch(ArrayIndexOutOfBoundsException e) {
e.toString();
e.getMessage();
e.printStackTrace();
System.out.println("******数组角标越界*******");
}catch(NullPointerException e) {
e.toString();
e.getMessage();
e.printStackTrace();
System.out.println("******空指针异常*******");
}
}
}
所以,当执行上述代码的时候,将会出现算数异常,也就是ArithmeticException。
如果将main函数中的代码换成这个样子:
public void main(String[] args){
int[] arr = new int[] {1,2};
arr = null;
div(1,0,arr);
}
这时将会出现空指针异常,也就是NullPointerException ,原因很简单,我们已经将arr数组置为null,所以访问的时候肯定是会出现空指针异常的。
如果我们再将div中的代码改成下面这个样子:
System.out.println(arr[3]);
数组中一共有两个值,0、1,我们访问的是3,很显然是数组角标越界,也就是ArrayIndexOutOfBoundsException 。
总结
1. 程序中可能有多个语句发生异常,可以同时放在try中。如果某条语句发生异常的时候,程序将会对catch中的异常进行匹配,如果能够匹配上,则执行相应的catch中的代码,如果没有匹配上,程序停止。
2. 如果程序中真的出现了多个异常,则只会执行try代码片段中的第一个出现异常的语句的异常处理语句,剩余的异常不会再处理。
使用多态进行异常处理
什么是多态呢?我们之前肯定学过,简单来讲,就是“用父类的引用指向子类对象”,我简单解释一下,看下面的代码:
Father f = new Son();
在这里,Son类是继承与Father类的,所以用Father的引用f来指向Son类的对象就是这个意思了,如果还不明白的话,就再好好看看前面的内容。
具体操作代码如下:
try {
System.out.println(arr[1]); // 数组越界
System.out.println(x / y); // 除零
Son s = (Son) f; // 类型转换
} catch (Exception e) {
e.toString();
e.getMessage();
e.printStackTrace();
System.out.println("出现错误");
}
上面的代码很简单,就是说当无论出现什么错误的时候,都只用一个Exception来匹配,我们知道,其实各种各样的异常都是继承于Exception类的,所以可以用Exception的引用指向具体的异常对象,如NullPointerExeception等。
多个catch语句之间的执行顺序
1. 按照顺序执行,从上到下
2. 如果catch的异常有继承关系,则:当子类异常在上,父类异常在下的时候,编译正常。也就是说,如果NullPointerException在上,Exception在下的时候,满足这一条;当子类异常在下,父类异常在上的时候,比如NullPointerException在下,Exception在上,那么就会编译报错,因为子类异常无法catch到。所以,多个异常需要使用子类父类的顺序进行使用。
注意:处理异常应该catch异常具体的子类,可以处理的更具体,不要为了简化代码使用异常的父类。
抛出处理
除了try……catch这种方法进行异常处理外,我们还可以使用抛出处理异常。
看下面的这个方法:
public static void div(int x, int y) throws Exception { // 声明异常,通知方法调用者。
if (y == 0) {
throw new Exception("除数为0"); // throw关键字后面接受的是具体的异常的对象
}
System.out.println(x / y);
System.out.println("除法运算");
}
上面的例子就是抛出处理,使用throw关键字,注意,在方法中使用的是throw,而在方法名上使用的是throws关键字,这个很好理解,有点类似于英文中的复数,一个方法中抛出的异常很有可能不止一个,所以使用throws方法。
public static void main(String[] args) {
try {
div(2, 0);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("over");
}
调用了div方法后,我们需要对这个方法抛出的异常进行处理,这又涉及到了try……catch块。如果不对抛出的异常进行处理,那么编译不会通过。在main函数上依然可以即系抛出,这样就是交给了JVM进行处理了,肯定是不推荐这样做的。
throw和throws的区别
1. 相同:都是用于做异常的抛出处理的。
2. 不同点:
使用的位置: throws 使用在函数上,throw使用在函数内
后面接受的内容的个数不同:
throws 后跟的是异常类,可以跟多个,用逗号隔开。
throw 后跟异常对象。
自定义异常
当现有异常体系中的异常无法满足我们的需求的时候,我们就需要自定义异常。
根据上面的介绍,我们知道,所有的异常都是继承于父类Exception,所以,我们自定义异常也是继承Exception就可以了。
class NoRiceException extends Exception {……}
这个Exception是什么意思呢?
字面意思就是没有米饭的异常,嘿嘿。
我们去食堂买饭,就说要买米饭(Rice),然后卖饭的阿姨告诉你,米饭卖没了,这就是NoRiceException,没有米饭的异常,这个异常在现有的异常体系中肯定是没有的。
下面再来介绍一个大头:
运行时异常和非运行时异常
- RuntimeException(运行时异常)
RuntimeException的子类有:
- ClassCastException————–多态中,可以使用Instanceof 判断,进行规避
- ArithmeticException————–进行if判断,如果除数为0,进行return
- NullPointerException————-进行if判断,是否为null
- ArrayIndexOutOfBoundsException————使用数组length属性,避免越界
这些异常时可以通过程序员的良好编程习惯进行避免的
- 非运行时异常(受检异常)
如果出现了非运行时异常必须进行处理throw或者try{}catch(){}处理,否则编译器报错。
- IOException 使用要导入包import java.io.IOException;
- ClassNotFoundException
- Sun 的API文档中的函数上声明异常,那么该异常是非运行是异常,
调用者必须处理。 - 自定义异常一般情况下声明为非运行时异常
总结
1. 子类覆盖父类方法时,父类方法抛出异常,子类的覆盖方法可以不抛出异常,或者抛出父类方法的异常,或者该父类方法异常的子类。
2. 父类方法抛出了多个异常,子类覆盖方法时,只能抛出父类异常的子集
3. 父类没有抛出异常子类不可抛出异常
4. 子类发生非运行时异常,需要进行try{}catch的(){}处理,不能抛出。
5. 子类不能比父类抛出更多的异常
接下来我们来看异常处理的最后一部分,finally
finally
我们知道,当程序出现异常的时候,经过异常处理,程序会停止执行,所以,在处理完异常以后,后续的代码将不会执行,这样肯定是有一定问题的,比如,我们调用了资源,需要关闭这个资源的时候,程序停止了,资源却仍然被占用着,这样是不是很有问题啊。为了处理这样的问题,就要使用finally块。
finally就是必须执行的代码。
1. 出现问题的情况:
try{ // 可能发生异常的代码 } catch( 异常类的类型 e ){ // 当发生指定异常的时候的处理代码 }catch…
比较适合用于专门的处理异常的代码,不适合释放资源的代码。
2. 实现方式一:
try{ } catch(){} finally{ // 释放资源的代码 }
finally块是程序在正常情况下或异常情况下都会运行的。
比较适合用于既要处理异常又有资源释放的代码
3. 实现方式二
try{ }finally{ // 释放资源 }
比较适合处理的都是运行时异常且有资源释放的代码。
4. finally:关键字主要用于释放系统资源。
- 在处理异常的时候该语句块只能有一个。
- 无论程序正常还是异常,都执行finally。
5. finally是否永远都执行?
- 只有一种情况,但是如果JVM退出了System.exit(0),finally就不执行。
- return都不能停止finally的执行过程。
以上,就是异常处理的全部内容。