《疯狂java讲义》学习(35):异常处理

异常处理

Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字,其中try关键字后紧跟一个花括号括起来的代码块,简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch块后还可以跟一个finally块,finally块用于回收再try块里打开的屋里资源,异常机制会保证finally块总被执行。throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。
我们希望所有的错误都可以在编译阶段被发现,就是在试图运行程序之前排除所有错误,但这是不现实的,余下的问题必须在运行期间得到解决,Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以再编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无需处理。Checkde异常提醒程序员所有可能发生的异常,但checked异常也给程序带来一些烦琐之处,所以Checked异常也是Java领域一个备受争论的话题。

初识异常

NullPointerException(空指针异常)

下面代码:

public class ExceptionTest {
    public static void main(String[] args) {
        String s = null;
        s.indexOf("a");
        System.out.println("end");
    }
}

变量s没有初始化就调用其实例方法indexOf,运行,将输出如下错误提示:

Exception in thread "main" java.lang.NullPointerException
    at ExceptionTest.main(ExceptionTest.java:5)

输出是告诉我们:在ExceptionTest类的main函数中,代码第5行,出现了空指针异常(java.lang.NullPointerException)。
但,具体发生了什么呢?当s.indexOf(“a”)的时候,Java虚拟机发现s的值为null,没有办法继续执行了,这时就启动异常处理机制,首先创建一个异常对象,这里是类NullPointerException的对象,然后查找看谁能处理这个异常,在示例代码中,没有代码能处理这个异常,因此Java启动默认处理机制,即打印异常栈信息到屏幕,并退出程序。
在介绍函数调用原理的时候,我们介绍过栈,异常栈信息就包括了从异常发生点到最上层调用者的轨迹,还包括行号,可以说,这个栈信息是分析异常最为重要的信息。
Java的默认异常处理机制是退出程序,异常发生点后的代码都不会执行,所以示例代码中的System.out.println(“end”)不会执行。

NumberFormatexception(数字格式异常)

我们来看一个例子,代码如下:

public class ExceptionTest {
    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("输入数字");
            return;
        }
        int num = Integer.parseInt(args[0]);
        System.out.println(num);
    }
}

args表示命令行参数,这段代码要求参数为一个数字,它通过Integer.parseInt将参数转换为一个整数,并输出这个整数。如果输入的参数不是数字,是abc这样,屏幕会抛出错误信息:

Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:492)
    at java.lang.Integer.parseInt(Integer.java:527)
    at ExceptionTest.main(ExceptionTest.java:7)

出现了异常NumberformatException。这个异常是怎么产生的呢?根据异常栈信息,我们看相关代码。NumberFormatException类65行附近代码如下:

64 static NumberFormatException forInputString(String s) {
65      return new NumberFormatException("For input string: \"" + s + "\"");
66 }

Integer类492行附近代码如下:

490 digit = Character.digit(s.charAt(i++), radix);
491 if (digit < 0) {
492      throw NumberFormatException.forInputString(s);
493 }
494 if (result < multmin) {
495      throw NumberFormatException.forInputString(s);
496 }

将这两行何为一行,主要代码就是:

throw new NumberFormatException(...)

new NumberFormatException是容易理解的,含义是创建一个类的对象,只是这个类是一个异常类。throw是什么意思?就是抛出异常,他会触发Java的异常处理机制。在之前的空指针异常中,我们没有看到throw的代码,可以认为throw是由Java虚拟机自己实现的。
throw关键字可以与return关键字进行对比。return代表正常退出,throw代表异常退出;return的返回位置是确定的,就是上一级调用者,而throw后执行哪行代码则经常是不确定的,由异常处理机制动态确定。
异常处理机制会从当前函数开始查找看谁”捕获“了这个异常,当前函数没有的话就查看上一层,知道主函数,如果主函数也没有,就使用默认机制,即输出异常栈信息并退出,这正是我们再屏幕输出中看到的。
对于屏幕输出中的异常栈信息,程序员是可以理解的,但普通用户无法理解,也不知道该怎么办,我们需要给用户一个更为友好的信息,告诉用户,他应该输入的是数字,要做到这一点,需要自己“捕获”异常。“捕获”是指使用try/catch关键字,代码如下:

