建议102:适时选择getDeclaredXXX和getXXX

getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得的是自身类的方法,包括公用的(public)方法、私有(private)方法,而且不受限于访问权限。其它的getDeclaredConstructors和getConstructors、getDeclaredFileds和getFields等于此相似。Java之所以如此处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑发生翻天覆地的变化,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。如果需要列出所有继承自父类的方法,简单先获得父类,然后使用getDeclaredMethods,之后持续递归即可。

建议103:反射访问属性或方法时将Accessible设置为true

Java中通过反射执行一个方法的过程如下:获取一个方法对象,然后根据isAccessible返回值确定是否能够执行,如果返回值为false则需要调用setAccessible(true)最后再调用invoke执行方法:通过反射方法执行方法时,必须在invoke之前检查Accessible属性。这是一个好习惯,也确实该如此,但方法对象的Accessible属性并不是用来决定是否可以访问的。动态修改一个类或执行方法时都会受到Java安全体制的制约,而安全的处理是非常耗资源的(性能非常低),因此对于运行期要执行的方法或要修改的属性就提供了Accessible可选项:由开发者决定是否要逃避安全体系的检查。AccessibleObject是Filed、Method、Constructor的父类,决定其是否可以快速访问而不进行访问控制检查,在AccessibleObject类中是以override变量保存该值的,但是具体是否快速执行时在Method的invoke方法中决定的。Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度的提升系统性能了(当然了,取消了安全检查,也可以运行private方法、访问private属性的)。经过测试,在大量的反射情况下,设置Accessible为true可以提高性能20倍左右。AccessibleObject的其它两个子类Field和Constructor与Method的情形类似:Accessible属性决定Field和Constructor是否受访问控制检查。我们在设置Field或执行Constructor时,务必要设置Accessible为true,这并不仅仅是因为操作习惯的问题,还是为我们的系统性能考虑。

建议104:使用forName动态加载类文件

动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否需要加载一个类,比如从Web上接收一个String参数作为类名,然后在JVM中加载并初始化,这就是动态加载,此动态加载通常是通过Class.forName(String)实现的,只是这个forName方法到底是什么意思呢?一个类文件只有在被加载到内存中才可能生成实例对象,也就是说一个对象的生成必然会经过两个步骤:1)加载到内存中生成Class的实例对象,2)通过new关键字生成实例对象。如果使用的是import关键字产生的依赖包,JVM在启动时会自动加载所有的依赖包的类文件,这没有什么问题,如果好动态加载类文件,就要使用forName的方法了,但问题是我们为什么要使用forName方法动态加载一个类文件呢?那是因为我们不知道生成的实例对象是什么类型(如果知道就不用动态加载),而且方法和属性都不可访问呀。问题又来了:动态加载的意义在于:加载一个类即表示要初始化该类的static变量,特别是static代码块,在这里我们可以做大量的工作,比如注册自己,初始化环境等,这才是我们要重点关注的逻辑,例如如下代码:

public class test {
    public static void main(String[] args) throws ClassNotFoundException {
        //动态加载
        Class.forName("com.study.advice103.Utils");
    }
}
class Utils{
    //静态代码块
    static{
        System.out.println("Do Something.....");
    }
}

没有对Utils做任何初始化,只是通过forName方法加载了Utils类,但是却产生了一个“Do Something.....”的输出,这就是因为Utils类加载后,JVM会自动初始化其static变量和static静态代码块,这是类加载机制所决定的。对于动态加载,最经典的应用是数据库驱动程序的加载片段

//加载驱动
        Class.forName("com.mysql..jdbc.Driver");
        String url="jdbc:mysql://localhost:3306/db?user=&password=";
        Connection conn =DriverManager.getConnection(url);
        Statement stmt =conn.createStatement();

在没有Hibernate和Ibatis等ORM框架的情况下,基本上每个系统都会有这么一个JDBC链接类,然后提供诸如Query、Delete等的方法。Driver的源码

public class Driver extends NonRegisteringDriver
    implements java.sql.Driver
{
  //构造函数
    public Driver()
        throws SQLException
    {
    }
   //静态代码块
    static 
    {
        try
        {
           //把自己注册到DriverManager中
            DriverManager.registerDriver(new Driver());
        }
        catch(SQLException E)
        {
           //异常处理
            throw new RuntimeException("Can't register driver!");
        }
    }
}

