到底怎样写 try-catch,才能称之为高手?

try-catch,一个大家最常见不过的语法,但是有些人却用不好。当面对一个崩溃的时候,不管三七二十一先 catch 住再说,没错,老崩溃是被 catch 住了,但是新崩溃又出现了,想想都是坑啊,本文就和大家探讨下这个话题,此外买一送一,再和大家探讨下空指针异常。

乱用 try-catch
写过 C ++ 的人都知道,那异常捕获特别不好用,甚至有些异常还捕获不了,在 Java 中,try-catch 特别好使,如果一个人的代码没有 try-catch,有时候会被大家质疑代码写的不健壮。

try-catch 虽好,但不可乱用,原因在于 try-catch 可以隐藏代码缺陷,不利于 bug 排查。如果没有 try-catch,那程序会崩溃,崩溃后从日志中可以一眼看出问题的原因。而如果用了 try-catch 以后,崩溃被 catch 住了,这个时候程序没有崩溃,但是程序运行会不正常。

我相信大家都明白:crash 并不可怕,可怕的是 crash 后却没有找到日志,你就慢慢去查吧,这感觉谁查谁知道。

举个例子来说明下:


public class Example {
    public static void main(String[] args) {

        BusinessLogic bl = new BusinessLogic();
        bl.doA();
        bl.doB();
        bl.doC();
        bl.doD();
        bl.doE();

    }

}


class BusinessLogic {

    public void doA(){
        System.out.println("I am doing A");
    }

    public void doB(){
        System.out.println("I am doing B");
    }

    public void doC(){
        int i = 1/0;
        System.out.println("I am doing C");
    }

    public void doD(){
        System.out.println("I am doing D");
    }

    public void doE(){
        System.out.println("I am doing E");
    }
}

上面的程序有个问题,在 doC 函数中存在除 0 异常,程序运行后会崩溃,我们可以得到如下日志反馈:


I am doing A
I am doing B
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at BusinessLogic.doC(Example.java:27)
    at Example.main(Example.java:7)
sandbox> exited with status 0

但是有的程序员,特别害怕崩溃,为了让自己的程序不会崩溃,直接在最外层进行 try-catch:


public class Example {
    public static void main(String[] args) {
        try {
            BusinessLogic bl = new BusinessLogic();
            bl.doA();
            bl.doB();
            bl.doC();
            bl.doD();
            bl.doE();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

}

结果,程序是不会崩溃了,但是逻辑并没有正确地执行,这个时候如果让你去排查这个问题,你能第一时间定位到 doC 函数有问题吗?我觉得大部分人都不能。

虽然这个例子看起来有点可笑,但是在实际开发中确实看到过不少这种现象,更有甚者,连 e.printStackTrace()这句话都懒得加,程序没有任何异常日志,想排查这种错误简直痛不欲生。

空指针强迫症
前面说到崩溃,大家知道排名前 2 的崩溃是什么吗?没错,那就是:
NullPointerException
IndexOutOfBoundsException

大家可以检查下自己的项目,看看这两个异常是不是占大头。所以,为了防止空指针异常,很多程序员会这样写代码:


public class Example {
    public static void main(String[] args) {
        BusinessLogic bl = new BusinessLogic();
        Object param = new Object();
        bl.doA(param);
    }

}


class BusinessLogic {

    public void doA(Object o){
        if (o != null) {
            System.out.println("I am doing A with param : " + o);
            doB(o);
        }
    }

    public void doB(Object o){
        if (o != null) {
            System.out.println("I am doing B with param : " + o);
            doC(o);
        }
    }

    public void doC(Object o){
        System.out.println("I am doing C with param : " + o);
    }

}

上面程序犯了一个错误,那就是重复判空,大量的冗余判断会影响程序的性能,上面的例子很简单,我相信大家都不会犯这个错误,但是当项目大了以后,当函数调用错综复杂的时候,你还能保证自己不犯错吗?很难!

大家还记得 C 语言的 strcpy 函数吗?


strcpy(char * __restrict to, const char * __restrict from)
{
    char *save = to;

    for (; (*to = *from); ++from, ++to);
    return(save);

可以看到,C 语言的 strcpy 函数,没有进行任何判断,比如你传递个空,或者说 to 的长度比 from 小,这些都会导致程序异常,但 strcpy 依然是不做任何判断,为的就是追求高性能。

对于 strcpy 来说,异常判断是上层调用方该考虑的事情,它作为底层 API 并不需要关注那么多,如果上层调用不合法,那就让程序挂掉就行了,谁叫你不按我的要求来呢!

这个涉及到架构设计的理念,我们在开发一个 sdk 的时候,其实是不需要每次都校验参数合法性的,明确对外暴露的 API,如果说外部调用不合法,一定要让程序挂掉,而不是不回应,这样上层也更好排查问题,尤其是一些调用频次较高的底层 sdk,更要注重这一点,性能和调用反馈是我们需要注意的。

这就好比搭积木,我们无需保证每块积木都是完全可靠的,但是每个积木之间无缝衔接,没有多一点,也没有少一点,而是刚刚好,它们共同构成了一个完整健壮的个体。写程序也是如此:

无需做诸多冗余的保证,只要各层级的代码能够无缝衔接就好

这就是我心中的代码设计理念,大道至简,简单到每个人都能理解。