public class ExceptionTest {
    public static void main(String[] args) {
        if(args.length<1){
            System.out.println("请输入数字");
            return;
        }
        try{
            int num = Integer.parseInt(args[0]);
            System.out.println(num);
        }catch(NumberFormatException e){
            System.err.println("参数" + args[0] + "不是有效的数字,请输入数字");
        }
    }
}

上述代码使用try/catch捕获并处理了异常,try后面的花括号{}内包含可能抛出异常的代码,括号后的catch语句包含能捕获的异常和处理代码,catch后面括号内是异常信息,包括异常类型和变量名,这里是NumberFormatException e,通过它可以获取更多异常信息,花括号{}内是处理代码,这类输出一个更为友好的提示信息。
捕获异常后,程序就不会异常退出了,但try语句内异常点之后的其他代码就不会执行了,执行完catch内的语句后,程序会继续执行catch花括号的代码。
此时我们队异常有了一个初步的了解。异常是相对于return的一种退出机制,可以由系统触发,也可以有程序通过throw语句触发,异常可以通过try/catch语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息。异常有不同的类型,接下来,我们来认识一下。

异常类

NullPointerException和NumberFormatException都是异常类,所有异常类都有一个共同的父类Throwable。

Throwable

NullPointerException和NumberFormatException有一个共同的父类Throwable,它有4个public构造方法:

1.     public Throwable()
2.     public Throwable(String message)
3.     public Throwable(String message, Throwable cause)
4.     publich Throwable(Throwable cause)

Throwable类有两个主要参数:一个是message,表示异常信息;另一个是cause,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发,cause表示底层异常。Throwable还有一个public方法用于设置cause:

Throwable intiCause(Throwable cause)

Throwable的某些子类没有带cause参数的构造方法,就可以通过这个方法来设置,这个方法最多只能被调用一次。在所有构造方法的内部,都有一句重要的函数调用:

fillInStackTrace();

它会将异常栈信息保存下来,这是我们能看到异常栈的关键。Throwable有一些常用方法用于获取异常信息,比如:

void printStackTrace() //打印异常栈信息到标准错误输出流
//打印栈信息到指定的流,PrintStream和PrintWriter在第13章介绍
void printStackTrace(PrintStream s)
void printStackTrace(PrintWriter s)
String getMessage() //获取设置的异常message
Throwable getCause() //获取异常的cause
//获取异常栈每一层的信息, 每个StackTraceElement包括文件名、类名、函数名、行号等信息
StackTraceElement[] getStackTrace()

异常类体系

以Throwable为根,Java定义了非常多的异常类,表示各种类型的异常,部分类见下图:
《疯狂java讲义》学习(35):异常处理
Throwable是所有异常的基类,它有两个子类:Error和Exception。
Error表示系统错误或资源耗尽,有Java系统自己使用,应用程序不应抛出和处理比如上图中列出的虚拟机错误(VirtualMacheError)及其子类内存溢出错误(OutOfMemory-Error)和栈溢出错误(StackOverflowError)。
Exception表示应用程序错误,它有很多子类,应用程序也可以通过继承Exception或其子类创建自定义异常,图6-1中列出了三个直接子类:IOException(输入输出I/O异常)、RuntimeException(运行时异常)、SQLException(数据库SQL异常)。
RuntimeException比较特殊,它的名字有点误导,因为其他异常也是运行时产生的,它表示的实际含义是未受检异常(unchecked exception),相对而言,Exception的其他子类和Exception自身则是受检异常(checked exception), Error及其子类也是未受检异常。
受检(checked)和未受检(unchecked)的区别在于Java如何处理这两种异常。对于受检异常,Java会强制要求程序员进行处理,否则会有编译错误,而对于未受检异常则没有这个要求。下文我们会进一步解释。
RuntimeException也有很多子类,如下表:
《疯狂java讲义》学习(35):异常处理
如此多不同的异常类其实并没有比Throwable这个基类多多少属性和方法,大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。
那为什么定义这么多不同的类呢?主要是为了名字不同。异常类的名字本身就代表了异常的关键信息,无论是抛出还是捕获异常,使用合适的名字都有助于代码的可读性和可维护性。

