目录
本文有较多补充,详见笔者的另外一篇博客【Java】异常处理
一、异常定义
通俗来讲,异常就是在程序运行过程中出现的不正常的情况,此时需要有相应的手段去处理产生的异常,若没有进行处理,则整个程序会因异常而终止。
异常有许多种,如IOException(输入输出异常),SQLException(SQL数据库操作异常),ClassNotFoundException(类未找到异常),ArrayIndexOutOfBoundsException(数组下标越界)等,下面以ArrayIndexOutOfBoundsException为例来看看异常长什么样子。
ArrayIndex类定义
public class ArrayIndex
{
public static void main(String[] args)
{
int[] a=new int[10];
a[10]=5;
for(int i=0;i<a.length;i++)
{
System.out.print(a+" ");
}
}
}
运行结果
可以看到,程序出现了一个ArrayIndexOutOfBoundsException异常,这是因为长度为10的数组下标范围为0-9,而我对a[10]做了赋值,因此下标越界,此时产生的异常没有相应的处理程序,整个程序终止。
二、try-catch语句
然而,当异常发生时,并不意味着程序必须终止,而是需要以合适的方式来处理异常,这也是程序健壮性的重要体现。
通常情况下,我们会使用try-catch语句来捕捉产生的异常并加以处理。
try-catch语句的一般写法
try
{
//需要执行的语句
}
catch (Exception e)
{
//捕捉到异常后进行处理
e.printStackTrace();
}
//可在此写多个catch语句来分别处理不同类型的异常
这里注意,所有的异常都是一个异常类,且它们都是Exception类的子类,而catch语句在捕捉异常时不只会捕捉括号里的类,也会捕捉括号里类的父类,因此如果括号里写的是Exception类,则会捕捉到所有类型的异常,下面再写catch语句就没有意义了,因为根本不可能用得上(实际编译器也不会让这样写)。这里的机制看不懂没关系,本文下面会有例子详细介绍
回到try-catch语句本身,这里的意思是try里写你想要执行的语句,但这些语句可能会出现异常,catch语句就是用来捕捉这些异常并加以处理的,仍以ArrayIndexOutOfBoundsException异常为例:
ArrayIndex类
public class ArrayIndex
{
public static void main(String[] args)
{
try
{
//正常执行语句
int[] a=new int[10];
a[10]=5;
for(int i=0;i<a.length;i++)
{
System.out.print(a+" ");
}
}
catch (ArrayIndexOutOfBoundsException a)
{
//捕捉到异常后进行处理
System.out.println("数组下标越界!");
a.printStackTrace();
}
}
}
运行结果
可以看到,catch语句确实捕捉到了 ArrayIndexOutOfBoundsException异常,并执行了println语句,红色字体是a.printStackTrace()方法的输出,这个方法是用来显示异常传递堆栈的,这个在第三节会细说。(返回代码为0,说明程序是正常结束的,不是因异常而终止,这就是异常处理的意义所在:在出现异常时,只需进行合适的处理,而不是直接使整个程序终止)
在try-catch语句之后还可以接一个finally,即try-catch-finally语句,finally的大括号里写的语句表示无论发不发生异常都要执行(这部分常用于资源回收或关闭,如数据库工具类中关闭Connection对象)
三、异常捕捉机制
1.异常传递和捕捉机制
本节解决一个问题:异常从产生开始,是如何传递的以及在何处被捕捉的?
我们来看代码
public class ArrayIndex
{
public static void f()
{
int[] a=new int[10];
a[10]=5;
System.out.println("我是f()");
}
public static void g()
{
f();
}
public static void h()
{
int i=10;
if(i<100)
{
g();
}
}
public static void k()
{
try
{
h();
}
catch (NullPointerException ne)
{
System.out.println("我是k()");
}
}
public static void main(String[] args)
{
try
{
k();
}
catch (ArrayIndexOutOfBoundsException ae)
{
//捕捉到异常后进行处理
System.out.println("数组下标越界!");
ae.printStackTrace();
}
}
}
这里在main中调用k函数,k函数又调用h函数,h函数调用g函数,g函数调用f函数,最终在f函数里会产生一个 ArrayIndexOutOfBoundsException异常,但在k函数中只能捕捉NullPointerException异常,在main中才可以正确捕捉ArrayIndexOutOfBoundsException异常。
运行结果

首先,我们要知道,如果在某函数中产生了一个异常,则产生异常的地方下面的所有代码都不会执行。那么,整个异常的传递和捕捉过程可总结为以下流程图