该程序的逻辑是这样的:数据库驱动程序已经由NonRegisteringDriver实现了,Driver类只是负责把自己注册到DriverManager中。当程序动态加载该驱动时,也就是执行到Class.forName("com.mysql..jdbc.Driver")时,Driver类会被加载到内存中,于是static代码块开始执行,也就是把自己注册到DriverManager中。需要说明的是,forName只是把一个类加载到内存中,并不保证由此产生一个实例对象,也不会执行任何方法,之所以会初始化static代码,那是由类加载机制所决定的,而不是forName方法决定的。也就是说,如果没有static属性或static代码块,forName就是加载类,没有任何的执行行为。forName只是加载类,并不执行任何代码。

建议105:动态加载不适合数组

如果forName要加载一个类,那它首先必须是一个类8个基本类型排除在外,它们不是一个具体的类;其次,它必须具有可追溯的类路径,否则就会报ClassNotFoundException。在Java中,数组是一个非常特殊的类,虽然它是一个类但没有定义类路径。要想动态创建和访问数组,基本的反射是无法实现的,于是Java就专门定义了一个Array数组反射工具类来实现动态探知数组的功能。

建议106:动态代理可以使代理模式更加灵活

Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。一个静态代理是通过主题角色(Proxy)和具体主题角色(Real Subject)共同实现主题角色(Subject)的逻辑的,只是代理角色把相关的执行逻辑委托给了具体角色而已。

建议107:使用反射增加装饰模式的普适性

装饰模式(Decorator Pattern)的定义是“动态的给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比于生成子类更为灵活”,不过,使用Java的动态代理也可以实现装饰模式的效果,而且其灵活性、适应性都会更强。装饰行为由动态代理实现,实现了对装饰类和被装饰类的完全解耦,提供了系统的扩展性。

建议108:反射让模板方法模式更强大

模板方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单的说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法中被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),它是用了简单的继承和覆写机制。

建议109:不需要太多关注反射效率

反射的效率是非常低的,不到万不得已就不要使用。事实上,这句话前半句是对的,后半句是错的。反射的效率相对于正常的代码执行确实低很多,但它是一个非常有效的运行期工具类,只要代码结构清晰、可读性好那就先开发起来,等到进行性能测试时证明此处性能确实有问题再修改也不迟(一般情况下,反射并不是性能的终极杀手,而代码结构混乱、可读性差则可能会埋下性能隐患)。Java泛型只存在于编译器,那为什么这个工具类可以取得运行期的泛型类型呢?那是因为该工具只支持继承的泛型类,如果是在Java编译时已经确定了泛型类的类型参数,那当然可以通过泛型类获得了。

建议110:提倡异常封装

Java语言的异常处理机制可以确保程序的健壮性,提高系统的可用率,但是Java API提供的异常都是比较低级的(这里的低级是指 " 低级别的 " 异常),只有开发人员才能看的懂,而对于终端用户来说与业务无关,是纯计算机语言的描述,那该怎么办?这就需要对异常进行封装。异常封装有三方面的优点:(1)提高系统的友好性

public static void doStuff() throws FileNotFoundException {
        InputStream is = new FileInputStream("无效文件.txt");     /* 文件操作 */  }

此时doStuff的友好性极差,出现异常时(如果文件不存在),该方法直接把FileNotFoundException异常抛到上层应用中(或者是最终用户),而上层应用(或用户要么自己处理)要么接着抛,最终的结果就是用户不知道这是什么问题,只是知道系统告诉他"  ,解决办法就是封装异常,可以把异常的阅读者分为两类:开发人员和用户。开发人员查找问题,需要打印出堆栈信息,而用户则需要了解具体的业务原因,比如文件太大、不能同时编写文件等,代码如下:

public static void doStuff2() throws MyBussinessException{
        try {
            InputStream is = new FileInputStream("无效文件.txt");
        } catch (FileNotFoundException e) {
            //方便开发人员和维护人员而设置的异常信息
            e.printStackTrace();
            //抛出业务异常
            throw new MyBussinessException();
        }      /* 文件操作 */
    }

(2)提高系统的可维护性,对异常进行分类处理,并进行封装输出不是捕捉直接输出。包装后,维护人员看到这样的异常就有了初步的判断,或者检查配置,或者初始化环境,不需要直接到代码层级去分析了。