自定义异常

除了Java API中定义的异常类,也可以自己定义异常类,一般是继承Exception或者它的某个子类。如果父类使RuntimeException或它的某个子类,则自定义异常也是未受检异常。
我们通过继承Exception来定义一个异常,代码如下:

public class AppException extends Exception {
    public AppException() {
        super();
    }
    public AppException(String message, Throwable cause) {
        super(message, cause);
    }
    public AppException(String message) {
        super(message);
    }
    public AppException(Throwable cause) {
        super(cause);
    }
}

和很多其他异常类一样,我们没有定义额外的属性和代码,只是继承了Exception,定义了构造方法并调用了父类的构造方法。

异常概述

每次在实现真正的业务逻辑之前,都需要不厌其烦地考虑各种可能出错的情况,针对各种错误情况给出补救措施——这是多么乏味的事情啊。程序员喜欢解决问题,喜欢开发带来的“创造”快感,都不喜欢像一个“堵漏”工人,去堵哪些由外在条件造成的“漏洞”。

对于穷举的错误处理机制,主要有如下两个缺点:

  • 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“ 错误处理代码和业务实现代码混杂。这种错误处理和业漏网之鱼”的异常情况,所以程序总是不够健壮。
  • 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。

程序员希望有一种强大的机制来解决穷举错误的问题,用伪码表示如下:

if(用户输入不合法)
{
    alert 输入不合法
    goto retry
}
else
{
    //业务实现代码
    ...
}

上面伪码提供了一个非常强大的“if块”——程序不管输入错误的原因是什么,只要用户输入不满足要求,程序就一次处理所有的错误。这种处理方法的好处是,是的错误处理代码变得有条理,只需要一个地方处理错误。

异常处理机制

Java的异常处理机制可以让程序员具有很好的容错性,让程序更加健壮。当程序运行出现意外情况时,系统自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

使用try…catch捕获异常

我们希望有一个非常强大的if块,可以表示所有的错误情况,让程序一次出来所有的错误,也就是希望将错误集中处理,处于这种考虑,将“错误处理代码”从“业务实现代码”中分离出来。实现伪码如下:

if(一切正常)
{
    //业务实现代码
    ...
}
else
{
    alert 输入不合法
    goto retry
}

但是一切正常是很抽象的,无法转换为计算机可以识别的代码,这种情形下,Java提供了一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理。下面是Java异常处理机制的语法结构。

try
{
    //业务实现代码
    ...
}
catch (Exception e)
{
    alert 输入不合法
    goto retry
}

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出异常。
当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交个该catch块处理,这个过程被称为捕获异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将推出。
下面是一个使用异常处理机制来处理程序的离职:

String inputStr=null;
// br.readLine():每当在键盘上输入一行内容时按回车键
// 刚输入的内容将被br读取到
while ((inputStr=br.readLine()) !=null){
    try {
        // 将用户输入的字符串以逗号作为分隔符,分解成2个字符串
        String[] posStrArr=inputStr.split(",");
        // 将2个字符串转换成用户下棋的坐标
        int xPos=Integer.parseInt(posStrArr[0]);
        int yPos=Integer.parseInt(posStrArr[1]);
        // 把对应的数组元素赋为"●"
        if (!gb.board[xPos - 1][yPos - 1].equals("╋"))
        {
            System.out.println("您输入的坐标点已有棋子了,"
                    + "请重新输入");
            continue;
        }
        gb.board[xPos - 1][yPos - 1]="●";
    }
    catch (Exception e){
        System.out.println("您输入的坐标不合法,请重新输入," + "下棋坐标应以x,y的格式");
        continue;
    }
    ...
}

上面程序把处理用户输入字符串的代码都放在try块里进行,只要用户输入的字符串不是有效的坐标值,系统将抛出一个异常对象,并将异常对象交给catch块,catch块的处理方式是向用户提示坐标不合法,然后使用continue忽略本次循环剩下的代码,开始执行下一次循环,这就保证该程序的容错性——用户可以随意输出,程序不会因为用户输入不合法而突然退出,程序会向用户提示合法输入,让用户再次输入。

