编写高质量代码:改善Java程序的151个建议(第8章:异常___建议114~117)

建议114:不要在构造函数中抛出异常

  Java异常的机制有三种:程序员

  • Error类及其子类表示的是错误,它是不须要程序员处理也不能处理的异常,好比VirtualMachineError虚拟机错误,ThreadDeath线程僵死等。
  • RunTimeException类及其子类表示的是非受检异常,是系统可能会抛出的异常,程序员能够去处理,也能够不处理,最经典的就是NullPointException空指针异常和IndexOutOfBoundsException越界异常。
  • Exception类及其子类(不包含非受检异常),表示的是受检异常,这是程序员必须处理的异常,不处理则程序不能经过编译,好比IOException表示的是I/O异常,SQLException表示的数据库访问异常。  

咱们知道,一个对象的建立过程通过内存分配,静态代码初始化、构造函数执行等过程,对象生成的关键步骤是构造函数,那是否是也容许在构造函数中抛出异常呢?从Java语法上来讲,彻底能够在构造函数中抛出异常,三类异常均可以,可是从系统设计和开发的角度来分析,则尽可能不要在构造函数中抛出异常,咱们以三种不一样类型的异常来讲明之。数据库

(1)、构造函数中抛出错误是程序员没法处理的编程

  在构造函数执行时,若发生了VirtualMachineError虚拟机错误,那就没招了,只能抛出,程序员不能预知此类错误的发生,也就不能捕捉处理。缓存

(2)、构造函数不该该抛出非受检异常ide

  咱们来看这样一个例子,代码以下:函数

class Person {
    public Person(int _age) {
        // 不满18岁的用户对象不能创建
        if (_age < 18) {
            throw new RuntimeException("年龄必须大于18岁.");
        }
    }

    public void doSomething() {
        System.out.println("doSomething......");
    }
}

  这段代码的意图很明显,年龄不满18岁的用户不会生成一个Person实例对象,没有对象,类行为doSomething方法就不可执行,想法很好,但这会致使不可预测的结果,好比咱们这样引用Person类: 性能

public static void main(String[] args) {
        Person p =  new Person(17);
        p.doSomething();
        /*其它的业务逻辑*/
    }

  很显然,p对象不能创建,由于是一个RunTimeException异常,开发人员能够捕捉也能够不捕捉,代码看上去逻辑很正确,没有任何瑕疵,可是事实上,这段程序会抛出异常,没法执行。这段代码给了咱们两个警示:测试

  1. 加剧了上层代码编写者的负担:捕捉这个RuntimeException异常吧,那谁来告诉我有这个异常呢?只有经过文档约束了,一旦Person类的构造函数通过重构后再抛出其它非受检异常,那main方法不用修改也是能够测试经过的,可是这里就可能会产生隐藏的缺陷,而写仍是很难重现的缺陷。不捕捉这个RuntimeException异常,这个是咱们一般的想法,既然已经写成了非受检异常,main方法的编码者彻底能够不处理这个异常嘛,大不了不执行Person的方法!这是很是危险的,一旦产生异常,整个线程都再也不继续执行,或者连接没有关闭,或者数据没有写入数据库,或者产生内存异常,这些都是会对整个系统产生影响。
  2. 后续代码不会执行:main方法的实现者本来是想把p对象的创建做为其代码逻辑的一部分,执行完doSomething方法后还须要完成其它逻辑,可是由于没有对非受检异常进行捕捉,异常最终会抛出到JVM中,这会致使整个线程执行结束后,后面全部的代码都不会继续执行了,这就对业务逻辑产生了致命的影响。

(3)、构造函数尽量不要抛出受检异常优化

  咱们来看下面的例子,代码以下:this