public  void doStuff3(){
        try{
            //doSomething
        }catch(FileNotFoundException e){
            log.info("文件未找到,使用默认配置文件....");
            e.printStackTrace();
        }catch(SecurityException e1){
            log.info(" 无权访问,可能原因是......");
            e1.printStackTrace();
        } }

(3)解决Java异常机制自身的缺陷。Java中的异常一次只能抛出一个,比如doStuff方法有两个逻辑代码片段,如果在第一个逻辑片段中抛出异常,则第二个逻辑片段就不再执行了,也就无法抛出第二个异常了,现在的问题是:如何才能一次抛出两个(或多个)异常呢?使用自行封装的异常可以解决该问题

class MyException extends Exception {
    // 容纳所有的异常
    private List<Throwable> causes = new ArrayList<Throwable>();
    // 构造函数,传递一个异常列表
    public MyException(List<? extends Throwable> _causes) {
        causes.addAll(_causes);
    }   // 读取所有的异常
    public List<Throwable> getExceptions() {
        return causes;
    }
}

MyException异常只是一个异常容器,可以容纳多个异常,但它本身并不代表任何异常含义,它所解决的是一次抛出多个异常的问题,具体调用如下:

public void doStuff() throws MyException {
        List<Throwable> list = new ArrayList<Throwable>();
        // 第一个逻辑片段
        try {
            // Do Something
        } catch (Exception e) {
            list.add(e);
        }
        // 第二个逻辑片段
        try {
            // Do Something
        } catch (Exception e) {
            list.add(e);
        }
        // 检查是否有必要抛出异常
        if (list.size() > 0) {
            throw new MyException(list);
        }   }

DoStuff方法的调用者就可以一次获得多个异常了,也能够为用户提供完整的例外情况说明。如Web界面注册时,展现层依次把User对象传递到逻辑层,Register方法需要对各个Field进行校验并注册,例如用户名不能重复,密码必须符合密码策略等,不要出现用户第一次提交时系统显示" 用户名重复 ",在用户修改用户名再次提交后,系统又提示" 密码长度小于6位 " 的情况,这种操作模式下的用户体验非常糟糕,最好的解决办法就是异常封装,建立异常容器,一次性地对User对象进行校验,然后返回所有的异常。

建议111:采用异常链传递异常

设计模式中有一个模式叫做责任链模式(Chain of Responsibility) ,它的目的是将多个对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止,异常的传递处理也应该采用责任链模式。异常需要封装,但仅仅封装还是不够的,还需要传递异常。一个系统友好性的标志是用户对该系统的" 粘性",粘性越高,系统越友好,粘性越低系统友好性越差。比如JavaEE项目一般都有三层结构:持久层,逻辑层,展现层,持久层负责与数据库交互,逻辑层负责业务逻辑的实现,展现层负责UI数据库的处理,有这样一个模块:用户第一次访问的时候,需要从持久层user.xml中读取信息,如果该文件不存在则提示用户创建之,如果我们直接把持久层的异常FileNotFoundException抛弃掉,逻辑层根本无法得知发生了何事,也就不能为展现层提供一个友好的处理结果了,最终倒霉的就是发展层:没有办法提供异常信息,只能告诉用户说“出错了,我也不知道出什么错了”毫无友好性可言。正确的做法是先封装,然后传递,过程如下:(1)把FIleNotFoundException封装为MyException。
(2)抛出到逻辑层,逻辑层根据异常代码(或者自定义的异常类型)确定后续处理逻辑,然后抛出到展现层。
(3)展现层自行决定要展现什么,如果是管理员则可以展现低层级的异常,如果是普通用户则展示封装后的异常。
使用异常链进行异常的传递,以IOException为例来看看是如何传递的,代码如下:

public class IOException extends Exception {
    public IOException() {
        super();
    }
    //定义异常原因
    public IOException(String message) {
        super(message);
    }
    //定义异常原因,并携带原始异常
    public IOException(String message, Throwable cause) {
        super(message, cause);
    }
    //保留原始异常信息
    public IOException(Throwable cause) {
        super(cause);
    }
}