每种异常类型都有单独的catch语句,如果多种异常处理的代码是类似的,这种写法比较烦琐。自Java 7开始支持一种新的语法,多个异常之间可以用“|”操作符,形如:

try {
      //可能抛出 ExceptionA和ExceptionB
} catch (ExceptionA | ExceptionB e) {
      e.printStackTrace();
}

重新抛出异常

在catch块内处理完后,可以重新抛出异常,异常可以是原来的,也可以是新建的,如下所示:

try{
    //可能触发异常的代码
}catch(NumberFormatException e){
    System.out.println("not valid number");
    throw new AppException("输入格式不正确", e);
}catch(Exception e){
    e.printStackTrace();
    throw e;
}

对于Exception,在打印出异常栈后,就通过throw e重新抛出了。
而对于NumberFormatException,重新抛出了一个AppException,当前Exception作为cause传递给了AppException,这样就形成了一个异常链,捕获到AppException的代码可以通过getCause()得到NumberFormatException。
为什么要重新抛出呢?因为当前代码不能够完全处理该异常,需要调用者进一步处理。
为什么要抛出一个新的异常呢?当然是因为当前异常不太合适。不合适可能是信息不够,需要补充一些新信息;还可能是过于细节,不便于调用者理解和使用,如果调用者对细节感兴趣,还可以继续通过getCause()获取到原始异常。

finally

异常机制中还有一个重要的部分,就是finally。catch后面可以跟finally语句,语法如下所示:

try{
    //可能抛出异常
}catch(Exception e){
    //捕获异常
}finally{
    //不管有无异常都执行
}

finally内的代码不管有无异常发生,都会执行,具体来看:

  • 如果没有异常发生,在try内的代码执行结束后执行。
  • 如果有异常发生且被catch捕获,在catch内的代码执行结束后执行。
  • 如果有异常发生但没被捕获,则在异常被抛给上层之前执行。

由于finally的这个特点,它一般用于释放资源,如数据库连接、文件流等。
try/catch/finally语法中,catch不是必需的,也就是可以只有try/finally,表示不捕获异常,异常自动向上传递,但finally中的代码在异常发生后也执行。
finally语句有一个执行细节,如果在try或者catch语句内有return语句,则return语句finally语句执行结束后才执行,但finally并不能改变返回值,我们来看下面的代码:

public static int test(){
    int ret = 0;
    try{
          return ret;
      }finally{
          ret = 2;
      }
}

这个函数的返回值是0,而不是2。实际执行过程是:在执行到try内的return ret;语句前,会先将返回值ret保存在一个临时变量中,然后才执行finally语句,最后try再返回那个临时变量,finally中对ret的修改不会被返回。
如果在finally中也有return语句呢?try和catch内的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖try和catch内的返回值,还会掩盖try和catch内的异常,就像异常没有发生一样,比如:

public static int test(){
    int ret = 0;
    try{
        int a = 5/0;
        return ret;
    }finally{
        return 2;
    }
}

以上代码中,5/0会触发ArithmeticException,但是finally中有return语句,这个方法就会返回2,而不再会向上传递异常,则原异常也会被掩盖,看如下代码:

public static void test(){
    try{
        int a = 5/0;
    }finally{
        throw new RuntimeException("hello");
    }
}

finally中抛出了RuntimeException,则原异常ArithmeticException就丢失了。所以,一般而言,为避免混淆,应该避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。

try-with-resources

对于一些使用资源的场景,比如文件和数据库连接,典型的使用流程是先打开资源,最后在finally语句中调用资源的关闭方法,针对这种场景,Java7开始支持一种新的语法,称之为try-with-resources,这种语法针对实现了java.lang.AutoCloseable接口的对象,该接口的定义为:

public interface AutoCloseable {
    void close() throws Exception;
}

没有try-with-resources时,使用形式如下:

public static void useResource() throws Exception {
      AutoCloseable r = new FileInputStream("hello"); //创建资源
      try {
          //使用资源
      } finally {
          r.close();
      }
}

使用try-with-resources语法,形式如下:

public static void useResource() throws Exception {
      try(AutoCloseable r = new FileInputStream("hello")) { //创建资源
          //使用资源
      }
}