//父类
class Base {
    // 父类抛出IOException
    public Base() throws IOException {
        throw new IOException();
    }
}
//子类
class Sub extends Base {
    // 子类抛出Exception异常
    public Sub() throws Exception {

    }
}

  就这么一段简单的代码,展现了在构造函数中抛出受检异常的三个不利方面:

  1. 致使子类膨胀:在咱们的例子中子类的无参构造函数不能省略,缘由是父类的无参构造函数抛出了IOException异常,子类的无参构造函数默认调用的是父类的构造函数,因此子类无参构造函数也必须抛出IOException或其父类。
  2. 违背了里氏替换原则"里氏替换原则" 是说父类能出现的地方子类就能够出现,并且将父类替换为子类也不会产生任何异常。那咱们回头看看Sub类是否能够替换Base类,好比咱们的上层代码是这样写的:
public static void main(String[] args) {
        try {
            Base base = new Base();
        } catch (Exception e) {    
            e.printStackTrace();
        }
    }

  而后,咱们指望把new Base()替换成new Sub(),并且代码可以正常编译和运行。很是惋惜,编译不经过,缘由是Sub的构造函数抛出了Exception异常,它比父类的构造函数抛出更多的异常范围要宽,必须增长新的catch块才能解决。  

  可能你们要问了,为何Java的构造函数容许子类的构造函数抛出更普遍的异常类呢?这正好与类方法的异常机制相反,类方法的异常是这样要求的:

// 父类
class Base {
    // 父类方法抛出Exception
    public void testMethod() throws Exception {

    }
}

// 子类
class Sub extends Base {
    // 父类方法抛出Exception
    @Override
    public void testMethod() throws IOException {

    }
}

  子类的方法能够抛出多个异常,但都必须是覆写方法的子类型,对咱们的例子来讲,Sub类的testMethod方法抛出的异常必须是Exception的子类或Exception类,这是Java覆写的要求。构造函数之因此于此相反,是由于构造函数没有覆写的概念,只是构造函数间的引用调用而已,因此在构造函数中抛出受检异常会违背里氏替换原则原则,使咱们的程序缺少灵活性。

  3.子类构造函数扩展受限:子类存在的缘由就是指望实现扩展父类的逻辑,但父类构造函数抛出异常却会让子类构造函数的灵活性大大下降,例如咱们指望这样的构造函数。

// 父类
class Base {
    public Base() throws IOException{
        
    }
}
// 子类
class Sub extends Base {
    public Sub() throws Exception{
        try{
            super();
        }catch(IOException e){
            //异常处理后再抛出
            throw e;
        }finally{
            //收尾处理
        }
    }
}

  很不幸,这段代码编译不经过,缘由是构造函数Sub没有把super()放在第一句话中,想把父类的异常从新包装再抛出是不可行的(固然,这里有不少种 “曲线” 的实现手段,好比从新定义一个方法,而后父子类的构造函数都调用该方法,那么子类构造函数就能够自由处理异常了),这是Java语法机制。

  将以上三种异常类型汇总起来,对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了 " 对己对人 " 都是有害的;受检异常尽可能不抛出,能用曲线的方式实现就用曲线方式实现,总之一句话:在构造函数中尽量不出现异常。

  注意 :在构造函数中不要抛出异常,尽可能曲线实现。

建议115:使用Throwable得到栈信息

  AOP编程能够很轻松的控制一个方法调用哪些类,也可以控制哪些方法容许被调用,通常来讲切面编程(好比AspectJ),只能控制到方法级别,不能实现代码级别的植入(Weave),好比一个方法被类A的m1方法调用时返回1,在类B的m2方法调用时返回0(同参数状况下),这就要求被调用者具备识别调用者的能力。在这种状况下,可使用Throwable得到栈信息,而后鉴别调用者并分别输出,代码以下: 

