NO.7 异常: 相处之道 | Java敲黑板系列

开场白

老铁:昨天我们对异常的来源、定义、控制流执行逻辑等方面进行了梳理。既然异常无法避免,那么如何与异常和平共处就显得尤为重要,今天我们来谈谈与异常的相处之道。

其中,“不要忽视”、“不要丢弃”是与异常相处之道中最重要的两条,下面我们分别来说说。

不要忽视

Java程序产生了异常如果不去捕获,那么发生异常的那个线程就将被中断;
而所谓“不要忽视”,就是当程序发生异常的时候,我们要对异常及时进行处理,“要有所作为,不当甩手掌柜”。

具体来讲,当程序发生异常,我们可以采取以下动作响应:

  1. 捕捉并处理异常,防止它传播给调用端;进而遏制异常的进一步传播。
  2. 捕捉,并再次原样抛出异常给调用端,将异常原样throw。
  3. 捕捉,然后抛出一个新异常给调用端,确保新抛出的异常包含原有异常信息,但可进一步封装,调用throw将新异常 抛出。
  4. 不捕捉,任由它传播给调用端,并且该函数会马上中断,不会继续执行该函数的后续语句。

对于第一种方式,看似是一种很负责任的方式;但是,如果实行不恰当的处理,往往会更加危险。如代码片段1所示。

public void transfer(String strMoney){
int money = 0;
try{
    money = Integer.parseInt(strMoney);
}catch(NumberFormatException nfe){
    //没有任何语句进行处理
}
//其他的业务代码......
}

敲黑板:

上述代码经常在我们的生产系统中出现,这是一种不良的编程实践:当异常发生时,上述代码对异常进行了捕获,按照我们NO.6 异常: 初识 | Java敲黑板系列中对异常执行的控制流的描述,捕获发生后进入catch子句,由于开发人员的各种原因(疏忽?故意?),catch子句里面没有任何代码,实际上异常并没有得到处理,因此catch子句执行完成后,catch区段之后的代码将“好像没有发生过什么似的”继续执行。换句话说,代码1的方式没有对异常做任何应对,只是将它给【吞】了。这样的做法很危险,因为后续程序可能就会遭遇失败,由于缺少对上述异常的必要处理与记录,直接导致我们定位错误非常困难。

更加安全的做法如代码片段2所示,我们不仅在屏幕上输出了异常发生的位置与导致异常的原因;此外我们还将异常详细信息输出到了一个日志文件。

public void transfer(String strMoney){
int money = 0;
try{
    money = Integer.parseInt(strMoney);
}catch(NumberFormatException nfe){
    //对异常进行处理
    System.out.print("In transfer exception : " + nfe.getMessage());
    logFile(nfe);
}
//其他的业务代码......
}

敲黑板:

第1种处理方式相比于第2、3、4方式,是更好的编程实践:
不要推诿或延迟处理异常,尽量就地解决他们。

不要丢弃

我们先从一个典型的实际例子说起,如代码片段3所示:

import java.io.*;

public class HideException {
    //一个函数同时读取两个文件
    public void readTwoFile() throws FileNotFoundException, IOException{
        BufferedReader br1 = null;
        BufferedReader br2 = null;
        FileReader fr = null;
        try{
            fr = new FileReader("A.txt"); //1
            br1 = new BufferedReader(fr); 
            int count = br1.read(); //2
            //process code1....

            fr = new FileReader("B.txt"); //3
            br2 = new BufferedReader(fr);
            count = br2.read(); //4
            //process code2
        }finally{
            if(br1 != null)
                br1.close(); //5
            if(br2 != null)
                br2.close(); //6
        }
    }

    //测试客户端
    public static void main(String[] args){
        HideException he = new HideException();
        try {
            he.readTwoFile();
        } catch (FileNotFoundException e) {
            //...
            e.printStackTrace(); //7
        } catch (IOException e) {
            //...
            e.printStackTrace(); //8
        }
    }
}

如代码片段3所示,readTwoFile用来读取两个文件。其中语句//1与语句//3有可能会抛出FileNotFoundException;语句//2、//4、//5、//6有可能会抛出IOException。按照对控制流的执行逻辑分析,上述代码中的finally子句是无论如何都会被执行的。当//3、//5同时发生异常时,会执行main函数中的哪条/哪些语句?请老铁们思考1分钟后再往下看。

答案揭晓:当//3产生了一个FileNotFoundException时,控制流会转向到finally子句,但是其中//5又发生了IOException,那么这个时候返回main调用端的将会是IOException而不是FileNotFoundException。因为//3产生的异常被//5抛出的异常所覆盖了。为此,将执行main函数中的语句//8。

敲黑板:

为此,如果采用代码3的处理方式,调用端收到的是IO错误,而无法知道其调用的函数的最初失败的原因是“未找到B.txt”,也就是说【丢弃】了异常。

如何解决上述问题了?欢迎老铁们在留言区写下您们的想法。其中解决方案将在下一篇文章中进行详细说明。

小结

敲黑板,画重点:

  1. 不要推诿或延迟处理异常,就地解决最好,并且需要实实在在的进行处理,而不是只捕捉,不动作。
  2. 一个函数尽管抛出了多个异常,但是只有一个异常可被传播到调用端。记住:最后被抛出的异常时唯一被调用端接收的异常,其他异常都会被吞没掩盖。如果调用端要知道造成失败的最初原因,程序之中就绝不能掩盖任何异常。

转载自公众号:代码荣耀
图1

猜你喜欢

转载自blog.csdn.net/maijia0754/article/details/80568761