资源r的声明和初始化放在try语句内,不用再调用finally,在语句执行完try语句后,会自动调用资源的close()方法。
资源可以定义多个,以分号分隔。在Java 9之前,资源必须声明和初始化在try语句块内,Java 9去除了这个限制,资源可以在try语句外被声明和初始化,但必须是final的或者是事实上final的(即虽然没有声明为final但也没有被重新赋值)。

throws

异常机制中,还有一个和throw很像的关键字throws,用于声明一个方法可能抛出的异常,语法如下:

public void test() throws AppException,
    SQLException, NumberFormatException {
    //主体代码
}

throws跟在方法的括号后面,可以声明多个异常,以逗号分隔。这个声明的含义是,这个方法内可能抛出这些异常,且没有对这些异常进行处理,至少没有处理完,调用者必须进行处理。这个声明没有说明具体什么情况会抛出什么异常,作为一个良好的实践,应该将这些信息用注释的方式进行说明,这样调用者才能更好地处理异常。
对于未受检异常,是不要求使用throws进行声明的,但对于受检异常,则必须进行声明,换句话说,如果没有声明,则不能抛出。
对于受检异常,不可以抛出而不声明,但可以声明抛出但实际不抛出。这主要用于在父类方法中声明,父类方法内可能没有抛出,但子类重写方法后可能就抛出了,子类不能抛出父类方法中没有声明的受检异常,所以就将所有可能抛出的异常都写到父类上了。
如果一个方法内调用了另一个声明抛出受检异常的方法,则必须处理这些受检异常,处理的方式既可以是catch,也可以是继续使用throws,如下所示:

public void tester() throws AppException {
    try {
        test();
    }   catch(SQLException e) {
        e.printStackTrace();
    }
}

对于test抛出的SQLException,这里使用了catch,而对于AppException,则将其添加到了自己方法的throws语句中,表示当前方法处理不了,继续由上层处理。

对比受检和未受检异常

通过以上介绍可以看出,为受检异常和受检异常的区别如下:受检异常必须出现在throws语句中,调用者必须处理,Java编译器会强制这一点,而未受检异常则没有这个要求。
为什么要有这个区分呢?我们自己定义异常的时候应该使用受检还是未受检异常呢?对于这个问题,业界有各种各样的观点和争论,没有特别一致的结论。
一种普遍的说法是:未受检异常表示编程的逻辑错误,编程时应该检查以避免这些错误,比如空指针异常,如果真的出现了这些异常,程序退出也是正常的,程序员应该检查程序代码的bug而不是想办法处理这种异常。受检异常表示程序本身没问题,但由于I/O、网络、数据库等其他不可预测的错误导致的异常,调用者应该进行适当处理。
但其实编程错误也是应该进行处理的,尤其是Java被广泛应用于服务器程序中,不能因为一个逻辑错误就使程序退出。所以,目前一种更被认同的观点是:Java中对受检异常和未受检异常的区分是没有太大意义的,可以统一使用未受检异常来代替。
这种观点的基本理由是:无论是受检异常还是未受检异常,无论是否出现在throws声明中,都应该在合适的地方以适当的方式进行处理,而不只是为了满足编译器的要求盲目处理异常,既然都要进行处理异常,受检异常的强制声明和处理就显得烦琐,尤其是在调用层次比较深的情况下。
其实观点本身并不太重要,更重要的是一致性,一个项目中,应该对如何使用异常达成一致,并按照约定使用。

如何使用异常

针对异常,我们介绍了try/catch/finally、catch匹配、重新抛出、throws、受检/未受捡异常,那到底该如何使用异常呢?下面从异常的使用情况、异常处理的目标和一般逻辑等多个角度进行介绍。

异常应该且仅用于异常情况

异常不能代替正常的条件判断。比如,循环处理数组元素的时候,应该先检查索引是否有效再进行处理,而不是等着抛出索引异常再结束循环。对于一个引用变量,如果正常情况下它的值也可能为null,那就应该先检查是不是null,不为null的情况下再进行调用。
另一方面,真正出现异常的时候,应该抛出异常,而不是返回特殊值。比如,String的substring()方法返回一个子字符串