在IOException的构造函数中,上一个层级的异常可以通过异常链进行传递,链中传递异常的代码如下所示:
 try{           //doSomething      }catch(Exception e){         throw new IOException(e);      }

捕捉到Exception异常,然后把它转化为IOException异常并抛出(此种方式也叫作异常转译),调用者获得该异常后再调用getCause方法即可获得Exception的异常信息,如此即可方便地查找到产生异常的基本信息,便于解决问题。异常需要封装和传递,在进行系统开发时不要" 吞噬 " 异常,也不要赤裸裸的抛出异常,封装后再抛出,或者通过异常链传递,可以达到系统更健壮,更友好的目的。

建议112:受检异常尽可能转化为非受检异常

之所以尽可能因为" 把所有的受检异常(Checked Exception)"都转化为非受检异常(Unchecked Exception)" 这一想法是不现实的:受检异常是正常逻辑的一种补偿手段,特别是对可靠性要求比较高的系统来说,在某些条件下必须抛出受检异常以便由程序进行补偿处理,也就是说受检异常有合理存在的理由,那为什么要把受检异常转化为非受检异常呢?受检异常有不足:(1)受检异常使接口声明脆弱:OOP(Object Oriented Programming,面向对象程序设计) 要求我们尽量多地面向接口编程,可以提高代码的扩展性、稳定性等,但是涉及异常问题就不一样了,例如系统初期是这样设计一个接口的:interface User{
    //修改用户密码,抛出安全异常
    public void changePassword() throws MySecurityException;
}随着开发接口实现着增加如果发现changePassword方法可能还需要抛出RejectChangeException(拒绝修改异常,如自动执行正在处理的任务时不能修改其代码),那就需要修改User接口了:changePassword方法增加抛出RejectChangeException异常,这会导致所有的User调用者都要追加了对RejectChangeException异常问题的处理。
这里产生了两个问题:一、异常是主逻辑的补充逻辑,修改一个补充逻辑,就会导致主逻辑也被修改,也就是出现了实现类 " 逆影响 " 接口的情景,我们知道实现类是不稳定的,而接口是稳定的,一旦定义了异常,则增加了接口的不稳定性,这是面向对象设计的严重亵渎;二、实现的变更最终会影响到调用者,破坏了封装性,这也是迪米特法则所不能容忍的。(2)受检异常使代码的可读性降低,一个方法增加可受检异常,则必须有一个调用者对异常进行处理代码膨胀许多,可读性也降低了,特别是在多个异常需要捕捉的情况下,多个catch块多个异常处理,而且还可能在catch块中再次抛出异常,这大大降低了代码的可读性。(3)受检异常增加了开发工作量,异常需要封装和传递,只有封装才能让异常更容易理解,上层模块才能更好的处理,可这会导致低层级的异常没完没了的封装,无端加重了开发的工作量。比如FileNotFoundException进行封装,并抛出到上一个层级,于是增加了开发工作量。
受检的缺点避免或减少:很简单的一个规则将受检异常转化为非受检异常即可,但是我们也不能把所有的受检异常转化为非受检异常,原因是在编码期上层模块不知道下层模块会抛出何种非受检异常,只有通过规则或文档来描述,可以这样说:1)受检异常提出的是" 法律下的自由 ",必须遵守异常的约定才能自由编写代码。2)非受检异常则是“ 协约性质的自由 ”,你必须告诉我你要抛什么异常,否则不会处理。
以User接口为例,我们在声明接口时不再声明异常,而是在具体实现时根据不同的情况产生不同的非受检异常,这样持久层和逻辑层抛出的异常将会由展现自行决定如何展示,不再受异常的规则约束了,大大简化开发工作,提高了代码的可读性。
" 尽可能 " 是以什么作为判断依据呢?受检异常转换为非受检异常是需要根据项目的场景来决定的,例如同样是刷卡,员工拿着自己的工卡到考勤机上打考勤,此时如果附近有磁性物质干扰,则考勤机可以把这种受检异常转化为非受检异常,黄灯闪烁后不做任何记录登记,因为考勤失败这种情景不是" 致命 "的业务逻辑,出错了,重新刷一下即可。但是到银行网点取钱就不一样了,拿着银行卡到银行取钱,同样有磁性物质干扰,刷不出来,那这种异常就必须登记处理,否则会成为威胁银行卡安全的事件。汇总成一句话:当受检异常威胁到了系统的安全性,稳定性,可靠性、正确性时,则必须处理,不能转化为非受检异常,其它情况则可以转化为非受检异常。
注意:受检异常威胁到系统的安全性,稳定性、可靠性、正确性时,不能转换为非受检异常。

建议113:不要在finally块中处理返回值

