一. 异常的概述
异常简单来说就是我们的程序出现了问题,java中提出了异常处理机制,是为了解决我们程序中出现的问题。Java中通过面向对象的思想把异常封装成了对象,对于不同的异常使用不同的类进行描述。
1.异常的体系:
Throwable类是java中所有异常的超类,字面意思就是可抛,当我的程序发生了异常,一般我们要把他抛给上一层的调用者,这个类中它有两个子类Error和Exception。
Error:这种异常非常强大,强大到我们不能够处理,一般就听天由命了,例如说VirtualMachineError这种异常代表java虚拟机崩溃或者用尽了内存,这种异常是由JVM抛出的,我们不可能在程序中给出解决办法,解决这个问题的方式就是修改我们的程序,而且可能是大改。
Exception:这种异常是表示合理的应用程序想要捕获的条件,就是程序员可处理的异常,而Exception又分为两类,一种是,叫做编译时受检测异常,这种异常我们需要用try…catch…处理,代表着可恢复;另一种是UncheckedException,也叫RuntimeException,代表运行时异常,意为大错已酿成,不可恢复。
异常体系中子类的后缀名都是父类的名字,阅读性很强。
2.可抛性
可抛性的体现是通过两个关键字来表示的,throws、throw。
throw是应用在函数里面代表着我们的代码出现了问题,我们要把他抛出,而抛出的是异常的对象。
Throws是应用在函数的声明上,用来标识我的程序可能会出现的异常,他抛出的是异常类,可以有多个。我的理解是throw是用来往我们的上一级抛异常的,但是首先我们要在我们的方法上声明我这个方法可能会出现的异常,就相当于异常的通行证,只有声明这个异常我们才可以把异常抛出去,当我们抛到上层领导那里后,领导也没有处理,他也抛出了,这时候在领导那里也要用throws声明一下。
除了声明抛出异常,我们还可以捕捉异常,就是我的程序出现了异常,我不把他往上一层调用者那里抛了,我直接在本地把他处理掉,这就使用到了try…catch…语句,下面会阐述到。
我们通过一个小示例来了解一下抛出的概念:
class ExceptionDemo {
public static void main(String[] args) {
int[] arr = new int[]{2,34,23};
//传入错误角标位3
int index = method(arr,3);
}
//定义一个方法用来返回指定数组指定角标位上的元素
public static int method(int[] arr,int index)
{
//如果传入角标越界,我们就抛出角标越界异常
if(index>arr.length-1)
//在异常的构造函数中我们可以指定信息
throw new ArrayIndexOutOfBoundsException("数组角标越界:"+index);
System.out.println("没有发生异常我就可以出现");
return arr[index];
}
}
/*
* 打印:
* Exception in thread "main"java.lang.ArrayIndexOutOfBoundsException: 数组角标越界3
at edu.heima.ExceptionDemo.method(ExceptionDemo.java:13)
atedu.heima.ExceptionDemo.main(ExceptionDemo.java:7)
* */
二.自定义异常
如果我们的程序中出现了一个异常,我觉得java中没有合适的异常类来描述,我可以自定义一个异常类来描述,例如对于角标不存在时java给我们定义了角标越界异常,而对于负数角标的情况java没有定义,这时我们可以自己定义一个类来描述这种异常。
我们自定义类是用来描述异常的,所以他一定要继承异常体系,只有这样才可以被throw和throws两个关键字所操作。我们以示例来演示说明一下:
/*
* 自定义异常类用来描述角标为负数的情况
*这个类必须要继承异常体系,只有这样才能具有可抛性
* */
//我们这里让这个类继承Exception
class NegativeIndexException extends Exception
{
//定义一个空参的构造函数,我们不用自己定义方法,父类已经给我们定义好了
NegativeIndexException()
{
}
//定义一个参数为Sting类型的构造函数,我们可以自定义信息以标识异常
NegativeIndexException(String s)
{
//在父类中已经定义好了打印自定义信息的方法,我们利用super关键字调用即可
super(s);
}
}
class ExceptionDemo {
/*主函数要调用method方法,而method方法可能会出现负数角标异常,
* 如果把这个异常抛给我,我也要声明一下这个异常
*/
public static void main(String[] args) throws NegativeIndexException {
int[] arr = new int[]{23,22,12};
//当传入角标位为-1时
method(arr,-1);
}
/*我们定义了method方法,这个方法可能会出现负数角标异常,所以我要在定义函数时把
可能出现的异常问题声明一下*/
private static int method(int[] arr, int i) throws NegativeIndexException {
//当角标位为负数时,我们抛出自定义异常类
if(i<0)
throw new NegativeIndexException("角标不可以为负数:i="+i);
System.out.println("异常发生后我就不能出现了");
return arr[i];
}
}
/*
* 打印:
*Exception in thread "main" edu.heima.NegativeIndexException: 角标不可以为负数:i=-1
atedu.heima.ExceptionDemo2.method(ExceptionDemo2.java:39)
atedu.heima.ExceptionDemo2.main(ExceptionDemo2.java:31)
**/
三.Excepiton异常的分类
在第一个示例中ArrayIndexOutOfBoundsException数组角标越界我们没有进行throws的声明,发现编译器也没有报错,但是我们自定义异常却要进行throws的声明,原因是因为数组角标越界异常继承自RuntimeException这个父类,而我们自定义的异常直接继承自Exception,我们在开头讲过,RuntimeException是运行时异常,还有一种异常叫做CheckedException编译时受检测异常,那么这两种异常的区别到底在哪?
CheckedException:在Exception中除了RuntimeException体系中的类都是编译时受检测异常。这种异常是在编译时就要进行处理的,如果你出现了这种异常而你没有进行声明(throw/throws)或者捕获(try…catch),那么编译器就会报错,这是为了让此问题在编译时就有其对应的处理方式,简而言之就是此类问题都可以被处理。例如我们在学习IO时,IOException就是此异常。
RuntimeException:Excepiton中的RuntimeException和其子类,这种异常是在运行时才会去显现出来的异常,无论你是否进行了处理,编译都不会报错。出现这种异常后,程序便会停止运行,出现的原因大多都是调用者的问题或者内部状态的改变而导致的,这种问题一般都不去处理,等到运行时让程序挂掉,因为一旦出现此异常,你的程序就被判了死刑,就需要修改代码了。所以我们一般是不需要往上层领导那里抛此类异常的,程序都已经挂了还抛出去有什么用,此类异常一般都不用throws声明抛出。
四.异常的捕捉
1.try…catch简析
其实在以前没有系统的接触异常时,我认为异常就是try…catch,try用于检测,catch用于处理,最后还加上一个finally最终执行。的确,这是异常最为常见的表现形式,我们现在来说一说异常的捕捉,也就是try…catch语句。
格式:
try
{
需要被检测的代码
}
catch(异常类 变量)
{
如果出现了异常,在这里处理
}
finally
{
最终执行的代码块
}
public class classExceptionDemo {
public static void main(String[] args) {
int[] arr = new int[]{23,34,21};
System.out.println("异常出现前");
try {
//我们打印角标3上的位置,这个角标不存在已经越界
System.out.println(arr[3]);
//异常发生后在try代码块中的其他语句不能被执行
System.out.println("异常在我头上,我还能显示吗");
}
//如果发生角标越界异常,就交由catch语句处理
catch(IndexOutOfBoundsException e) {
System.out.println("发生了角标越界异常");
}
System.out.println("异常发生后经过catch语句处理我还可以显示");
}
}
/*
* 打印:
* 异常出现前
发生了角标越界异常
异常发生后经过catch语句处理我还可以显示
**/
通过这个示例我们可以看出try…catch语句是一种预处理体系,即使我的程序发生了异常也能运行完毕,需要说一点的是这个示例中角标越界异常是运行时异常,对于这种异常我们一般是不用try…catch捕获的,因为一旦出现这种异常代表我的程序已经被判了死刑,即使运行下去也没太大意义,所以直接让他停止时最科学的选择,为了演示方便,就偷懒了一下。
2.catch中抛出异常
我们还可以在捕捉中重新把他抛出去,当我捕捉到这个异常后,我不知道如何处理那么我可以把他抛给上层领导,就是在catch语句中写入throw ,当然必须在函数上使用throws声明。
对于什么时候使用抛什么时候使用捕获,当我们可以在内部自己处理的时候我们捕捉,当我们无法处理时我们就抛掉他。需要注意的地方是有时候在面试时他们会在try语句中直接抛出异常,注意这个异常是抛给了catch,不是抛给了方法,而且在try中throw语句抛出异常后下面不能再有执行语句了,因为执行不到。
3.多catch的情况
当我们的代码中可能出现不止一种异常,我们要捕捉到的异常会有多个,这个时候就出现了多catch情况,这种情况有一个需要注意的细节,比如我们定义了多个catch语句来处理try中的代码会发生的异常,但是多个catch语句中有一个父类型catch语句,比如catch(Exception e),这时候父类异常不可以放在子类异常的前面那样编译器会报错,因为按流程语句的特点,当检测到异常后先交给第一个catch语句,如果第一个catch语句参数是父类异常,那么他就直接处理了,而不去访问下面那个具体的子类异常,所以如果多catch中出现了父类异常,一定要放在最下面。
4.finally代码块
finally代码块里面存放的代码是最终会执行的语句,这个功能一般我们用来做清理工作,因为如果有一些消耗资源的操作,比如IO、JDBC,如果我们使用完没有及时的关闭,那么我的系统内存会一直被占用,当我运行的东西越来越多,而我又不去释放掉这些不用的资源,就造成了内存泄露,异常的出现要求我们无论在什么样的情况下资源都能被及时的清理。
try…finally结构:这种结构也是保证资源正确关闭的一种手段,我们在try中书写可能出现异常的代码,然后抛给上级,但是资源必须关掉,所以出现了try…finally这种结构。
五.异常的注意事项
1.老虎苍蝇一起打
用一个Exception捕捉所有的异常,这种使用从代码角度看的确可以捕捉到所有的异常,但是很不幸的是你不知道到底异常出在什么位置上
2.隐藏异常
当我们的代码出现异常后,我利用try…catch捕捉到并处理了,但是我没有向我的调用者说明该异常发生了,这样就会让应该暴露出来的异常隐藏掉,我们在编写程序时一定要让问题清晰可见,而不是糊弄过去。
3.描述异常
我们在描述异常时一定要尽量全面的对这个异常进行描述,比如为什么会出现此异常,异常出现的位置,异常有什么解决方式等等。
4.精练try...catch
将try…catch尽可能的缩短,一定要明确哪些语句是可能产生异常的,不要什么语句都加到try中去。
5.单独处理
尽量为每一个可捕捉异常写一个try…catch,这样可以避免异常的丢失。
6.子类抛异常注意点
当子类继承了父类并复写了父类中的方法,如果父类中方法抛出了异常,那么子类就只能抛出父类异常或者此异常的子类,不能抛出比父类更多的异常。