(注:该流程图借用翁恺老师的慕课《面向对象程序设计-Java语言》的内容)
建议自己按照流程图走一遍,再看下面的解释
这里的“退出到外层”的意思是跳出当前代码块,如h函数中调用g函数时,此时ArrayIndexOutOfBoundsException异常已经由g函数传出,而g函数所处的不是函数,则退出到外层,即跳出if代码块,跳出后相当于if语句是一个整体,由这个整体传出异常,此时这个异常的外层就是h函数了。
整体看来,异常的传递和捕捉机制可以总结为:由最初产生异常处(当做最下层)开始,层层往上传递,每次传递都尝试捕捉这个异常(通过try-catch语句)并进行处理,若当前层无法处理则再次向上throw,直到能够正确处理为止。(由输出的调用堆栈顺序也能直观看出)
2.再次抛出异常
这里再提一嘴,用try-catch语句成功捕捉到异常后并不一定要在这个地方就处理完全,有可能这里觉得不能够完全处理好这个异常,那么可以再次将这个异常抛出,下面重点关注k函数的修改,如:
public class ArrayIndex
{
public static void f()
{
int[] a=new int[10];
a[10]=5;
System.out.println("我是f()");
}
public static void g()
{
f();
}
public static void h()
{
int i=10;
if(i<100)
{
g();
}
}
public static void k()
{
try
{
h();
}
catch (ArrayIndexOutOfBoundsException e)
{
System.out.println("我是k(),我捕捉到了ArrayIndexOutOfBoundsException");
throw e;
}
}
public static void main(String[] args)
{
try
{
k();
}
catch (ArrayIndexOutOfBoundsException ae)
{
//捕捉到异常后进行处理
System.out.println("数组下标越界!");
ae.printStackTrace();
}
}
}
运行结果

四、异常与类
1.自定义异常类
上面所讲的异常,如ArrayIndexOutOfBoundsException和NullPointerException异常等,都属于系统运行时刻异常,这些异常是Java已经定义好的非常常见的异常类型,并不需要我们去声明,然而,有时可能会发生一些不太常见的异常,或我们想要按照自己的想法处理异常,这时我们就可以选择自定义一个异常类,这个异常类必须是Exception类或Throwable类的子类(一般都写Exception类),来看代码:
class openException extends Exception
{
//自定义情形
}
class closeException extends Exception
{
//自定义情形
}
public class ArrayIndex
{
public static int open()
{
return -1;
}
public static void readFile() throws openException,closeException
{
if(open()==-1)
{
throw new openException();
}
}
public static void main(String[] args)
{
try
{
readFile();
}
catch (NullPointerException e)
{
System.out.println("空指针");
e.printStackTrace();
}
catch (openException e)
{
System.out.println("打开失败");
e.printStackTrace();
}
catch (closeException e)
{
System.out.println("关闭失败");
e.printStackTrace();
}
System.out.println("处理异常后继续执行");
}
}
在这段代码中,我们实现了两个自定义异常类openException和closeException,来分别模拟文件打开异常和关闭异常的情形,并在main函数中设计了try-catch函数来处理自定义的异常
运行结果
可见,我们自定义的openException异常从readFile函数抛出,并由main中的try-catch语句捕捉并处理。
这里要注意,如readFile函数,如果函数声明时就告诉别人我这里可能会产生openException异常和closeException异常,那么在外层调用readFile函数时必须在try的语句内,并且必须要有相应的catch语句来捕捉openException异常和closeException异常,否则编译器会报错,如下:
try
{
readFile();
}
catch (NullPointerException e)
{
System.out.println("空指针");
e.printStackTrace();
}
catch (openException e)
{
System.out.println("打开失败");
e.printStackTrace();
}
//这里把捕捉closeException的语句注释掉
//catch (closeException e)
//{
// System.out.println("关闭失败");
// e.printStackTrace();
//}
报错

意思是没有捕捉处理closeException的语句
2.异常与继承
这部分有几个知识点:
(1)catch捕捉的异常类并不是完全精确匹配,其子类也能捕捉
举个例子,若openException是Exception的子类,则第一条语句写了catch(Exception e),下面就不能再写catch(openException e)了,因为第一条就一定会被捕捉到,第二条没有意义,如下:
try
{
readFile();
}
catch (Exception e)
{
System.out.println("所有类型异常");
e.printStackTrace();
}
catch (openException e)
{
System.out.println("打开失败");
e.printStackTrace();
}
catch (closeException e)
{
System.out.println("关闭失败");
e.printStackTrace();
}
报错

(2)子类覆盖(重写)父类函数时,不能声明比原父类函数更多的异常
这是因为在Java中,存在向上造型的机制,即可由父类的管理者来管理子类的实例对象,这是Java多态的重要组成部分。在这种情况下,如果允许子类重写的函数声明比原父类函数更多的异常,那么当调用这个函数时,因为是父类的管理者,父类函数可能只抛出一个异常,而实际上此时父类的管理者管理的是子类的实例对象,就可能抛出两个异常,这是调用者所无法预料的,也很可能无法很好处理这个异常。
因此,为了保持多态的安全性,Java语法规定子类的重写方法不能声明比父类方法更多的异常。
(3)若子类构造函数声明抛出异常,则必须声明父类可能抛出的所有异常
这是因为在子类的构造函数中,会自动调用父类的构造函数,若父类可能抛出5个异常,而子类只声明了4个异常,那么我们在使用子类对象的时候,剩下那个异常就很可能不会被捕捉到,所以Java语法规定,子类声明的异常必须包含父类所有可能抛出的异常。
1205

被折叠的 条评论
为什么被折叠?