class Foo {
    public static boolean method() {
        // 取得当前栈信息
        StackTraceElement[] sts = new Throwable().getStackTrace();
        // 检查是不是methodA方法调用
        for (StackTraceElement st : sts) {
            if (st.getMethodName().equals("methodA")) {
                return true;
            }
        }
        return false;
    }
}
//调用者
class Invoker{
    //该方法打印出true
    public static void methodA(){
        System.out.println(Foo.method());
    }
    //该方法打印出false
    public static void methodB(){
        System.out.println(Foo.method());
    }
}

  注意看Invoker类,两个方法methodAmethodB都调用了Foo的method方法,都是无参调用,返回值却不一样,这是咱们的Throwable类发挥效能了。JVM在建立一本Throwable类及其子类时会把当前线程的栈信息记录下来,以便在输出异常时准肯定位异常缘由,咱们来看Throwable源代码。

public class Throwable implements Serializable {
    private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
    //出现异常记录的栈帧
    private StackTraceElement[] stackTrace = UNASSIGNED_STACK;
    //默认构造函数
    public Throwable() {
//记录栈帧 fillInStackTrace(); } //本地方法,抓取执行时的栈信息
private native Throwable fillInStackTrace(int dummy); public synchronized Throwable fillInStackTrace() { if (stackTrace != null || backtrace != null /* Out of protocol state */) { fillInStackTrace(0); stackTrace = UNASSIGNED_STACK; } return this; } }

  在出现异常时(或主动声明一个Throwable对象时),JVM会经过fillInStackTrace方法记录下栈帧信息,而后生成一个Throwable对象,这样咱们就能够知道类间的调用顺序,方法名称及当前行号等了。

  得到栈信息能够对调用者进行判断,而后决定不一样的输出,好比咱们的methodA和methodB方法,一样地输入参数,一样的调用方法,可是输出却不一样,这看起来很想是一个bug:方法methodA调用method方法正常显示,而方法methodB调用却会返回错误数据,所以咱们虽然能够根据调用者的不一样产生不一样的逻辑,但这仅局限在对此方法的普遍认知上,更多的时候咱们使用method方法的变形体,代码以下:  

class Foo {
    public static boolean method() {
        // 取得当前栈信息
        StackTraceElement[] sts = new Throwable().getStackTrace();
        // 检查是不是methodA方法调用
        for (StackTraceElement st : sts) {
            if (st.getMethodName().equals("methodA")) {
                return true;
            }
        }
        throw new RuntimeException("除了methodA方法外,该方法不容许其它方法调用");
    }
}

  只是把“return false” 替换成了一个运行期异常,除了methodA方法外,其它方法调用都会产生异常,该方法经常使用做离线注册码校验,让破解者视图暴力破解时,因为执行者不是指望的值,所以会返回一个通过包装和混淆的异常信息,大大增长了破解难度。

建议116:异常只为异常服务

  异常只为异常服务,这是何解?难道异常还能为其它服务不成?确实能,异常本来是正常逻辑的一个补充,可是有时候会被当作主逻辑使用,看以下代码:

//判断一个枚举是否包含String枚举项
    public static <T extends Enum<T>> boolean Contain(Class<T> clz,String name){
        boolean result = false;
        try{
            Enum.valueOf(clz, name);
            result = true;
        }catch(RuntimeException e){
            //只要是抛出异常,则认为不包含
        }
        return result;
    }

  判断一个枚举是否包含指定的枚举项,这里会根据valueOf方法是否抛出异常来进行判断,若是抛出异常(通常是IllegalArgumentException异常),则认为是不包含,若不抛出异常则能够认为包含该枚举项,看上去这段代码很正常,可是其中有是哪一个错误:

  1. 异常判断下降了系统的性能
  2. 下降了代码的可读性,只有详细了解valueOf方法的人才能读懂这样的代码,由于valueOf抛出的是一个非受检异常
  3. 隐藏了运行期可能产生的错误,catch到异常,但没有作任何处理。

  咱们这段代码是用一段异常实现了一个正常的业务逻辑,这致使代码产生了坏味道。要解决从问题也很容易,即不在主逻辑中实使用异常,代码以下:  