public class test113{
    public static void main(String[] args) {
        try {
            System.out.println(doStuff(-1));
            System.out.println(doStuff(100));
        } catch (Exception e) {
            System.out.println("这里是永远不会到达的");
        }
    }
    //该方法抛出受检异常
    public static int doStuff(int _p) throws Exception {
        try {
            if (_p < 0) {
                throw new DataFormatException(" 数据格式错误 ");
            } else {
                return _p;
            }

        } catch (Exception e) {
            // 异常处理
            throw e;
        } finally {
            return -1;
        }
    }
}

main方法中的doStuff方法的返回值是什么?doStuff方法永远都不会抛出异常吗?答案是:doStuff(-1)的值是-1,doStuff(100)的值也是-1,调用doStuff方法永远都不会抛出异常,有这么神奇?原因就是我们在finally代码块中加入了return语句,而这会导致出现以下两个问题:(1)覆盖了try代码块中的return返回值。当执行doStuff(-1)时,doStuff方法产生了DataFormatException异常,catch块在捕捉此异常后直接抛出,之后代码执行到finally代码块,就会重置返回值,结果就是-1了。也就是出现先返回,再重置返回的情况,有人可能会思考,是不是可以定义变量,在finally中修改后return呢

public static int doStuff() {
        int a = 1;
        try {
            return a;
        } catch (Exception e) {

        } finally {
            // 重新修改一下返回值
            a = -1;
        }
        return 0;
    }

该方法的返回值永远是1,不会是-1或0(为什么不会执行到" return 0 " 呢?原因是finally执行完毕后该方法已经有返回值了,后续代码就不会再执行了),这都是源于异常代码块的处理方式,在代码中try代码块就标志着运行时会有一个Throwale线程监视着该方法的运行,若出现异常,则交由异常逻辑处理。方法是在栈内存中运行的,并且会按照“ 先进后出 ”的原则执行,main方法调用了doStuff方法,则main方法在下层,doStuff方法在上层,当doStuff方法执行完" return a " 时,此方法的返回值已经确定int类型1(a变量的值,注意基本类型都是拷贝值,而不是引用),此时finally代码块再修改a的值已经与doStuff返回者没有任何关系了,因此该方法永远都会返回1。(2)屏蔽异常为什么明明把异常throw出去了,但main方法却捕捉不到呢?这是因为异常线程在监视到有异常发生时,就会登记当前的异常类型为DataFormatException,但是当执行器执行finally代码块时,则会重新为doStuff方法赋值,也就是告诉调用者" 该方法执行正确,没有产生异常,返回值为1 ",于是乎,异常神奇的消失了,其简化代码如下所示:

public static void doSomeThing(){
        try{
            //正常抛出异常
            throw new RuntimeException();
        }finally{
            //告诉JVM:该方法正常返回
            return;
        }
    }
public static void main(String[] args) {
        try {
            doSomeThing();
        } catch (RuntimeException e) {
            System.out.println("这里是永远不会到达的");
        }
    }

上面finally代码块中的return已经告诉JVM:doSomething方法正常执行结束,没有异常,所以main方法就不可能获得任何异常信息了。这样的代码会使可读性大大降低,读者很难理解作者的意图,增加了修改的难度。在finally中处理return返回值,代码看上去很完美,都符合逻辑,但是执行起来就会产生逻辑错误,最重要的一点是finally是用来做异常的收尾处理的,一旦加上了return语句就会让程序的复杂度徒然上升,而且会产生一些隐蔽性非常高的错误。与return语句相似,System.exit(0)或RunTime.getRunTime().exit(0)出现在异常代码块中也会产生非常多的错误假象,增加代码的复杂性。

建议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方法。

建议119:启动线程前stop方法是不可靠的

不使用stop方法进行状态的设置,直接通过判断条件来决定线程是否可启用。对于start方法的缺陷,一般不会引起太大的问题,只是增加了线程启动和停止的精度而已。

建议120:不使用stop方法停止线程

线程启动完毕后,在运行时可能需要中止,Java提供的终止方法只有一个stop,但是不建议使用这个方法,因为它有以下三个问题:(1)stop方法是过时的:从Java编码规则来说,已经过时的方法不建议采用。
(2)stop方法会导致代码逻辑不完整:stop方法是一种" 恶意 " 的中断,一旦执行stop方法,即终止当前正在运行的线程不管线程逻辑是否完整,这是非常危险的。(3)stop方法会破坏原子逻辑:多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是正因为此,stop方法却会带来更大的麻烦,它会丢弃所有的锁,导致原子逻辑受损。既然终止一个线程不能使用stop方法,那怎样才能终止一个正在运行的线程呢?答案也简单,使用自定义的标志位决定线程的执行情况