public String substring(int beginIndex) {
    if(beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if(subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return(beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

代码会检查beginIndex的有效性,如果无效,会抛出StringIndexOutOfBoundsExcep-tion异常。纯技术上一种可能的替代方法是不抛出异常而返回特殊值null,但beginIndex无效是异常情况,异常不能作为正常处理。

异常处理的目标

异常大概可以分为三种来源:用户、程序员、第三方。用户是指用户的输入有问题;程序员指编码错误;第三方就是其他情况,有如:I/O错误、网络、数据库、第三方服务等。每种异常都应该进行适当的处理。
处理的目标可以分为恢复和报告。恢复是指通过程序自动解决问题。报告的最终对象可能是用户,即程序使用者,也可能是系统运维人员或程序员。报告的目的也是为了恢复,但这个恢复经常需要人的参与。
对用户,如果用户输入不对,可以提示用户具体哪里输入不对,如果是编程错误,可以提示用户系统错误、建议联系客服,如果是第三方连接问题,可以提示用户稍后重试。
对系统运维人员或程序员,他们一般不关系用户输入错误,而关注编程错误或第三方错误,对于这些错误,需要报告尽量完整的细节,包括异常链、异常栈,以便尽快定位和解决问题。
用户输入或编程错误一般都是难以通过程序自动解决的,第三方错误则可能可以,甚至很多时候,程序都不应该坚定第三方是可靠地,应该有容错机制。比如,某个第三方服务器连接不上发个短信,可能的容错机制是换另一个提供相同功能的第三方试试,还可能是间隔一段时间进行冲洗试验,在多次失败之后再报告错误。

异常处理的一般逻辑

如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。
如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为cause重新抛出一个异常。
总有一层代码需要为异常负责,可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是主程序。如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。
这个逻辑与在公司中处理问题的逻辑是类似的,每个级别都有自己应该解决的问题,自己能处理的自己处理,不能处理的就应该报告上级,把下级告诉他的和他自己知道的一并告诉上级,最终,公司老板必须要为所有问题负责。每个级别既不应该掩盖问题,也不应该逃避责任。
本章介绍了Java中的异常机制。在没有异常机制的情况下,唯一的退出机制是return,判断是否异常的方法就是返回值。方法根据是否异常返回不同的返回值,调用者根据不同返回值进行判断,并进行相应处理。每一层方法都需要对调用的方法的每个不同返回值进行检查和处理,程序的正常逻辑和异常逻辑混杂在一起,代码往往难以阅读理解和维护。另外,因为异常毕竟是少数情况,程序员经常偷懒,假装异常不会发生,而忽略对异常返回值的检查,降低了程序的可靠性。
在有了异常机制后,程序的正常逻辑与异常逻辑可以相分离,异常情况可以集中进行处理,异常还可以自动向上传递,不再需要每层方法都进行处理,异常也不再可能被自动忽略,从而,处理异常情况的代码可以大大减少,代码的可读性、可靠性、可维护性也都可以得到提高。

Java实例练习

嵌套try-catch-finally捕获异常

实际上,和其余的块语句,诸如if、for、while一样,try-catch-finally也是可以嵌套使用的。也就是说,一个try-catch-finally可以嵌套在另一个try语句块的try、catch或finally部分。本实例将演示如何使用嵌套try-catch-finally捕获异常。

1.

新建项目NestTryCatch,并在其中创建一个NestTryCatch.java文件。在该类的主方法中使用嵌套try-catch-finally捕获数组越界和算数异常。核心代码如下所示:

package NestTryCatch;

public class NestTryCatch {
    public static void main(String[] args) {
        int array[] = new int[5];
        int n = 0;
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        for (int i = 0; i < 10; i++) {
            try {
                try {
                    n = array[i] / (i - 1);   //这里可能错误出现0异常
                } catch (ArithmeticException e) {
                    System.out.println("0不能做被除对象");
                }
                System.out.println(n);
            } catch (IndexOutOfBoundsException e) {
                System.out.println("数组索引越界");
            }
        }
    }
}

运行程序,结果如下:

0
0不能做被除对象
0
2
1
1
数组索引越界
数组索引越界
数组索引越界
数组索引越界
数组索引越界

将try-catch-finally嵌套在catch语句中是因为我们在处理异常的时候很可能会引发新的异常。