编写java程序151条建议读书笔记(15)

来源:互联网 发布:程序员所说的接口 编辑:程序博客网 时间:2024/06/05 07:02

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

Java异常的机制有三种:1)Error类及其子类表示的是错误,它是不需要程序员处理也不能处理的异常,比如VirtualMachineError虚拟机错误,ThreadDeath线程僵死等。2)RunTimeException类及其子类表示的是非受检异常,是系统可能会抛出的异常,程序员可以去处理,也可以不处理,最经典的就是NullPointException空指针异常和IndexOutOfBoundsException越界异常。
 3)Exception类及其子类(不包含非受检异常),表示的是受检异常,这是程序员必须处理的异常,不处理则程序不能通过编译,比如IOException表示的是I/O异常,SQLException表示的数据库访问异常。  
一个对象的创建过程经过内存分配、静态代码初始化、构造函数执行等过程,对象生成的关键步骤是构造函数,从Java语法上来说,完全可以在构造函数中抛出异常,三类异常都可以,但是从系统设计和开发的角度来分析,则尽量不要在构造函数中抛出异常,以三种不同类型的异常来说明。(1)构造函数中抛出错误是程序员无法处理的。在构造函数执行时,若发生了VirtualMachineError虚拟机错误,那就没招了,只能抛出,程序员不能预知此类错误的发生,也就不能捕捉处理。(2)构造函数不应该抛出非受检异常,构造函数中含有异常的话。1)加重了上层代码编写者的负担:捕捉这个RuntimeException异常吧,那谁来告诉我有这个异常呢?只有通过文档约束了,一旦构造函数经过重构后再抛出其它非受检异常,那main方法不用修改也是可以测试通过的,但是这里就可能会产生隐藏的缺陷,而写还是很难重现的缺陷。不捕捉这个RuntimeException异常,这个是我们通常的想法,既然已经写成了非受检异常,main方法的编码者完全可以不处理这个异常嘛,大不了不执行类的方法!这是非常危险的,一旦产生异常,整个线程都不再继续执行,或者链接没有关闭,或者数据没有写入数据库,或者产生内存异常,这些都是会对整个系统产生影响。
2)后续代码不会执行:main方法的实现者原本是想把p对象的建立作为其代码逻辑的一部分,执行完doSomething方
法后还需要完成其它逻辑,但是因为没有对非受检异常进行捕捉,异常最终会抛出到JVM中,这会导致整个线程执行结束后后面所有的代码都不会继续执行了,这就对业务逻辑产生了致命的影响。(3)构造函数尽可能不要抛出受检异常

//父类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类,但是这里不能替换原因是Sub的构造函数抛出了Exception异常,它比父类的构造函数抛出更多的异常范围要宽,必须增加新的catch块才能解决。为什么Java的构造函数允许子类的构造函数抛出更广泛的异常类呢?这正好与类方法的异常机制相反。子类的方法可以抛出多个异常,但都必须是覆写方法的子类型,对我们的例子来说,Sub类的testMethod方法抛出的异常必须是Exception的子类或Exception类,这是Java覆写的要求。构造函数之所以于此相反,是因为构造函数没有覆写的概念,只是构造函数间的引用调用而已,所以在构造函数中抛出受检异常会违背里氏替换原则原则,使我们的程序缺乏灵活性。3)子类构造函数扩展受限:子类存在的原因就是期望实现扩展父类的逻辑,但父类构造函数抛出异常却会让子类构造函数的灵活性大大降低。将以上三种异常类型汇总起来,对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了 " 对己对人 " 都是有害的;受检异常尽量不抛出,能用曲线的方式实现就用曲线方式实现,总之一句话:在构造函数中尽可能不出现异常。注意 :在构造函数中不要抛出异常,尽量曲线实现。

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

AOP编程可以很轻松的控制一个方法调用哪些类,也能够控制哪些方法允许被调用,一般来说切面编程(比如AspectJ),只能控制到方法级别,不能实现代码级别的植入(Weave),比如一个方法被类A的m1方法调用时返回1,在类B的m2方法调用时返回0(同参数情况下),这就要求被调用者具有识别调用者的能力。在这种情况下,可以使用Throwable获得栈信息,然后鉴别调用者并分别输出,在出现异常时(或主动声明一个Throwable对象时),JVM会通过fillInStackTrace方法记录下栈帧信息,然后生成一个Throwable对象,这样我们就可以知道类间的调用顺序,方法名称及当前行号等了。

建议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;    }
异常只能用在非正常的情况下,不能成为正常情况下的主逻辑,也就是说,异常是是主逻辑的辅助场景,不能喧宾夺主。而且,异常虽然是描述例外事件的,但能避免则避免之,除非是确实无法避免的异常。

建议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毫秒左右),难道我们的系统连如此微小的性能消耗都不予许吗?注意:性能问题不是拒绝异常的借口。

建议118:不推荐覆写start方法

多线程比较简单的实现方式是继承Thread类,然后覆写run方法,在客户端程序中通过调用对象的start方法即可启动一个线程,这是多线程程序的标准写法。

class MultiThread extends Thread{    @Override    public synchronized void start() {        //调用线程体        run();    }    @Override    public void run() {        //MultiThread do someThing    }}public static void main(String[] args) {        //多线程对象        MultiThread m = new MultiThread();        //启动多线程        m.start();    }

线程demo覆写run方法,写上自己的业务逻辑即可,但为什么要覆写start方法呢?最常见的理由是:要在客户端调用start方法启动线程,不覆写start方法怎么启动run方法呢?于是乎就覆写了start方法,在方法内调用run方法。这是一个错误的多线程应用,main方法根本就没有启动一个子线程,整个应用程序中只有一个主线程在运行,并不会创建任何其它的线程。对此,有很简单的解决办法。只要删除MultiThread类的start方法即可。通过看Thread类的start方法的代码,关键是本地方法start0,它实现了启动线程、申请栈内存、运行run方法、修改线程状态等职责,线程管理和栈内存管理都是由JVM负责的,如果覆盖了start方法,也就是撤销了线程管理和栈内存管理的能力,这样如何启动一个线程呢?事实上,不需要关注线程和栈内存的管理,主需要编码者实现多线程的逻辑即可(即run方法体),这也是JVM比较聪明的地方,简化多线程应用。确实有必要覆写start方法,只要在start方法中加上super.start()即可,此时调用了父类的start方法,没有主动调用run方法,这是由JVM自行调用的,不用显示实现,而且是一定不能实现。此方式虽然解决了" 覆写start方法 "的问题,但是基本上无用武之地,到目前为止还没有发现一定要覆写start方法的多线程应用,所以要求覆写start的场景。都可以使用其他的方式实现,例如类变量、事件机制、监听等方式。注意:继承自Thread类的多线程类不必覆写start方法。


阅读全文
0 0
原创粉丝点击