正文
异常是什么?Java如何描述异常?
- 异常,顾名思义就是指程序执行过程中出现的不正常情况。例如:
class ExceptionDemo {
public static void main(String[] args) {
int[] arr = new int[3];
System.out.println(arr[3]); // java.lang.ArrayIndexOutOfBoundsException
}
}
通过编译运行上面的代码,我们发现异常发生在运行时期。
- 在Java中用类的形式对这些不正常情况进行了描述和封装,这些类就称为异常类。其实异常就是Java通过面向对象的思想将程序中出现的问题封装成了对象。
异常体系?
程序中可能会出现的问题有很多,比如:角标越界、空指针等,那么这些问题就需要用不同的类进行具体的描述。将这些类的共性进行向上抽取就形成了异常体系。
从上面的图我们可以看到:Throwable是Java异常体系的顶级类,它的下面有两个子类:Exception和Error。
-
Throwable,顾名思义,该异常体系所有的子类包括Throwable都具有可抛性。具有可抛性的原因就在于:无论是哪种异常情况,一旦发生就应该"抛出"让调用者知道并处理。可抛性具体是通过两个关键字:throw和throws来体现的。
-
Exception:指可以处理的异常情况。
-
Error:指一般不可处理的异常情况。因为这类问题是由JVM抛出的严重问题,所以这种问题一旦发生一般不做针对性处理,建议直接修改程序。
异常处理过程?throw?
- 那么异常到底是如何进行处理的呢?我们借助下面这个例子来进行解释:
class Demo {
public void method(int[] arr, int index) {
System.out.println(arr[index]); // 2
System.out.println("method run");
}
}
class ExceptionDemo {
public static void main(String[] args) {
int[] arr = new int[3];
Demo d = new Demo();
d.method(arr, 3); // 1
}
}
// 一:当代码执行到2时,程序出现异常,而Java针对这种异常情况已经提前做好了描述,即是:ArrayIndexOutOfBoundsException。于是,JVM就会在此处将该异常信息封装成对应异常对象并使用"throw关键字"将该对象抛给调用者:throw new ArrayIndexOutOfBoundsException(index)。注意:这个操作是由JVM自动完成。
// 二:由于method()是被ExceptionDemo类的主函数调用,所以该对象被抛给ExceptionDemo类的主函数。
// 三:由于ExceptionDemo类的主函数并未对该异常情况进行处理,所以该异常对象继续被ExceptionDemo类的主函数向上抛给JVM。
// 四:JVM收到此异常对象之后就会启动JVM的默认异常处理机制,即是:将该异常对象的信息直接全部打印到控制台。
通过上面代码的运行结果我们还能看出:当程序发生了异常之后,该程序就被终止了,也就是说异常有终止函数的功能。
- 既然JVM自动使用"throw关键字"抛出异常,那我们当然也可以手动执行这个操作。就像下面这样:
class Demo
{
public int method(int[] arr, int index)
{
if(arr == null)
throw new NullPointerException("数组的引用不能为空!"); // 可以自定义异常信息
if(index >= arr.length)
{
throw new ArrayIndexOutOfBoundsException("数组的角标越界!"+index);
}
if(index < 0)
{
throw new ArrayIndexOutOfBoundsException("数组的角标不能为负数!:"+index);
}
return arr[index];
}
}
class ExceptionDemo
{
public static void main(String[] args)
{
int[] arr = new int[3];
Demo d = new Demo();
int num = d.method(null, -30);
System.out.println("num=" + num);
System.out.println("over");
}
}
自定义异常?throws?
对于角标为负数的情况,我们可以用"负数角标异常"来表示,但是这种异常在Java中并没有被定义和描述过。我们可以按照Java异常的面向对象思想,将负数角标这种异常情况进行自定义描述,并将其封装成对象。如下:
class FuShuIndexException extends Exception // 自定义负数角标异常
{
FuShuIndexException()
{
}
FuShuIndexException(String msg)
{
super(msg);
}
}
要注意当我们在方法中抛出该异常时,需要进行捕捉(下面会提到)或声明(使用关键字"throws")。声明的原因是:这样调用者在调用该方法之前就可以预先有一些处理方式:
// 此处省略FuShuIndexException类,与上同
class Demo
{
public int method(int[] arr, int index) throws FuShuIndexException { // 使用"throws关键字"声明异常
if(arr == null)
throw new NullPointerException("数组的引用不能为空!");
if(index < 0)
{
throw new FuShuIndexException("角标不能为负数!");
}
return arr[index];
}
}
class ExceptionDemo3
{
public static void main(String[] args) throws FuShuIndexException
{
int[] arr = new int[3];
Demo d = new Demo();
int num = d.method(null,-30);
}
}
我们其实可以注意到上面的NullPointerException并没有进行捕捉或声明,其实通过查看jdk文档,我们发现:NullPointerException是RuntimeException的子类。异常除了根据上面的异常体系划分之外,还可以划分为运行时异常和编译时异常。那么这两者有什么区别呢?
-
编译时异常(又称为编译时被检测异常):具体指的是Exception和除了RuntimeException体系的其他子类。这种异常在编译时就会进行检测从而让这种问题有对应的处理方式,所以这样的问题一般都可以针对性地处理。
-
运行时异常(又称为编译时不检测异常):具体指的就是Exception中的RuntimeException和其子类。这种异常在编译时一般不处理直接通过,而在运行时让程序强制停止,表明调用者应该对代码进行修正。因为这种问题的发生会导致程序无法继续执行,它更多是由于调用者或者引发了内部状态的改变而导致的。
异常捕捉?
上面说过当我们抛出自定义异常FuShuIndexException时,需要进行捕捉或声明。异常的捕捉其实就是一种可以对异常进行针对性处理的方式。所以如果我们对FuShuIndexException进行捕捉就是下面这样:
// 此处省略FuShuIndexException类,与上同
class Demo
{
public int method(int[] arr, int index) throws FuShuIndexException
{
if(index < 0)
throw new FuShuIndexException("角标变成负数啦!"); // 1
return arr[index];
}
}
class ExceptionDemo
{
public static void main(String[] args)
{
int[] arr = new int[3];
Demo d = new Demo();
try // 对抛出的FuShuIndexException异常进行针对性处理
{
int num = d.method(arr, -1);
System.out.println("num=" + num);
}
catch (FuShuIndexException e) // 捕捉FuShuIndexException异常 // 2
{
e.printStackTrace(); // 3
}
}
}
// 同样的,我们对上面代码的异常处理过程进行分析:
// 一:当代码执行到1时,程序出现异常,JVM就在此处将该异常信息进行了封装并抛给了调用者ExceptionDemo的主函数:throw new FuShuIndexException("角标变成负数啦!");
// 二:由于ExceptionDemo的主函数针对该异常已经进行了针对性的处理,于是代码执行到2,JVM将该异常对象赋值给了e,即:FuShuIndexException e = new FuShuIndexException("角标变成负数啦!");
// 三:接下来对该异常对象进行针对性的处理:即执行3处的代码。
我们需要注意:如果程序中的方法抛出了多个异常,那么在调用该方法时,必须有对应的多个catch块进行针对性的处理。即:代码内部有几个需要检测的异常,就抛几个,抛出几个,就有几个catch块。就像下面这样:
// 此处省略FuShuIndexException类,与上同
class Demo
{
public int method(int[] arr, int index) throws FuShuIndexException
{
if(arr == null)
throw new NullPointerException("数组的引用不能为空!");
if(index < 0)
throw new FuShuIndexException();
return arr[index];
}
}
class ExceptionDemo
{
public static void main(String[] args)
{
int[] arr = new int[3];
Demo d = new Demo();
try
{
int num = d.method(null,-1);
System.out.println("num=" + num);
}
catch(NullPointerException e) // 捕捉NullPointerException异常
{
System.out.println(e.toString());
}
catch (FuShuIndexException e) // 捕捉FuShuIndexException异常
{
e.printStackTrace();
}
}
}
由于异常体系的原因,当有多个catch块的时候,父类的catch块放在最下面。因为多个catch块按照定义顺序依次执行。
关于使用"throws"和"try-catch",我们需要注意以下问题:
-
如果函数中的内容抛出了编译时异常,那么该函数上要么使用"throws"进行声明要么使用"try-catch"进行捕捉。否则编译失败。
-
如果一个函数调用到了声明了异常的函数,那么该函数要么使用"throws"进行声明要么使用"try-catch"进行捕捉,否则编译失败。
那么我们什么时候使用"throws"进行声明,什么时候使用"try-catch"进行捕捉呢?如果发生异常的内容我们可以进行针对性处理,那么使用"try-catch"进行捕捉;如果我们无法进行处理,那么就用使用"throws"进行声明,由调用者进行处理。
finally?
finally块也是异常处理的一部分。无论是否捕获或处理异常,finally块里的语句都会被执行,它通常用于关闭或释放资源:
class Demo {
void show()throws Exception
{
try
{
//开启资源。
throw new Exception();
}
catch(Exception e)
{
}
finally
{
//关闭资源。
}
}
}
关于finally块,我们需要注意下面几个问题:
- 当发生以下四种情况时,出现在Java程序中的finally块不一定会被执行。
当程序在进入try语句块之前就出现异常时,比如:
public class Test {
public static void testFinally() {
int i = 1 / 0;
try {
} catch (Exception e) {
} finally {
System.out.println("execute finally");
}
}
public static void main(String[] args) {
testFinally();
}
}
当程序在try块中强制退出时,比如:
public class Test {
public static void testFinally() {
try {
System.exit(0); // 调用System.exit(0)强制退出
} catch (Exception e) {
} finally {
System.out.println("finally block");
}
}
public static void main(String[] args) {
testFinally();
}
}
当程序所在的线程死亡或关闭CPU时,finally块也不会执行。
- 当try块或catch块中有return语句时,finally块中的代码会执行并且会在return之前执行。因为程序执行return就意味着结束对当前函数的调用并跳出该函数体,因此任何语句要执行都只能在return之前执行。
class Demo {
public static void main(String[] args) {
try {
return;
}
catch (Exception e) {
return;
}
finally {
System.out.println("Finally block");
}
}
}
- 此外,如果try-finally或者catch-finally中都有return语句,那么finally块中的return语句将会覆盖别处的return语句:
class Demo {
public static int testFinally() {
try {
return 1;
} catch (Exception e) {
return 0;
} finally {
return 3;
}
}
public static void main(String[] args) {
int result = testFinally();
System.out.println(result);
}
}
有关异常的注意事项
子类在覆盖父类方法时,如果父类的方法抛出了异常,那么子类的方法只能抛出父类的异常或者该异常的子类,这也就是说:如果父类的方法没有抛出异常,那么子类覆盖方法时也不能抛出异常,这时就只能使用try-catch进行捕捉;如果父类抛出多个异常,那么子类只能抛出父类异常的子集。具体的原因如下:
// 自定义三个异常
class AException extends Exception
{
}
class BException extends AException
{
}
class CException extends Exception
{
}
// 父类
class Fu
{
void show() throws AException
{
}
}
// 子类
class Zi extends Fu
{
void show() throws AException // 该方法就只能抛出AException或者BException
{
}
}
// 原因如下:
class Test
{
void method(Fu f) // Fu f = new Zi();为了让子类也能正常调用该方法,就意味着子类只能抛出该方法能处理的异常。
{
try
{
f.show();
}
catch (AException a) // 只能处理AException和其子类
{
}
}
public static void main(String[] args)
{
Test t = new Test();
t.show(new Zi());
}
}