    // 判断一个枚举是否包含String枚举项
    public static <T extends Enum<T>> boolean Contain(Class<T> clz, String name) {
        // 遍历枚举项
        for (T t : clz.getEnumConstants()) {
            // 枚举项名称是否相等
            if (t.name().equals(name)) {
                return true;
            }
        }
        return false;
    }

  异常只能用在非正常的状况下,不能成为正常状况下的主逻辑,也就是说,异常是是主逻辑的辅助场景,不能喧宾夺主。

  并且,异常虽然是描述例外事件的,但能避免则避免之,除非是确实没法避免的异常,例如: 

public static void main(String[] args) {
        File file = new File("a.txt");
        try {
            FileInputStream fis = new FileInputStream(file);
            // 其它业务处理
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            // 异常处理
        }
    }

  这样一段代码常常在咱们的项目中出现,但常常写并不表明不可优化,这里的异常类FileNotFoundException彻底能够在它诞生前就消除掉:先判断文件是否存在,而后再生成FileInputStream对象,这也是项目中常见的代码:

    public static void main(String[] args) {
        File file = new File("a.txt");
        // 常常出现的异常,能够先作判断
        if (file.exists() && !file.isDirectory()) {
            try {
                FileInputStream fis = new FileInputStream(file);
                // 其它业务处理
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                // 异常处理
            }
        }
    }

  虽然增长了if判断语句,增长了代码量,可是却减小了FileNotFoundException异常出现的概率,提升了程序的性能和稳定性。

建议117:多使用异常,把性能问题放一边

  咱们知道异常是主逻辑的例外逻辑,举个简单的例子来讲,好比我在马路上走(这是主逻辑),忽然开过一辆车,我要避让(这是受检异常,必须处理),继续走着,忽然一架飞机从我头顶飞过(非受检异常),咱们能够选在继续行走(不捕捉),也能够选择指责其噪音污染(捕捉,主逻辑的补充处理),再继续走着,忽然一颗流星砸下来,这没有选择,属于错误,不能作任何处理。这样具有完整例外场景的逻辑就具有了OO的味道,任何一个事务的处理均可能产生非预期的效果,问题是须要以何种手段来处理,若是不使用异常就须要依靠返回值的不一样来进行处理了,这严重失去了面向对象的风格。

  咱们在编写用例文档(User case Specification)时,其中有一项叫作 " 例外事件 ",是用来描述主场景外的例外场景的,例如用户登陆的用例,就会在" 例外事件 "中说明" 连续3此登陆失败即锁定用户帐号 ",这就是登陆事件的一个异常处理,具体到咱们的程序中就是:  

public void login(){
        try{
            //正常登录
        }catch(InvalidLoginException lie){
            //    用户名无效
        }catch(InvalidPasswordException pe){
            //密码错误的异常
        }catch(TooMuchLoginException){
            //屡次登录失败的异常
        }
    }

  如此设计则可让咱们的login方法更符合实际的处理逻辑,同时使主逻辑(正常登陆,try代码块)更加清晰。固然了,使用异常还有不少优势,可让正常代码和异常代码分离、能快速查找问题(栈信息快照)等,可是异常有一个缺点:性能比较慢。

  Java的异常机制确实比较慢,这个"比较慢"是相对于诸如String、Integer等对象来讲的,单单从对象的建立上来讲,new一个IOException会比String慢5倍,这从异常的处理机制上也能够解释:由于它要执行fillInStackTrace方法,要记录当前栈的快照,而String类则是直接申请一个内存建立对象,异常类慢一筹也就在所不免了。

  并且,异常类是不能缓存的,指望先创建大量的异常对象以提升异常性能也是不现实的。

  难道异常的性能问题就没有任何能够提升的办法了?确实没有,可是咱们不能由于性能问题而放弃使用异常,并且通过测试,在JDK1.6下,一个异常对象的建立时间只需1.4毫秒左右(注意是毫秒,一般一个交易是在100毫秒左右),难道咱们的系统连如此微小的性能消耗都不予许吗?

 注意:性能问题不是拒绝异常的借口。

相关文章
相关标签/搜索