class SafeStopThread extends Thread {
    // 此变量必须加上volatile
    /*
     * volatile: 1.作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.
     * 2.被设计用来修饰被不同线程访问和修改的变量。如果不加入volatile
     * ,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会。
     */
    private volatile boolean stop = false;
    @Override
    public void run() {
        // 判断线程体是否运行
        while (stop) {
            // doSomething
        }
    }
    public void terminate() {
        stop = true;
    }
}

这是很简单的办法,在线程体中判断是否需要停止运行,即可保证线程体的逻辑完整性,而且也不会破坏原子逻辑,Thread还提供了interrupt中断线程的方法它不能终止一个正在执行着的线程,它只是修改中断标志而已。总之,如果期望终止一个正在运行的线程,则不能使用已过时的stop方法。需要自行编码实现,如此即可保证原子逻辑不被破坏,代码逻辑不会出现异常。当然,如果我们使用的是线程池(比如ThreadPoolExecutor类),那么可以通过shutdown方法逐步关闭池中的线程,它采用的是比较温和、安全的关闭线程方法,完全不会产生类似stop方法的弊端。

建议121:线程优先级只使用三个等级

线程的优先级(Priority)决定了线程获得CPU运行的机会,优先级越高获得的运行机会越大,优先级越低获得的机会越小。Java的线程有10个级别(准确的说是11个级别,级别为0的线程是JVM的,应用程序不能设置该级别),那是不是说级别是10的线程肯定比级别是9的线程先运行呢?我们来看如下一个多线程类:

class TestThread implements Runnable {
    public void start(int _priority) {
        Thread t = new Thread(this);
        // 设置优先级别
        t.setPriority(_priority);
        t.start();
    }
    @Override
    public void run() {
        // 消耗CPU的计算
        for (int i = 0; i < 100000; i++) {
            Math.hypot(924526789, Math.cos(i));
        }
        // 输出线程优先级
        System.out.println("Priority:" + Thread.currentThread().getPriority());
    }
}
public static void main(String[] args) {
        //启动20个不同优先级的线程
        for (int i = 0; i < 20; i++) {
            new TestThread().start(i % 10 + 1);
        }
    }

该多线程实现了Runnable接口,实现了run方法,注意在run方法中有一个比较占用CPU的计算,该计算毫无意义 这里创建了20个线程,每个线程在运行时都耗尽了CPU的资源,因为优先级不同,线程调度应该是先处理优先级高的,然后处理优先级低的,也就是先执行2个优先级为10的线程,然后执行2个优先级为9的线程,2个优先级为8的线程......但是结果却并不是这样Priority:5  Priority:7 Priority:10 Priority:6 Priority:9。。。(1)并不是严格按照线程优先级来执行的,比如线程优先级为5的线程比优先级为7的线程先执行,优先级为1的线程比优先级为2的线程先执行,很少出现优先级为2的线程比优先级为10的线程先执行(注意,这里是" 很少 ",是说确实有可能出现,只是几率低,因为优先级只是表示线程获得CPU运行的机会,并不代表强制的排序号)。(2)优先级差别越大,运行机会差别越明显,比如优先级为10的线程通常会比优先级为2的线程先执行,但是优先级为6的线程和优先级为5的线程差别就不太明显了,执行多次,你会发现有不同的顺序。这两个现象是线程优先级的一个重要表现,之所以会出现这种情况,是因为线程运行是需要获得CPU资源的,那谁能决定哪个线程先获得哪个线程后获得呢?这是依照操作系统设置的线程优先级来分配的,也就是说,每个线程要运行,需要操作系统分配优先级和CPU资源,对于JAVA来说JVM调用操作系统的接口设置优先级,比如windows操作系统优先级都相同吗?事实上,不同的操作系统线程优先级是不同的,Windows有7个优先级,Linux有140个优先级,Freebsd则由255个(此处指的优先级个数,不同操作系统有不同的分类,如中断级线程,操作系统级等,各个操作系统具体用户可用的线程数量也不相同)。Java是跨平台的系统,需要把这10个优先级映射成不同的操作系统的优先级,于是界定了Java的优先级只是代表抢占CPU的机会大小,优先级越高,抢占CPU的机会越大,被优先执行的可能性越高,优先级相差不大,则抢占CPU的机会差别也不大,这就是导致了优先级为9的线程可能比优先级为10的线程先运行。Java的缔造者们也觉察到了线程优先问题,于是Thread类中设置了三个优先级,此意就是告诉开发者,建议使用优先级常量。在编码时直接使用这些优先级常量,可以说在大部分情况下MAX_PRIORITY的线程会比MIN_PRIORITY的线程优先运行,但是不能认为是必然会,不能把这个优先级做为核心业务的必然条件,Java无法保证优先级高肯定会先执行,只能保证高优先级有更多的执行机会。因此,建议在开发时只使用此三类优先级( MIN_PRIORITY 、NORM_PRIORITY 、MAX_PRIORITY ),没有必要使用其他7个数字,这样也可以保证在不同的操作系统上优先级的表现基本相同。如果优先级相同呢?这很好办,也是由操作系统决定的。基本上是按照FIFO原则(先入先出,First Input First Output),但也是不能完全保证。

建议122:使用线程异常处理器提升系统可靠性

编写一个Socket应用,监听指定端口,实现数据包的接收和发送逻辑,这在早期系统间进行数据交互是经常使用的,这类接口通常需要考虑两个问题:一个是避免线程阻塞,保证接收的数据尽快处理;二是:接口的稳定性和可靠性问题,数据包很复杂,接口服务的系统也很多,一旦守候线程出现异常就会导致Socket停止,这是非常危险的,那有什么办法避免?Java1.5版本以后在Thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理。如果Socket应用出现了不可预测的异常是否可以自动重启呢?其实使用线程异常处理器很容易解决,来看一个异常处理器应用实例,代码如下:

class TcpServer implements Runnable {
    // 创建后即运行
    public TcpServer() {
        Thread t = new Thread(this);
        t.setUncaughtExceptionHandler(new TcpServerExceptionHandler());
        t.start();
    }
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(1000);
                System.out.println("系统正常运行:" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }      // 抛出异常
        throw new RuntimeException();
    }
    // 异常处理器
    private static class TcpServerExceptionHandler implements
            Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            // 记录线程异常信息
            System.out.println("线程" + t.getName() + " 出现异常,自行重启,请分析原因。");
            e.printStackTrace();
            // 重启线程,保证业务不中断
            new TcpServer();
        }
    }
}

这段代码的逻辑比较简单,在TcpServer类创建时启动一个线程,提供TCP服务,例如接收和发送文件,具体逻辑在run方法中实现。同时设置了该线程出现运行期异常(也就是Uncaught Exception)时,由TcpServerExceptionHandler异常处理器来处理异常。那么TcpServerExceptionHandler做什么事呢?两件事:1、记录异常信息,以便查找问题
2、重新启动一个新线程,提供不间断的服务。有了这两点,TcpServer就可以稳定的运行了,即使出现异常也能自动重启,客户端代码比较简单,只需要new TcpServer()即可。结果分析出当Thread-0出现异常时,系统自动重启了Thread-1线程,继续提供服务,大大提高了系统的性能,在实际环境中应用,则需要注意以下三个方面:
1、共享资源锁定:如果线程产生异常的原因是资源被锁定,自动重启会增加系统的负担,无法提供不间断服务。例如一个即时通信服务(XMPP Server)出现信息不能写入的情况,即使再怎么启动服务,也是无法解决问题的。在此情况下最好的办法是停止所有的线程,释放资源。2、脏数据引起系统逻辑混乱:异常的产生中断了正在执行的业务逻辑,特别是如果正在处理一个原子操作(像即时通讯服务器的用户验证和签到这两个事件应该在一个操作中处理,不允许出现验证成功,但签到不成功的情况),但如果此时抛出了运行期异常就有可能会破坏正常的业务逻辑,例如出现用户认证通过了,但签到不成功的情况,在这种情境下重启应用服务器,虽然可以提供服务,但对部分用户却产生了逻辑异常。3、内存溢出:线程异常了,但由该线程创建的对象并不会马上回收,如果再重亲启动新线程,再创建一批对象,特别是加入了场景接管,就非常危险了,例如即时通信服务,重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务重启,在此种情况下,就需要在线程初始化时加载大量对象以保证用户的状态信息,但是如果线程反复重启,很可能会引起OutOfMemory内存泄露问题。